GraphQL WS
GraphQL Websocket allows you to use the subscription
feature of GraphQL using the Websocket transport protocol. This module is based on the graphql-ws package. It pre-configures the socket server and GraphQL server to work together.
Feature
- Support multiple GraphQL server
- Enable subscription feature of GraphQL
Installation
This module need to be used with @tsed/apollo
module. So, you must install it before (see here).
npm install --save @tsed/graphql-ws graphql-ws
yarn add @tsed/graphql-ws graphql-ws
import {Configuration} from "@tsed/common";
import "@tsed/platform-express";
import "@tsed/apollo";
import "@tsed/graphql-ws";
import {join} from "path";
@Configuration({
apollo: {
server1: {
// GraphQL server configuration
path: "/",
playground: true, // enable playground GraphQL IDE. Set false to use Apollo Studio
plugins: [], // Apollo plugins
wsServerOptions: {
// See options descriptions on
},
wsUseServerOptions: {
// See options descriptions on GraphQL WS
}
// Give custom server instance
// server?: (config: Config) => ApolloServer;
// ApolloServer options
// ...
// See options descriptions on https://www.apollographql.com/docs/apollo-server/api/apollo-server.html
}
},
graphqlWs: {
// global options
wsServerOptions: {
// See options descriptions on
},
wsUseServerOptions: {
// See options descriptions on
}
}
})
export class Server {}
Register plugins
You can register plugins with the plugins
property. The plugins are executed in the order of declaration.
import {Configuration} from "@tsed/common";
import "@tsed/platform-express";
import "@tsed/apollo";
import {join} from "path";
@Configuration({
apollo: {
server1: {
plugins: [] // Apollo plugins
}
}
})
export class Server {}
But if you need to register and access to the injector, you can use the $alterApolloServerPlugins
hook. For example, you can register the graphql-ws
necessary to support the subscription
feature of GraphQL like this:
import {Constant, Inject, InjectorService, Module} from "@tsed/di";
import {useServer} from "graphql-ws/lib/use/ws";
import Http from "http";
import Https from "https";
import {WebSocketServer} from "ws";
import {GraphQLWSOptions} from "./GraphQLWSOptions";
@Module()
export class GraphQLWSModule {
@Constant("graphqlWs", {})
private settings: GraphQLWSOptions;
@Inject(Http.Server)
private httpServer: Http.Server | null;
@Inject(Https.Server)
private httpsServer: Https.Server | null;
@Inject()
private injector: InjectorService;
createWSServer(settings: GraphQLWSOptions) {
const wsServer = new WebSocketServer({
...(this.settings.wsServerOptions || {}),
...settings.wsServerOptions,
server: this.httpsServer || this.httpServer!,
path: settings.path
});
return useServer(
{
...(this.settings.wsUseServerOptions || {}),
...settings.wsUseServerOptions,
schema: settings.schema
},
wsServer
);
}
async $alterApolloServerPlugins(plugins: any[], settings: GraphQLWSOptions) {
const wsServer = await this.createWSServer(settings);
this.injector.logger.info(`Create GraphQL WS server on: ${settings.path}`);
return plugins.concat({
serverWillStart() {
return {
async drainServer() {
await wsServer.dispose();
}
};
}
} as any);
}
}
Note
Ts.ED provide a @tsed/graphql-ws
package to support the subscription
feature of GraphQL. See here for more details.
Nexus
Installation
npm install --save @tsed/apollo
npm install --save nexus graphql apollo-server-express
npm install --save-dev apollo-server-testing
npm install --save @tsed/apollo graphql
npm install --save nexus graphql apollo-server-koa
npm install --save-dev apollo-server-testing
Now, we can configure the Ts.ED server by importing @tsed/apollo
in your Server:
import {Configuration} from "@tsed/common";
import "@tsed/platform-express";
import "@tsed/apollo";
import {schema} from "./schema";
import {join} from "path";
@Configuration({
apollo: {
server1: {
// GraphQL server configuration
path: "/",
playground: true, // enable playground GraphQL IDE. Set false to use Apollo Studio
schema,
plugins: [] // Apollo plugins
// Give custom server instance
// server?: (config: Config) => ApolloServer;
// ApolloServer options
// ...
// See options descriptions on https://www.apollographql.com/docs/apollo-server/api/apollo-server.html
}
}
})
export class Server {}
Then create schema/index.ts
:
import {makeSchema} from "nexus";
import {join} from "path";
export const schema = makeSchema({
types: [], // 1
outputs: {
typegen: join(process.cwd(), "..", "..", "nexus-typegen.ts"), // 2
schema: join(process.cwd(), "..", "..", "schema.graphql") // 3
}
});
TypeGraphQL
Installation
To begin, install the @tsed/typegraphql
package:
npm install --save @tsed/typegraphql graphql apollo-server-express
npm install --save type-graphql apollo-datasource apollo-datasource-rest
npm install --save-dev apollo-server-testing
npm install --save @tsed/typegraphql graphql apollo-server-koa
npm install --save type-graphql apollo-datasource apollo-datasource-rest
npm install --save-dev apollo-server-testing
Now, we can configure the Ts.ED server by importing @tsed/typegraphql
in your Server:
Configuration
import {Configuration} from "@tsed/di";
import "@tsed/platform-express";
import "@tsed/typegraphql";
import "./resolvers/index"; // barrel file with all resolvers
@Configuration({
typegraphql: {
server1: {
// GraphQL server configuration
path: "/",
playground: true, // enable playground GraphQL IDE. Set false to use Apollo Studio
// resolvers?: (Function | string)[];
// dataSources?: Function;
// server?: (config: Config) => ApolloServer;
// Apollo Server options
// See options descriptions on https://www.apollographql.com/docs/apollo-server/api/apollo-server.html
serverConfig: {
plugins: []
}
// middlewareOptions?: ServerRegistration;
// type-graphql
// See options descriptions on https://19majkel94.github.io/type-graphql/
// buildSchemaOptions?: Partial<BuildSchemaOptions>;
}
}
})
export class Server {}
The following codesandbox example show you how you can use this configuration:
Types
import {Configuration} from "@tsed/di";
import "@tsed/platform-express";
import "@tsed/typegraphql";
import "./resolvers/index"; // barrel file with all resolvers
@Configuration({
typegraphql: {
server1: {
// GraphQL server configuration
path: "/",
playground: true, // enable playground GraphQL IDE. Set false to use Apollo Studio
// resolvers?: (Function | string)[];
// dataSources?: Function;
// server?: (config: Config) => ApolloServer;
// Apollo Server options
// See options descriptions on https://www.apollographql.com/docs/apollo-server/api/apollo-server.html
serverConfig: {
plugins: []
}
// middlewareOptions?: ServerRegistration;
// type-graphql
// See options descriptions on https://19majkel94.github.io/type-graphql/
// buildSchemaOptions?: Partial<BuildSchemaOptions>;
}
}
})
export class Server {}
We want to get the equivalent of this type described in SDL:
type Recipe {
id: ID!
title: String!
description: String
creationDate: Date!
ingredients: [String!]!
}
So we create the Recipe class with all properties and types:
class Recipe {
id: string;
title: string;
description?: string;
creationDate: Date;
ingredients: string[];
}
Then we decorate the class and its properties with decorators:
import {Field, ID, ObjectType} from "type-graphql";
@ObjectType()
export class Recipe {
@Field((type) => ID)
id: string;
@Field()
title: string;
@Field({nullable: true})
description?: string;
@Field()
creationDate: Date;
@Field((type) => [String])
ingredients: string[];
}
The detailed rules for when to use nullable, array and others are described in fields and types docs.
Resolvers
After that we want to create typical crud queries and mutation. To do that we create the resolver (controller) class that will have injected RecipeService in the constructor:
import {Inject} from "@tsed/di";
import {ResolverController} from "@tsed/typegraphql";
import {Arg, Args, Query} from "type-graphql";
import {RecipeNotFoundError} from "../errors/RecipeNotFoundError";
import {RecipesService} from "../services/RecipesService";
import {Recipe} from "../types/Recipe";
import {RecipesArgs} from "../types/RecipesArgs";
@ResolverController(Recipe)
export class RecipeResolver {
@Inject()
private recipesService: RecipesService;
@Query((returns) => Recipe)
async recipe(@Arg("id") id: string) {
const recipe = await this.recipesService.findById(id);
if (recipe === undefined) {
throw new RecipeNotFoundError(id);
}
return recipe;
}
@Query((returns) => [Recipe])
recipes(@Args() {skip, take}: RecipesArgs) {
return this.recipesService.findAll({skip, take});
}
}
Multiple GraphQL server
If you register multiple GraphQL servers, you must specify the server id in the @ResolverController
decorator.
@ResolverController(Recipe, {id: "server1"})
Another solution is to not use @ResolverController
(use @Resolver
from TypeGraphQL), and declare explicitly the resolver in the server configuration:
@Configuration({
graphql: {
server1: {
resolvers: {
RecipeResolver
}
},
server2: {
resolvers: {
OtherResolver
}
}
}
})
Data Source
Data source is one of the Apollo server features which can be used as option for your Resolver or Query. Ts.ED provides a DataSourceService decorator to declare a DataSource which will be injected to the Apollo server context.
import {DataSource} from "@tsed/typegraphql";
import {RESTDataSource} from "apollo-datasource-rest";
import {User} from "../models/User";
@DataSource()
export class UserDataSource extends RESTDataSource {
constructor() {
super();
this.baseURL = "https://myapi.com/api/users";
}
getUserById(id: string): Promise<User> {
return this.get(`/${id}`);
}
}
Then you can retrieve your data source through the context in your resolver like that:
import {ResolverController} from "@tsed/typegraphql";
import {Arg, Authorized, Ctx, Query} from "type-graphql";
import {UserDataSource} from "../datasources/UserDataSource";
import {User} from "../models/User";
@ResolverController(User)
export class UserResolver {
@Authorized()
@Query(() => User)
public async user(@Arg("userId") userId: string, @Ctx("dataSources") dataSources: any): Promise<User> {
const userDataSource: UserDataSource = dataSources.userDataSource;
return userDataSource.getUserById(userId);
}
}
Get Server instance
ApolloService (or TypeGraphQLService) lets you to retrieve an instance of ApolloServer.
import {AfterRoutesInit} from "@tsed/common";
import {Inject, Injectable} from "@tsed/di";
import {ApolloService} from "@tsed/apollo";
import {ApolloServer} from "apollo-server-express";
@Injectable()
export class UsersService implements AfterRoutesInit {
@Inject()
private apolloService: ApolloService;
// or private typeGraphQLService: TypeGraphQLService;
private server: ApolloServer;
$afterRoutesInit() {
this.server = this.apolloService.get("server1");
if (!this.server) {
throw new Error("Server instance 'server1' not found");
}
}
}
For more information about ApolloServer, look at its documentation here;
Testing
Here is an example to create a test server based on TypeGraphQL and run a query:
TIP
The unit example is also available to test any Apollo Server!
import {PlatformTest} from "@tsed/common";
import {ApolloService} from "@tsed/apollo";
import "@tsed/platform-express";
import {ApolloServerTestClient, createTestClient} from "apollo-server-testing";
import {expect} from "chai";
import gql from "graphql-tag";
import {Server} from "./app/Server";
const GET_RECIPES = gql`
query GetRecipes {
recipes {
title
description
creationDate
}
}
`;
describe("Recipes", () => {
let request: ApolloServerTestClient;
beforeAll(PlatformTest.bootstrap(Server));
beforeAll(() => {
const server = PlatformTest.get<ApolloService>(ApolloService).get("server1")!;
// OR const server = PlatformTest.get<ApolloService>(ApolloService).get("typegraphql-server1")!;
request = createTestClient(server);
});
afterAll(PlatformTest.reset);
it("should get recipes", async () => {
const response = await request.query({
query: GET_RECIPES,
variables: {}
});
expect(response.data).toEqual({
recipes: [
{
creationDate: "2020-08-20T00:00:00.000Z",
description: "Description",
title: "title"
}
]
});
});
});
import {PlatformTest} from "@tsed/common";
import "@tsed/platform-express";
import {ApolloServerTestClient, createTestClient} from "apollo-server-testing";
import {expect} from "chai";
import gql from "graphql-tag";
import {ApolloService} from "@tsed/apollo";
import {Server} from "./app/Server";
const GET_RECIPES = gql`
query GetRecipes {
recipes {
title
description
creationDate
}
}
`;
describe("Recipes", () => {
let request: ApolloServerTestClient;
before(PlatformTest.bootstrap(Server));
before(() => {
const server = PlatformTest.get<ApolloService>(ApolloService).get("server1")!;
// for TypeGraphQL
// use PlatformTest.get<ApolloService>(ApolloService).get("typegraphl-server1")!;
request = createTestClient(server);
});
after(PlatformTest.reset);
it("should get recipes", async () => {
const response = await request.query({
query: GET_RECIPES,
variables: {}
});
expect(response.data).to.deep.eq({
recipes: [
{
creationDate: "2020-08-20T00:00:00.000Z",
description: "Description",
title: "title"
}
]
});
});
});
import {Field, ID, ObjectType} from "type-graphql";
@ObjectType()
export class Recipe {
@Field((type) => ID)
id: string;
@Field()
title: string;
@Field({nullable: true})
description?: string;
@Field()
creationDate: Date;
@Field((type) => [String])
ingredients: string[];
}
import {ArgsType, Field, Int} from "type-graphql";
@ArgsType()
export class RecipeArgs {
@Field((type) => Int, {nullable: true})
skip?: number;
@Field((type) => Int, {nullable: true})
take?: number;
@Field({nullable: true})
title?: string;
}
import {Recipe} from "../types/Recipes";
export class RecipesService {
recipes: Recipe[] = [
new Recipe({
id: "1",
title: "title",
description: "Description",
creationDate: new Date("2020-08-20"),
ingredients: []
})
];
async findById(id: string) {
return this.recipes.find((item) => item.id === id);
}
async findAll(options: any) {
return this.recipes;
}
}
import {Inject} from "@tsed/di";
import {ResolverController} from "@tsed/typegraphql";
import {Arg, Args, Query} from "type-graphql";
import {RecipeNotFoundError} from "../errors/RecipeNotFoundError";
import {RecipesService} from "../services/RecipesService";
import {Recipe} from "../types/Recipe";
import {RecipesArgs} from "../types/RecipesArgs";
@ResolverController(Recipe)
export class RecipeResolver {
@Inject()
private recipesService: RecipesService;
@Query((returns) => Recipe)
async recipe(@Arg("id") id: string) {
const recipe = await this.recipesService.findById(id);
if (recipe === undefined) {
throw new RecipeNotFoundError(id);
}
return recipe;
}
@Query((returns) => [Recipe])
recipes(@Args() {skip, take}: RecipesArgs) {
return this.recipesService.findAll({skip, take});
}
}