diff --git a/packages/apollo/tests/code-first-multigraph/app.module.ts b/packages/apollo/tests/code-first-multigraph/app.module.ts new file mode 100644 index 000000000..d6b38a24b --- /dev/null +++ b/packages/apollo/tests/code-first-multigraph/app.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { PostModule } from './post/post.module'; +import { UserModule } from './user/user.module'; + +@Module({ + imports: [PostModule, UserModule], +}) +export class ApplicationModule {} diff --git a/packages/apollo/tests/code-first-multigraph/gateway.module.ts b/packages/apollo/tests/code-first-multigraph/gateway.module.ts new file mode 100644 index 000000000..2fe4c8673 --- /dev/null +++ b/packages/apollo/tests/code-first-multigraph/gateway.module.ts @@ -0,0 +1,18 @@ +import { Module } from '@nestjs/common'; +import { GqlModuleOptions, GraphQLModule } from '@nestjs/graphql'; +import { ApolloGatewayDriver, ApolloGatewayDriverConfig } from '../../lib'; + +@Module({ + imports: [ + GraphQLModule.forRoot({ + driver: ApolloGatewayDriver, + gateway: { + serviceList: [ + { name: 'user', url: 'http://localhost:3000/user/graphql' }, + { name: 'post', url: 'http://localhost:3000/post/graphql' }, + ], + }, + }), + ], +}) +export class GatewayModule {} diff --git a/packages/apollo/tests/code-first-multigraph/main.ts b/packages/apollo/tests/code-first-multigraph/main.ts new file mode 100644 index 000000000..c73dd67a5 --- /dev/null +++ b/packages/apollo/tests/code-first-multigraph/main.ts @@ -0,0 +1,20 @@ +import { ValidationPipe } from '@nestjs/common'; +import { NestFactory } from '@nestjs/core'; +import { ApplicationModule } from './app.module'; +import { GatewayModule } from './gateway.module'; + +/** + * This use case comes from the need for an application + * to be designed in a way which would allow future context + * separation from a deployment point of view. + */ +export async function bootstrap() { + const app = await NestFactory.create(ApplicationModule); + app.useGlobalPipes(new ValidationPipe()); + await app.listen(3000); + + const gateway = await NestFactory.create(GatewayModule); + await gateway.listen(3001); +} + +bootstrap(); diff --git a/packages/apollo/tests/code-first-multigraph/post/external-user/user.resolver.ts b/packages/apollo/tests/code-first-multigraph/post/external-user/user.resolver.ts new file mode 100644 index 000000000..4c3ad20b2 --- /dev/null +++ b/packages/apollo/tests/code-first-multigraph/post/external-user/user.resolver.ts @@ -0,0 +1,14 @@ +import { Resolver, ResolveField, Parent } from '@nestjs/graphql'; + +import { Post, Publication } from '../post.type'; +import { User } from './user.type'; + +@Resolver(() => User) +export class UserResolver { + @ResolveField('posts', () => [Post]) + userPosts(@Parent() user: User): Post[] { + return [ + { id: 10, authorId: user.id, status: Publication.DRAFT, title: 'test' }, + ]; + } +} diff --git a/packages/apollo/tests/code-first-multigraph/post/external-user/user.type.ts b/packages/apollo/tests/code-first-multigraph/post/external-user/user.type.ts new file mode 100644 index 000000000..e727b3f52 --- /dev/null +++ b/packages/apollo/tests/code-first-multigraph/post/external-user/user.type.ts @@ -0,0 +1,10 @@ +import { ObjectType, Directive, Field, ID } from '@nestjs/graphql'; + +@ObjectType() +@Directive('@extends') +@Directive('@key(fields: "id")') +export class User { + @Field(() => ID) + @Directive('@external') + id: number; +} diff --git a/packages/apollo/tests/code-first-multigraph/post/post.module.ts b/packages/apollo/tests/code-first-multigraph/post/post.module.ts new file mode 100644 index 000000000..eb9685757 --- /dev/null +++ b/packages/apollo/tests/code-first-multigraph/post/post.module.ts @@ -0,0 +1,22 @@ +import { Module } from '@nestjs/common'; +import { GraphQLModule } from '@nestjs/graphql'; + +import { ApolloDriverConfig } from '../../../lib'; +import { ApolloFederationDriver } from '../../../lib/drivers'; + +import { UserResolver } from './external-user/user.resolver'; +import { PostResolver } from './post.resolver'; + +@Module({ + imports: [ + GraphQLModule.forRoot({ + driver: ApolloFederationDriver, + debug: false, + autoSchemaFile: true, + path: '/post/graphql', + include: [PostModule], + }), + ], + providers: [PostResolver, UserResolver], +}) +export class PostModule {} diff --git a/packages/apollo/tests/code-first-multigraph/post/post.resolver.ts b/packages/apollo/tests/code-first-multigraph/post/post.resolver.ts new file mode 100644 index 000000000..4c5587e17 --- /dev/null +++ b/packages/apollo/tests/code-first-multigraph/post/post.resolver.ts @@ -0,0 +1,48 @@ +import { + Resolver, + Query, + ResolveField, + Parent, + ID, + Args, +} from '@nestjs/graphql'; + +import { User } from './external-user/user.type'; +import { Post, Publication } from './post.type'; + +@Resolver(() => Post) +export class PostResolver { + store = new Set([ + { + id: 1, + authorId: 1, + title: 'How to Jedi II', + status: Publication.PUBLISHED, + }, + { + id: 2, + authorId: 2, + title: 'Why lightsabers are unreliable', + status: Publication.DRAFT, + }, + ]); + + @Query(() => [Post]) + posts() { + return [...this.store]; + } + + @Query(() => [Post]) + post(@Args('id', { type: () => ID }) id: number): Post | undefined { + for (const post of this.store) { + if (post.id.toString() === id.toString()) { + return post; + } + } + } + + @ResolveField(() => User) + user(@Parent() post: Post) { + return { __typename: 'User', id: post.authorId }; + } +} diff --git a/packages/apollo/tests/code-first-multigraph/post/post.type.ts b/packages/apollo/tests/code-first-multigraph/post/post.type.ts new file mode 100644 index 000000000..99d8e77dd --- /dev/null +++ b/packages/apollo/tests/code-first-multigraph/post/post.type.ts @@ -0,0 +1,30 @@ +import { + registerEnumType, + ObjectType, + Directive, + Field, + ID, +} from '@nestjs/graphql'; + +export enum Publication { + PUBLISHED, + DRAFT, +} + +registerEnumType(Publication, { name: 'Publication' }); + +@ObjectType() +@Directive('@key(fields: "id")') +export class Post { + @Field(() => ID) + id: number; + + @Field() + title: string; + + @Field(() => Publication) + status: Publication; + + @Field(() => ID) + authorId: number; +} diff --git a/packages/apollo/tests/code-first-multigraph/user/user.module.ts b/packages/apollo/tests/code-first-multigraph/user/user.module.ts new file mode 100644 index 000000000..b0d439b23 --- /dev/null +++ b/packages/apollo/tests/code-first-multigraph/user/user.module.ts @@ -0,0 +1,21 @@ +import { Module } from '@nestjs/common'; +import { GraphQLModule } from '@nestjs/graphql'; + +import { ApolloDriverConfig } from '../../../lib'; +import { ApolloFederationDriver } from '../../../lib/drivers'; + +import { UserResolver } from './user.resolver'; + +@Module({ + imports: [ + GraphQLModule.forRoot({ + driver: ApolloFederationDriver, + debug: false, + autoSchemaFile: true, + path: '/user/graphql', + include: [UserModule], + }), + ], + providers: [UserResolver], +}) +export class UserModule {} diff --git a/packages/apollo/tests/code-first-multigraph/user/user.resolver.ts b/packages/apollo/tests/code-first-multigraph/user/user.resolver.ts new file mode 100644 index 000000000..bb603f649 --- /dev/null +++ b/packages/apollo/tests/code-first-multigraph/user/user.resolver.ts @@ -0,0 +1,50 @@ +import { + Field, + ObjectType, + Resolver, + Query, + ID, + Directive, + Args, + ResolveReference, +} from '@nestjs/graphql'; + +@ObjectType() +@Directive('@key(fields: "id")') +class User { + @Field(() => ID) + id: number; + + @Field() + name: string; +} + +@Resolver(() => User) +export class UserResolver { + private readonly store = new Set([ + { id: 1, name: 'Luke' }, + { id: 2, name: 'Han' }, + ]); + + @Query(() => [User]) + users(): User[] { + return [...this.store]; + } + + @Query(() => User) + user(@Args('id', { type: () => ID }) id: number): User | undefined { + for (const user of this.store) { + if (user.id.toString() === id.toString()) { + return user; + } + } + } + + @ResolveReference() + resolveReference(reference: { + __typename: string; + id: number; + }): User | undefined { + return this.user(reference.id); + } +} diff --git a/packages/apollo/tests/e2e/code-first-multigraph.spec.ts b/packages/apollo/tests/e2e/code-first-multigraph.spec.ts new file mode 100644 index 000000000..5fa4170a1 --- /dev/null +++ b/packages/apollo/tests/e2e/code-first-multigraph.spec.ts @@ -0,0 +1,161 @@ +import { Test } from '@nestjs/testing'; +import { HttpStatus, INestApplication, Type } from '@nestjs/common'; +import { buildClientSchema, getIntrospectionQuery, printSchema } from 'graphql'; +import * as request from 'supertest'; + +import { ApplicationModule } from '../code-first-multigraph/app.module'; +import { GatewayModule } from '../code-first-multigraph/gateway.module'; + +async function initNestApp(ModuleClass: Type) { + const module = await Test.createTestingModule({ + imports: [ModuleClass], + }).compile(); + + const app = await module.createNestApplication(); + await app.init(); + return app; +} + +async function getSDL(app: INestApplication, endpoint: string) { + const { body, status } = await request(app.getHttpServer()) + .post(endpoint) + .send({ + query: `{ _service { sdl } } `, + }); + expect(status).toBe(HttpStatus.OK); + return body.data._service.sdl; +} + +async function introspect(app: INestApplication, endpoint: string) { + const { body, status } = await request(app.getHttpServer()) + .post(endpoint) + .send({ query: getIntrospectionQuery() }); + + expect(status).toBe(HttpStatus.OK); + return printSchema(buildClientSchema(body.data)); +} + +describe('Multiple GraphQL endpoints in the same application', () => { + describe('Application', () => { + it('should successfully start the application', async () => { + const app = await initNestApp(ApplicationModule); + await app.close(); + }); + + describe('Schema', () => { + let app: INestApplication; + + beforeAll(async () => { + app = await initNestApp(ApplicationModule); + }); + + afterAll(() => app.close()); + + it('should constrain the UserModule schema to its submodule definitions', async () => { + const sdl = await getSDL(app, '/user/graphql'); + + expect(sdl).toMatchInlineSnapshot(` + "type Query { + users: [User!]! + user(id: ID!): User! + } + + type User @key(fields: \\"id\\") { + id: ID! + name: String! + } + " + `); + }); + + it('should constrain the PostModule schema to its submodule definitions', async () => { + const sdl = await getSDL(app, '/post/graphql'); + + expect(sdl).toMatchInlineSnapshot(` + "type Query { + posts: [Post!]! + post(id: ID!): [Post!]! + } + + type Post @key(fields: \\"id\\") { + id: ID! + title: String! + status: Publication! + authorId: ID! + user: User! + } + + enum Publication { + PUBLISHED + DRAFT + } + + type User @extends @key(fields: \\"id\\") { + id: ID! @external + posts: [Post!]! + } + " + `); + }); + }); + }); + + describe('Gateway', () => { + let app: INestApplication; + + beforeAll(async () => { + app = await initNestApp(ApplicationModule); + await app.listen(3000); + }); + + afterAll(() => app.close()); + + it('should successfully start the gateway', async () => { + const gateway = await initNestApp(GatewayModule); + await gateway.close(); + }); + + describe('Schema', () => { + let gateway: INestApplication; + + beforeAll(async () => { + gateway = await initNestApp(GatewayModule); + }); + + afterAll(() => gateway.close()); + + it('should expose the stitched schema', async () => { + const schema = await introspect(gateway, '/graphql'); + + expect(schema).toMatchInlineSnapshot(` + "type Post { + authorId: ID! + id: ID! + status: Publication! + title: String! + user: User! + } + + enum Publication { + DRAFT + PUBLISHED + } + + type Query { + post(id: ID!): [Post!]! + posts: [Post!]! + user(id: ID!): User! + users: [User!]! + } + + type User { + id: ID! + name: String! + posts: [Post!]! + } + " + `); + }); + }); + }); +});