@loopback/docs
Version:
Documentation files rendered at [https://loopback.io](https://loopback.io)
465 lines (370 loc) • 13.1 kB
Markdown
# /graphql
This module provides integration with [GraphQL](https://graphql.org/) using
[type-graphql](https://typegraphql.com/).
{% include note.html content="
The `@loopback/graphql` module provides first-class GraphQL support in
LoopBack 4. For the general OpenAPI REST API wrapper, see
[Using OpenAPI-to-GraphQL](https://loopback.io/doc/en/lb4/Using-openapi-to-graphql.html)" %}

## Stability: ⚠️Experimental⚠️
> Experimental packages provide early access to advanced or experimental
> functionality to get community feedback. Such modules are published to npm
> using `0.x.y` versions. Their APIs and functionality may be subject to
> breaking changes in future releases.
## Installation
```sh
npm install --save /graphql
```
## Basic Use
Let's assume we build an application to expose GraphQL endpoints similar as
[/example-graphql](https://github.com/loopbackio/loopback-next/tree/master/examples/graphql).
```ts
export class MyApplication extends BootMixin(RestApplication) {
constructor(config: ApplicationConfig) {
super(config);
this.projectRoot = __dirname;
this.component(GraphQLComponent);
this.configure(GraphQLBindings.GRAPHQL_SERVER).to({asMiddlewareOnly: true});
}
}
```
## Configure GraphQLServer
This package can be used in two flavors:
- As a server for LoopBack applications
```ts
import {Application} from '/core';
import {GraphQLServer} from '/graphql';
const app = new Application();
const serverBinding = app.server(GraphQLServer);
app.configure(serverBinding.key).to({host: '127.0.0.1', port: 0});
server = await app.getServer(GraphQLServer);
// ...
await app.start();
```
- As a middleware for LoopBack REST applications
```ts
import {BootMixin} from '/boot';
import {RepositoryMixin} from '/repository';
import {RestApplication} from '/rest';
import {ApplicationConfig} from '/core';
import {GraphQLComponent, GraphQLBindings} from '/graphql';
export class GraphqlDemoApplication extends BootMixin(
RepositoryMixin(RestApplication),
) {
constructor(options: ApplicationConfig = {}) {
super(options);
this.component(GraphQLComponent);
this.configure(GraphQLBindings.GRAPHQL_SERVER).to({
asMiddlewareOnly: true,
});
const server = this.getSync(GraphQLBindings.GRAPHQL_SERVER);
this.expressMiddleware('middleware.express.GraphQL', server.expressApp);
// ...
// Customize @loopback/boot Booter Conventions here
this.bootOptions = {
graphqlResolvers: {
// Customize ControllerBooter Conventions here
dirs: ['graphql-resolvers'],
extensions: ['.js'],
nested: true,
},
};
}
}
```
The GraphQLServer configuration can also be passed in from the application
config, such as:
```ts
const app = new Application({
graphql: {
asMiddlewareOnly: true,
},
});
```
## Add GraphQL types
The `/graphql` packages supports GraphQL schemas to be defined using
only classes and decorators from `type-graphql`.
```ts
import {Entity, model, property} from '/repository';
import {field, Float, ID, Int, objectType} from '/graphql';
({description: 'Object representing cooking recipe'})
({settings: {strict: false}})
export class Recipe extends Entity {
(type => ID)
({id: true})
id: string;
()
()
title: string;
(type => String, {
nullable: true,
deprecationReason: 'Use `description` field instead',
})
get specification(): string | undefined {
return this.description;
}
({
nullable: true,
description: 'The recipe description with preparation info',
})
()
description?: string;
(type => [Int])
ratings: number[];
()
()
creationDate: Date;
(type => Int)
protected numberInCollection: number;
(type => Int)
ratingsCount: number;
(type => [String])
ingredients: string[];
(type => Int)
protected get ingredientsLength(): number {
return this.ingredients.length;
}
(type => Float, {nullable: true})
get averageRating(): number | null {
const ratingsCount = this.ratings.length;
if (ratingsCount === 0) {
return null;
}
const ratingsSum = this.ratings.reduce((a, b) => a + b, 0);
return ratingsSum / ratingsCount;
}
}
```
Please note that we also use ` ` and `property` decorators to define how
the entities being persisted with LoopBack repositories.
## Add GraphQL resolver classes
To serve GraphQL, an application will provide resolver classes to group
query/mutation/resolver functions, similarly as how REST API endpoints are
exposed via controller classes.
We use TypeScript decorators to provide metadata about individual resolvers. See
[type-graphql docs](https://typegraphql.com/) for more details.
Let's add `recipe-resolver.ts` to `src/graphql-resolvers` so that it can be
automatically discovered and loaded by the `/graphql` component.
Please note that we re-export `type-graphql` decorators as camel case variants,
such as `query` instead of `Query`. It's recommended that your applications
import such decorators from `/graphql`.
```ts
import {service} from '/core';
import {repository} from '/repository';
import {
arg,
fieldResolver,
Int,
mutation,
query,
resolver,
root,
ResolverInterface,
} from '/graphql';
import {RecipeInput} from '../graphql-types/recipe-input';
import {Recipe} from '../graphql-types/recipe-type';
import {RecipeRepository} from '../repositories';
import {RecipeService} from '../services/recipe.service';
(of => Recipe)
export class RecipeResolver implements ResolverInterface<Recipe> {
constructor(
// Inject an instance of RecipeRepository
('RecipeRepository')
private readonly recipeRepo: RecipeRepository,
// Inject an instance of RecipeService
(RecipeService) private readonly recipeService: RecipeService,
) {}
// Map to a GraphQL query to get recipe by id
(returns => Recipe, {nullable: true})
async recipe( ('recipeId') recipeId: string) {
return this.recipeRepo.getOne(recipeId);
}
// Map to a GraphQL query to list all recipes
(returns => [Recipe])
async recipes(): Promise<Recipe[]> {
return this.recipeRepo.getAll();
}
// Map to a GraphQL mutation to add a new recipe
(returns => Recipe)
async addRecipe( ('recipe') recipe: RecipeInput): Promise<Recipe> {
return this.recipeRepo.add(recipe);
}
// Map to a calculated GraphQL field - `numberInCollection`
()
async numberInCollection( () recipe: Recipe): Promise<number> {
const index = await this.recipeRepo.findIndex(recipe);
return index + 1;
}
// Map to a calculated GraphQL field - `ratingsCount`
()
ratingsCount(
() recipe: Recipe,
('minRate', type => Int, {defaultValue: 0.0})
minRate: number,
): number {
return this.recipeService.ratingsCount(recipe, minRate);
}
}
```
## Use LoopBack dependency injections in resolver classes
All of LoopBack decorators for dependency injection , such as ` `,
` `, ` `, and ` `, can be used with resolver classes.
```ts
import {service} from '/core';
(of => Recipe)
export class RecipeResolver implements ResolverInterface<Recipe> {
constructor(
// constructor injection of service
('RecipeRepository')
private readonly recipeRepo: RecipeRepository,
(RecipeService) private readonly recipeService: RecipeService,
// It's possible to inject the resolver data
(GraphQLBindings.RESOLVER_DATA) private resolverData: ResolverData,
) {}
}
```
## Discover and load GraphQL resolvers
The `GraphQLComponent` contributes a booter that discovers and registers
resolver classes from `src/graphql-resolvers` during `app.boot()`.
## Propagate context data
The `GraphQLServer` allows you to propagate context from Express to resolvers.
### Register a GraphQL context resolver
The GraphQL context object can be built/enhanced by the context resolver. The
original value is `{req: Request, res: Response}` that represents the Express
request and response object.
```ts
export class GraphqlDemoApplication extends BootMixin(
RepositoryMixin(RestApplication),
) {
constructor(options: ApplicationConfig = {}) {
super(options);
// ...
// It's possible to register a graphql context resolver
this.bind(GraphQLBindings.GRAPHQL_CONTEXT_RESOLVER).to(context => {
// Add your custom logic here to produce a context from incoming ExpressContext
return {...context};
});
}
// ...
}
```
### Access the GraphQL context inside a resolver
```ts
(of => Recipe)
export class RecipeResolver implements ResolverInterface<Recipe> {
constructor(
// constructor injection of service
('RecipeRepository')
private readonly recipeRepo: RecipeRepository,
(RecipeService) private readonly recipeService: RecipeService,
// It's possible to inject the resolver data
(GraphQLBindings.RESOLVER_DATA) private resolverData: ResolverData,
) {
// `this.resolverData.context` is the GraphQL context
}
// ...
}
```
### Set up authorization checker
We can customize the `authChecker` for
[TypeGraphQL Authorization](https://typegraphql.com/docs/authorization.html).
```ts
export class GraphqlDemoApplication extends BootMixin(
RepositoryMixin(RestApplication),
) {
constructor(options: ApplicationConfig = {}) {
super(options);
// ...
// It's possible to register a graphql auth checker
this.bind(GraphQLBindings.GRAPHQL_AUTH_CHECKER).to(
(resolverData, roles) => {
// Use resolverData and roles for authorization
return true;
},
);
}
// ...
}
```
The resolver classes and graphql types can be decorated with ` ` to
enforce authorization.
```ts
(of => Recipe)
export class RecipeResolver implements ResolverInterface<Recipe> {
constructor() {} // ...
(returns => Recipe, {nullable: true})
('owner') // Authorized against `owner` role
async recipe( ('recipeId') recipeId: string) {
return this.recipeRepo.getOne(recipeId);
}
}
```
## Register GraphQL middleware
We can register one or more
[TypeGraphQL Middleware](https://typegraphql.com/docs/middlewares.html) as
follows:
```ts
export class GraphqlDemoApplication extends BootMixin(
RepositoryMixin(RestApplication),
) {
constructor(options: ApplicationConfig = {}) {
super(options);
const server = this.getSync(GraphQLBindings.GRAPHQL_SERVER);
this.expressMiddleware('middleware.express.GraphQL', server.expressApp);
// Register a GraphQL middleware
server.middleware((resolverData, next) => {
// It's invoked for each field resolver, query and mutation operations
return next();
});
}
}
```
## Export GraphQL Schema as a file
Exporting the generated GraphQL schema file can be done by calling
`exportGraphQLSchema` on the `GraphQLServer`:
```ts
import {Application} from '/core';
import {GraphQLServer} from '/graphql';
const app = new Application();
const serverBinding = app.server(GraphQLServer);
app.configure(serverBinding.key).to({host: '127.0.0.1', port: 0});
server = await app.getServer(GraphQLServer);
// set up your resolvers
await server.exportGraphQLSchema(pathToFile);
```
For applications using `GraphQLComponent` to discover resolvers, exporting the
schema file is similar to `exportOpenApiSpec` on `RestServer`:
```ts
// export-graphql-schema.ts, sibling to application.ts
import {MyApplication, MyApplicationConfig} from './application';
async function exportGraphQLSchema(): Promise<void> {
const config: MyApplicationConfig = {
rest: {
port: +(process.env.PORT ?? 3000),
host: process.env.HOST ?? 'localhost',
},
};
const outFile = process.argv[2] ?? '';
const app = new MyApplication(config);
await app.boot();
const server = await app.getServer(GraphQLServer);
await server.exportGraphQLSchema(outFile);
}
exportGraphQLSchema().catch(err => {
console.error('Fail to export GraphQL spec from the application.', err);
process.exit(1);
});
```
## Try it out
Check out
[/example-graphql](https://github.com/loopbackio/loopback-next/tree/master/examples/graphql).
## Contributions
- [Guidelines](https://github.com/loopbackio/loopback-next/blob/master/docs/CONTRIBUTING.md)
- [Join the team](https://github.com/loopbackio/loopback-next/issues/110)
## Tests
Run `npm test` from the root folder.
## Contributors
See
[all contributors](https://github.com/loopbackio/loopback-next/graphs/contributors).
## License
MIT