diff --git a/examples/graphql/src/application.ts b/examples/graphql/src/application.ts index 642592e39708..90a1da6ed5fc 100644 --- a/examples/graphql/src/application.ts +++ b/examples/graphql/src/application.ts @@ -5,10 +5,10 @@ import {BootMixin} from '@loopback/boot'; import {ApplicationConfig} from '@loopback/core'; +import {GraphQLBindings, GraphQLComponent} from '@loopback/graphql'; import {RepositoryMixin} from '@loopback/repository'; import {RestApplication} from '@loopback/rest'; import path from 'path'; -import {GraphQLBindings, GraphQLComponent} from '@loopback/graphql'; import {sampleRecipes} from './sample-recipes'; export {ApplicationConfig}; @@ -26,6 +26,11 @@ export class GraphqlDemoApplication extends BootMixin( asMiddlewareOnly: true, }); + // It's possible to register a graphql context resolver + this.bind(GraphQLBindings.GRAPHQL_CONTEXT_RESOLVER).to(context => { + return {...context}; + }); + this.bind('recipes').to([...sampleRecipes]); // Set up default home page diff --git a/examples/graphql/src/datasources/recipes.datasource.ts b/examples/graphql/src/datasources/recipes.datasource.ts index 33d644e6af66..3d0d2e5ae828 100644 --- a/examples/graphql/src/datasources/recipes.datasource.ts +++ b/examples/graphql/src/datasources/recipes.datasource.ts @@ -28,7 +28,8 @@ const config = { [ContextTags.NAMESPACE]: RepositoryBindings.DATASOURCES, }, }) -export class RecipesDataSource extends juggler.DataSource +export class RecipesDataSource + extends juggler.DataSource implements LifeCycleObserver { static dataSourceName = 'recipes'; static readonly defaultConfig = config; diff --git a/examples/graphql/src/graphql-resolvers/recipe-resolver.ts b/examples/graphql/src/graphql-resolvers/recipe-resolver.ts index 4d4b15614aa9..9909292ca653 100644 --- a/examples/graphql/src/graphql-resolvers/recipe-resolver.ts +++ b/examples/graphql/src/graphql-resolvers/recipe-resolver.ts @@ -3,14 +3,16 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {service} from '@loopback/core'; +import {inject, service} from '@loopback/core'; import { arg, fieldResolver, + GraphQLBindings, Int, mutation, query, resolver, + ResolverData, ResolverInterface, root, } from '@loopback/graphql'; @@ -27,6 +29,8 @@ export class RecipeResolver implements ResolverInterface { @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, ) {} @query(returns => Recipe, {nullable: true}) diff --git a/extensions/graphql/README.md b/extensions/graphql/README.md index 06c3beca9981..c4ca0562e3ed 100644 --- a/extensions/graphql/README.md +++ b/extensions/graphql/README.md @@ -256,6 +256,8 @@ export class RecipeResolver implements ResolverInterface { @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, ) {} } ``` @@ -265,6 +267,47 @@ export class RecipeResolver implements ResolverInterface { 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 + +```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 { + 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, + ) {} + // ... +} +``` + ## Try it out Check out diff --git a/extensions/graphql/src/graphql.container.ts b/extensions/graphql/src/graphql.container.ts index 148d6bce4577..4ad4a43f2da8 100644 --- a/extensions/graphql/src/graphql.container.ts +++ b/extensions/graphql/src/graphql.container.ts @@ -82,9 +82,9 @@ export class LoopBackContainer implements ContainerType { debug( 'Resolver %s found in context %s', resolverClass.name, - this.ctx.name, + resolutionCtx.name, found, ); - return this.ctx.getValueOrPromise(found.key); + return resolutionCtx.getValueOrPromise(found.key); } } diff --git a/extensions/graphql/src/graphql.server.ts b/extensions/graphql/src/graphql.server.ts index bed748876b43..dccbcb0ea009 100644 --- a/extensions/graphql/src/graphql.server.ts +++ b/extensions/graphql/src/graphql.server.ts @@ -17,12 +17,18 @@ import { Server, } from '@loopback/core'; import {HttpOptions, HttpServer} from '@loopback/http-server'; +import {ContextFunction} from 'apollo-server-core'; import {ApolloServer, ApolloServerExpressConfig} from 'apollo-server-express'; +import {ExpressContext} from 'apollo-server-express/dist/ApolloServer'; import express from 'express'; import {buildSchema, NonEmptyArray, ResolverInterface} from 'type-graphql'; import {LoopBackContainer} from './graphql.container'; import {GraphQLBindings, GraphQLTags} from './keys'; +export {ContextFunction} from 'apollo-server-core'; +export {ApolloServerExpressConfig} from 'apollo-server-express'; +export {ExpressContext} from 'apollo-server-express/dist/ApolloServer'; + /** * Options for GraphQL server */ @@ -95,9 +101,16 @@ export class GraphQLServer extends Context implements Server { container: new LoopBackContainer(this), }); - const serverConfig = { + // Allow a graphql context resolver to be bound to GRAPHQL_CONTEXT_RESOLVER + const graphqlContextResolver: ContextFunction = + (await this.get(GraphQLBindings.GRAPHQL_CONTEXT_RESOLVER, { + optional: true, + })) ?? (context => context); + + const serverConfig: ApolloServerExpressConfig = { // enable GraphQL Playground playground: true, + context: graphqlContextResolver, ...this.options.graphql, schema, }; diff --git a/extensions/graphql/src/keys.ts b/extensions/graphql/src/keys.ts index 69469208d73c..914124ed93f5 100644 --- a/extensions/graphql/src/keys.ts +++ b/extensions/graphql/src/keys.ts @@ -6,7 +6,7 @@ import {BindingKey, Constructor} from '@loopback/core'; import {ResolverData} from 'type-graphql'; import {GraphQLComponent} from './graphql.component'; -import {GraphQLServer} from './graphql.server'; +import {ContextFunction, ExpressContext, GraphQLServer} from './graphql.server'; export namespace GraphQLBindings { export const GRAPHQL_SERVER = BindingKey.create( @@ -17,6 +17,10 @@ export namespace GraphQLBindings { 'components.GraphQLComponent', ); + export const GRAPHQL_CONTEXT_RESOLVER = BindingKey.create< + ContextFunction + >('graphql.contextResolver'); + export const RESOLVER_DATA = BindingKey.create>( 'graphql.resolverData', );