Federation SDL is invalid for non-Apollo tools
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.
Yeah this has been annoying me for a bit, but I've been ignoring it. Does Apollo have any guidance here?
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 Interesting. Our sdl field calls our federation_sdl helper, which does not include those definitions, so we should probably add them...