Can not print schema for a subgraph: GraphQLError: Type User must define one or more fields
I want to be able to generate a schema of a subgraph in order to later feed it into rover compose. I want to be able to use code-first approach in nest.
Is there an existing issue for this?
- [X] I have searched the existing issues
Current behavior
I am following this guide https://docs.nestjs.com/graphql/generating-sdl and this issue https://github.com/nestjs/graphql/issues/1597 but the generated schema does not contain relevant directives.
Minimum reproduction code
https://github.com/maxkomarychev/rover-compose-problem-demo
Steps to reproduce
- clone repo
-
npm i -
npx ts-node generate-schema.ts - obseve:
err: (message: string, options?: GraphQLErrorOptions) => new GraphQLError(
^
GraphQLError: Type User must define one or more fields.
update 1
after experimenting a bit I got rid of the problem by removing directive @Directive('@key(fields: "id")') but I do need it for federation :)
Expected behavior
I way to generate a schema of a subgraph with all relevant directives, like so:
expected schema
schema
@link(url: "https://specs.apollo.dev/link/v1.0")
{
query: Query
mutation: Mutation
}
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@composeDirective", "@extends", "@external", "@inaccessible", "@interfaceObject", "@key", "@override", "@provides", "@requires", "@shareable", "@tag"])
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 @requires(fields: federation__FieldSet!) on FIELD_DEFINITION
directive @provides(fields: federation__FieldSet!) on FIELD_DEFINITION
directive @external(reason: String) on OBJECT | FIELD_DEFINITION
directive @tag(name: String!) repeatable on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION | SCHEMA
directive @extends on OBJECT | INTERFACE
directive @shareable repeatable on OBJECT | FIELD_DEFINITION
directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION
directive @override(from: String!) on FIELD_DEFINITION
directive @composeDirective(name: String) repeatable on SCHEMA
directive @interfaceObject on OBJECT
type User
@key(fields: "id")
{
id: ID!
name: String!
dob: String!
}
type Query {
users: [User!]!
user(id: ID!): User!
_entities(representations: [_Any!]!): [_Entity]!
_service: _Service!
}
type Mutation {
createUser(name: String!): User!
}
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
Package version
11.0.4
Graphql version
graphql: 16.6.0
apollo-server-express:
apollo-server-fastify:
NestJS version
9.3.12
Node.js version
18.9.0
In which operating systems have you tested?
- [X] macOS
- [ ] Windows
- [ ] Linux
Other
No response<
Ran into this as well. I managed to get it working by using the full GraphQLModule and its GraphQLSchemaHost export instead like so:
import { printSubgraphSchema } from "@apollo/subgraph";
import { NestFactory } from "@nestjs/core";
import { GraphQLModule, GraphQLSchemaHost } from "@nestjs/graphql";
import {
ApolloFederationDriver,
ApolloFederationDriverConfig,
} from "@nestjs/apollo";
async function generateSchema() {
const app = await NestFactory.create(
GraphQLModule.forRoot<ApolloFederationDriverConfig>({
driver: ApolloFederationDriver,
autoSchemaFile: {
federation: 2,
},
include: [ModuleContainingYourResolvers],
})
);
await app.init();
const gqlSchemaFactory = app.get(GraphQLSchemaHost);
console.log(printSubgraphSchema(gqlSchemaFactory.schema));
}
EDIT: I think I spoke too soon. While this does add import for federation directives, it does not appear to include all types, e.g. Query is missing.
EDIT2: Turns out the 'include' option is a whitelist of modules that should be part of the module graph, hence it acted like there were no resolvers. Weirdly it still found the models themselves, which fooled me.
Here's a new version, although it does have some caveats:
import { printSubgraphSchema } from "@apollo/subgraph";
import { NestFactory } from "@nestjs/core";
import { GraphQLModule, GraphQLSchemaHost } from "@nestjs/graphql";
import {
ApolloFederationDriver,
ApolloFederationDriverConfig,
} from "@nestjs/apollo";
import { Module } from '@nestjs/common';
async function generateSchema() {
@Module({
imports: [
GraphQLModule.forRoot<ApolloFederationDriverConfig>({
driver: ApolloFederationDriver,
autoSchemaFile: {
federation: 2,
},
}),
],
providers: [
MyResolver,
],
})
class AppModule {}
const app = await NestFactory.create(AppModule);
await app.init();
const gqlSchemaFactory = app.get(GraphQLSchemaHost);
console.log(printSubgraphSchema(gqlSchemaFactory.schema));
}
Unfortunately it requires that your resolvers have no dependencies that needs to be injected, so they can be instantiated. Otherwise you'll need to either add its dependencies or mock them. Hence it's not really a solution, just a workaround until someone smarter drops by with a proper solution 🤞 .
thanks for the answer @Phault !
Unfortunately none of that works for me - I am only getting this:
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@composeDirective", "@extends", "@external", "@inaccessible", "@interfaceObject", "@key", "@override", "@provides", "@requires", "@shareable", "@tag"])
it doesn't contain anything at all from my types (neither actual data types, nor mutations)
update ok I managed to make it work by adding my module with resolves to imports rather than provider:
import { NestFactory } from '@nestjs/core';
import { GraphQLModule, GraphQLSchemaHost } from '@nestjs/graphql';
import { printSubgraphSchema } from '@apollo/subgraph';
import {
ApolloFederationDriver,
ApolloFederationDriverConfig,
} from '@nestjs/apollo';
import { UsersModule } from './src/users/users.module';
import { Module } from '@nestjs/common';
async function generateSchema() {
@Module({
imports: [
GraphQLModule.forRoot<ApolloFederationDriverConfig>({
driver: ApolloFederationDriver,
autoSchemaFile: {
federation: 2,
},
}),
UsersModule, // <<<<==== here!!!!!!!!!!!!!
],
})
class AppModule {}
const app = await NestFactory.create(AppModule);
await app.init();
const gqlSchemaHost = app.get(GraphQLSchemaHost);
console.log(printSubgraphSchema(gqlSchemaHost.schema));
}
generateSchema();
that said this still seems a bit hacky, I am eager to learn the right way to do it.
I kind of managed to get it work with lazy modules but it requires breaking down chain of modules https://docs.nestjs.com/fundamentals/lazy-loading-modules.
graph LR
UsersModule --> |providers| UsersResolver
UsersModule --> |providers| UsersService
UsersResolver -->|constructor injection| UsersService
graph LR
UsersModule -->|providers| UsersResolver
UsersResolverLazyServices -->|providers| UsersService
UsersResolver -->|constructor injection| LazyModuleLoader
UsersResolver -->|runtime resolution on demand via LazyModule| UsersService
i.e.
before:
@Module({
providers: [UsersResolver, UsersService],
})
export class UsersModule {}
export class UsersResolver {
constructor(private readonly usersService: UsersService) {}
...
}
after:
@Module({
providers: [UsersResolver], // <<<<< remove `UsersService` from providers of users module
})
export class UsersModule {}
// introduce new intermediate modules to have all lazy imports
@Module({
providers: [UsersService],
})
class UsersResolverLazyServices {}
export class UsersResolver {
constructor(private readonly loader: LazyModuleLoader) {
}
private _usersService: UsersService;
async getUserService() {
if (!this._usersService) {
const ref = await this.loader.load(() => UsersResolverLazyServices);
this._usersService = await ref.get(UsersService);
}
return this._usersService;
}
...
}
I still wonder though why it doesn't generate everything that I can normally get via a live query:
query {
_service {
sdl
}
}
which returns the following schema:
with query
schema
@link(url: "https://specs.apollo.dev/link/v1.0")
{
query: Query
mutation: Mutation
}
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@composeDirective", "@extends", "@external", "@inaccessible", "@interfaceObject", "@key", "@override", "@provides", "@requires", "@shareable", "@tag"])
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 @requires(fields: federation__FieldSet!) on FIELD_DEFINITION
directive @provides(fields: federation__FieldSet!) on FIELD_DEFINITION
directive @external(reason: String) on OBJECT | FIELD_DEFINITION
directive @tag(name: String!) repeatable on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION | SCHEMA
directive @extends on OBJECT | INTERFACE
directive @shareable repeatable on OBJECT | FIELD_DEFINITION
directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION
directive @override(from: String!) on FIELD_DEFINITION
directive @composeDirective(name: String) repeatable on SCHEMA
directive @interfaceObject on OBJECT
type User
@key(fields: "id")
{
id: ID!
name: String!
dob: String!
age: Int!
email: String!
}
type Query {
users: [User!]!
user(id: ID!): User!
_entities(representations: [_Any!]!): [_Entity]!
_service: _Service!
}
type Mutation {
createUser(name: String!): User!
}
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
but instead I am getting:
with a script
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@composeDirective", "@extends", "@external", "@inaccessible", "@interfaceObject", "@key", "@override", "@provides", "@requires", "@shareable", "@tag"])
type User
@key(fields: "id")
{
id: ID!
name: String!
dob: String!
age: Int!
email: String!
}
type Query {
users: [User!]!
user(id: ID!): User!
}
type Mutation {
createUser(name: String!): User!
}
Glad to hear you made some progress :)
I'm not well-versed in federation land yet, but I think the difference you're seeing is because the script only prints the subgraph schema (which is ready to be consumed by rover compose), whereas the introspection you did with the query includes everything necessary for the subgraph GraphQL server to act like a standalone GraphQL server. Kind of like composing a supergraph with only that single subgraph.