TypeGraphQL
Although TypeGraphQL is data-layer library agnostic, it integrates well with other decorator-based libraries, like TypeORM, sequelize-typescript or Typegoose.
Installation
To begin, install the @tsed/typegraphql
package:
npm install --save @tsed/apollo graphql type-graphql @apollo/server @apollo/datasource-rest graphql-scalars
npm install --save-dev apollo-server-testing
npm install --save @tsed/apollo graphql type-graphql @apollo/server @as-integration/koa @apollo/datasource-rest graphql-scalars
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({
apollo: {
server1: {
// GraphQL server configuration
// See options descriptions on https://www.apollographql.com/docs/apollo-server/api/apollo-server.html
path: "/",
playground: true // enable playground GraphQL IDE. Set false to use Apollo Studio
// resolvers?: (Function | string)[];
// dataSources?: Function;
// server?: (config: Config) => ApolloServer;
// plugins: []
// middlewareOptions?: ServerRegistration;
// type-graphql
// See options descriptions on https://19majkel94.github.io/type-graphql/
// buildSchemaOptions?: Partial<BuildSchemaOptions>;
}
}
})
export class Server {}
Types
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});
}
}
Inject Ts.ED Context
There are two ways to inject the Ts.ED context in your resolver. The first one is to use the @InjectContext()
decorator:
import {Inject, InjectContext} from "@tsed/di";
import {PLatformContext} from "@tsed/platform-http";
import {ResolverController} from "@tsed/typegraphql";
import {Arg, Args, Query} from "type-graphql";
import {Recipe} from "../types/Recipe";
@ResolverController(Recipe)
export class RecipeResolver {
@InjectContext()
private $ctx: PLatformContext;
@Query((returns) => Recipe)
async recipe(@Arg("id") id: string) {
this.$ctx.logger.info("Hello world");
console.log(this.$ctx.request.headers);
}
}
The second one is to use the @Ctx()
decorator provided by TypeGraphQL:
import {ResolverController} from "@tsed/typegraphql";
import {Arg, Ctx, Query} from "type-graphql";
import {Recipe} from "../types/Recipe";
@ResolverController(Recipe)
export class RecipeResolver {
@InjectContext()
private $ctx: Context;
@Query((returns) => Recipe)
async recipe(@Arg("id") id: string, @Ctx("req.$ctx") $ctx: PlatformContext) {
$ctx.logger.info("Hello world");
console.log($ctx.request.headers);
}
}
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";
import {DataSource, InjectApolloContext, ApolloContext, InjectApolloContext} from "@tsed/apollo";
import {Constant, Opts} from "@tsed/di";
import {RESTDataSource} from "@apollo/datasource-rest";
@DataSource()
export class UserDataSource extends RESTDataSource {
@InjectContext()
protected $ctx: PlatformContext;
@Constant("envs.USERS_URL", "https://myapi.com/api/users")
protected baseURL: string;
@InjectApolloContext()
protected context: CustomApolloContext;
constructor(server: ApolloServer, logger: Logger) {
super({
logger,
cache: server.cache
});
}
willSendRequest(path, request) {
request.headers["authorization"] = this.context.token;
}
getUserById(id: string) {
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);
}
}
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
}
}
}
})
Subscriptions
Ts.ED provides a @tsed/graphql-ws
package to support the subscription
feature of GraphQL. See here for more details.