apollo-federation-ruby icon indicating copy to clipboard operation
apollo-federation-ruby copied to clipboard

Federation SDL is invalid for non-Apollo tools

Open jturkel opened this issue 2 years ago • 3 comments

I'm not sure if this is intentional but the generated federation SDL is invalid for non-Apollo tools because it doesn't include the federation directive definitions. Here's a minimal reproducible test case:

require 'bundler/inline'

gemfile do
  source 'https://rubygems.org'
  gem 'graphql', '2.0.22'
  gem 'apollo-federation', '3.8.0'
end

class ProductType < GraphQL::Schema::Object
  include ApolloFederation::Object
  key fields: :upc

  field :upc, String, null: false
end

class QueryType < GraphQL::Schema::Object
  field :product, ProductType, null: false
end

class ProductSchema < GraphQL::Schema
  include ApolloFederation::Schema
  federation version: '2.0'
  query QueryType
end

sdl = ProductSchema.federation_sdl
puts sdl
GraphQL::Schema.from_definition(sdl)

This fails with the following exception:

/Users/jturkel/.rbenv/versions/3.2.1/lib/ruby/gems/3.2.0/gems/graphql-2.0.22/lib/graphql/schema/build_from_definition.rb:244:in `block in prepare_directives': No definition for @federation__key on Product at 4:1 (ArgumentError)

I've confirmed graphql-js behaves the same way:

const { buildSchema, printSchema, validateSchema } = require('graphql');

const schemaDefinition = `
# Uncomment the following the schema will be valid
# scalar federation__FieldSet
# scalar link__Import
# enum link__Purpose {
#   SECURITY
#   EXECUTION
# }
# directive @link(url: String!, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA
# directive @federation__key(fields: federation__FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE

extend schema
  @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@inaccessible", "@tag"])

type Product @federation__key(fields: "upc") {
  upc: String!
}

type Query {
  product: Product!
}
`;

const schema = buildSchema(schemaDefinition);
const validationErrors = validateSchema(schema);
if (validationErrors.length > 0) {
  console.error('Schema validation errors:');
  validationErrors.forEach((error) => console.error(error.message));
} else {
  console.log('Schema is valid.');
}

This fails with the following exception:

/Users/jturkel/code/bug_test_cases/node_modules/graphql/validation/validate.js:135
    throw new Error(errors.map((error) => error.message).join('\n\n'));
    ^

Error: Unknown directive "@link".

Unknown directive "@federation__key".
    at assertValidSDL (/Users/jturkel/code/bug_test_cases/node_modules/graphql/validation/validate.js:135:11)
    at buildASTSchema (/Users/jturkel/code/bug_test_cases/node_modules/graphql/utilities/buildASTSchema.js:44:34)
    at buildSchema (/Users/jturkel/code/bug_test_cases/node_modules/graphql/utilities/buildASTSchema.js:109:10)
    at Object.<anonymous> (/Users/jturkel/code/bug_test_cases/validate_schema.js:26:16)

I've also confirmed that the Apollo tools work fine if the federation directive definitions are included in the SDL.

jturkel avatar Jun 15 '23 19:06 jturkel

Yeah this has been annoying me for a bit, but I've been ignoring it. Does Apollo have any guidance here?

grxy avatar Jun 20 '23 16:06 grxy

The Apollo Federation spec says to include all uses of federation-specific directives but there's no mention of the directive definitions, although all of the spec examples omit the directive definitions.

I followed the @apollo/subgraph setup instructions to create a simple test server using the official Apollo libraries:

import gql from 'graphql-tag';
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone'
import { buildSubgraphSchema } from '@apollo/subgraph';

const typeDefs = gql`
  extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key", "@shareable"])

  type Query {
    me: User
  }

  type User @key(fields: "id") {
    id: ID!
    username: String
  }
`;

const resolvers = {};

const server = new ApolloServer({
  schema: buildSubgraphSchema({ typeDefs, resolvers }),
});

const { url } = await startStandaloneServer(server, {
  listen: { port: 4000 },
});

console.log(`🚀  Server ready at: ${url}`);

Querying Query._service.sdl returns a schema with federation directive definitions:

schema
  @link(url: "https://specs.apollo.dev/link/v1.0")
{
  query: Query
}

extend schema
  @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key", "@shareable"])

directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA

directive @key(fields: federation__FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE

directive @federation__requires(fields: federation__FieldSet!) on FIELD_DEFINITION

directive @federation__provides(fields: federation__FieldSet!) on FIELD_DEFINITION

directive @federation__external(reason: String) on OBJECT | FIELD_DEFINITION

directive @federation__tag(name: String!) repeatable on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION

directive @federation__extends on OBJECT | INTERFACE

directive @shareable on OBJECT | FIELD_DEFINITION

directive @federation__inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION

directive @federation__override(from: String!) on FIELD_DEFINITION

type Query {
  me: User
  _entities(representations: [_Any!]!): [_Entity]!
  _service: _Service!
}

type User
  @key(fields: "id")
{
  id: ID!
  username: String
}

enum link__Purpose {
  """
  `SECURITY` features provide metadata necessary to securely resolve fields.
  """
  SECURITY

  """
  `EXECUTION` features provide metadata necessary for operation execution.
  """
  EXECUTION
}

scalar link__Import

scalar federation__FieldSet

scalar _Any

type _Service {
  sdl: String
}

union _Entity = User

So in summary, the Apollo tooling works fine with or without the directive definitions but their reference implementation does include the directives the Query._service.sdl field.

jturkel avatar Jun 26 '23 17:06 jturkel

@jturkel Interesting. Our sdl field calls our federation_sdl helper, which does not include those definitions, so we should probably add them...

grxy avatar Jul 11 '23 16:07 grxy