UNPKG

@loopback/docs

Version:

Documentation files rendered at [https://loopback.io](https://loopback.io)

465 lines (370 loc) 13.1 kB
# @loopback/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)" %} ![type-graphql](https://raw.githubusercontent.com/loopbackio/loopback-next/master/extensions/graphql/type-graphql.png) ## 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 @loopback/graphql ``` ## Basic Use Let's assume we build an application to expose GraphQL endpoints similar as [@loopback/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 '@loopback/core'; import {GraphQLServer} from '@loopback/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 '@loopback/boot'; import {RepositoryMixin} from '@loopback/repository'; import {RestApplication} from '@loopback/rest'; import {ApplicationConfig} from '@loopback/core'; import {GraphQLComponent, GraphQLBindings} from '@loopback/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 `@loopback/graphql` packages supports GraphQL schemas to be defined using only classes and decorators from `type-graphql`. ```ts import {Entity, model, property} from '@loopback/repository'; import {field, Float, ID, Int, objectType} from '@loopback/graphql'; @objectType({description: 'Object representing cooking recipe'}) @model({settings: {strict: false}}) export class Recipe extends Entity { @field(type => ID) @property({id: true}) id: string; @field() @property() title: string; @field(type => String, { nullable: true, deprecationReason: 'Use `description` field instead', }) get specification(): string | undefined { return this.description; } @field({ nullable: true, description: 'The recipe description with preparation info', }) @property() description?: string; @field(type => [Int]) ratings: number[]; @field() @property() creationDate: Date; @field(type => Int) protected numberInCollection: number; @field(type => Int) ratingsCount: number; @field(type => [String]) ingredients: string[]; @field(type => Int) protected get ingredientsLength(): number { return this.ingredients.length; } @field(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 `@model` 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 `@loopback/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 `@loopback/graphql`. ```ts import {service} from '@loopback/core'; import {repository} from '@loopback/repository'; import { arg, fieldResolver, Int, mutation, query, resolver, root, ResolverInterface, } from '@loopback/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'; @resolver(of => Recipe) export class RecipeResolver implements ResolverInterface<Recipe> { constructor( // Inject an instance of RecipeRepository @repository('RecipeRepository') private readonly recipeRepo: RecipeRepository, // Inject an instance of RecipeService @service(RecipeService) private readonly recipeService: RecipeService, ) {} // Map to a GraphQL query to get recipe by id @query(returns => Recipe, {nullable: true}) async recipe(@arg('recipeId') recipeId: string) { return this.recipeRepo.getOne(recipeId); } // Map to a GraphQL query to list all recipes @query(returns => [Recipe]) async recipes(): Promise<Recipe[]> { return this.recipeRepo.getAll(); } // Map to a GraphQL mutation to add a new recipe @mutation(returns => Recipe) async addRecipe(@arg('recipe') recipe: RecipeInput): Promise<Recipe> { return this.recipeRepo.add(recipe); } // Map to a calculated GraphQL field - `numberInCollection` @fieldResolver() async numberInCollection(@root() recipe: Recipe): Promise<number> { const index = await this.recipeRepo.findIndex(recipe); return index + 1; } // Map to a calculated GraphQL field - `ratingsCount` @fieldResolver() ratingsCount( @root() recipe: Recipe, @arg('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 `@inject`, `@service`, `@repository`, and `@config`, can be used with resolver classes. ```ts import {service} from '@loopback/core'; @resolver(of => Recipe) export class RecipeResolver implements ResolverInterface<Recipe> { constructor( // constructor injection of service @repository('RecipeRepository') private readonly recipeRepo: RecipeRepository, @service(RecipeService) private readonly recipeService: RecipeService, // It's possible to inject the resolver data @inject(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 @resolver(of => Recipe) export class RecipeResolver implements ResolverInterface<Recipe> { constructor( // constructor injection of service @repository('RecipeRepository') private readonly recipeRepo: RecipeRepository, @service(RecipeService) private readonly recipeService: RecipeService, // It's possible to inject the resolver data @inject(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 `@authorized` to enforce authorization. ```ts @resolver(of => Recipe) export class RecipeResolver implements ResolverInterface<Recipe> { constructor() {} // ... @query(returns => Recipe, {nullable: true}) @authorized('owner') // Authorized against `owner` role async recipe(@arg('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 '@loopback/core'; import {GraphQLServer} from '@loopback/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 [@loopback/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