Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Can not print schema for a subgraph: GraphQLError: Type User must define one or more fields #2765

Open
2 of 4 tasks
maxkomarychev opened this issue Apr 7, 2023 · 5 comments

Comments

@maxkomarychev
Copy link

maxkomarychev commented Apr 7, 2023

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?

  • I have searched the existing issues

Current behavior

I am following this guide https://docs.nestjs.com/graphql/generating-sdl and this issue #1597 but the generated schema does not contain relevant directives.

Minimum reproduction code

https://github.com/maxkomarychev/rover-compose-problem-demo

Steps to reproduce

  1. clone repo
  2. npm i
  3. npx ts-node generate-schema.ts
  4. 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?

  • macOS
  • Windows
  • Linux

Other

No response<

@maxkomarychev maxkomarychev changed the title Can not print schema for a subgraph Can not print schema for a subgraph: GraphQLError: Type User must define one or more fields Apr 7, 2023
@Phault
Copy link

Phault commented Apr 9, 2023

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 🤞 .

@maxkomarychev
Copy link
Author

maxkomarychev commented Apr 9, 2023

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.

@maxkomarychev
Copy link
Author

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
Loading
graph LR
  UsersModule -->|providers| UsersResolver
  UsersResolverLazyServices -->|providers| UsersService
  UsersResolver -->|constructor injection| LazyModuleLoader
  UsersResolver -->|runtime resolution on demand via LazyModule| UsersService
Loading

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;
  }
  ...
}

@maxkomarychev
Copy link
Author

maxkomarychev commented Apr 10, 2023

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!
}

@Phault
Copy link

Phault commented Apr 10, 2023

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants