Skip to content

Commit

Permalink
feat(graphql): add support for graphql context propagation
Browse files Browse the repository at this point in the history
Signed-off-by: Raymond Feng <[email protected]>
  • Loading branch information
raymondfeng committed Sep 2, 2020
1 parent 9c31297 commit 6497d81
Show file tree
Hide file tree
Showing 7 changed files with 77 additions and 7 deletions.
7 changes: 6 additions & 1 deletion examples/graphql/src/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -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
Expand Down
3 changes: 2 additions & 1 deletion examples/graphql/src/datasources/recipes.datasource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
6 changes: 5 additions & 1 deletion examples/graphql/src/graphql-resolvers/recipe-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -27,6 +29,8 @@ export class RecipeResolver implements ResolverInterface<Recipe> {
@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})
Expand Down
43 changes: 43 additions & 0 deletions extensions/graphql/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,8 @@ export class RecipeResolver implements ResolverInterface<Recipe> {
@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,
) {}
}
```
Expand All @@ -265,6 +267,47 @@ export class RecipeResolver implements ResolverInterface<Recipe> {
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<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,
) {}
// ...
}
```

## Try it out

Check out
Expand Down
4 changes: 2 additions & 2 deletions extensions/graphql/src/graphql.container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
15 changes: 14 additions & 1 deletion extensions/graphql/src/graphql.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -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<ExpressContext> =
(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,
};
Expand Down
6 changes: 5 additions & 1 deletion extensions/graphql/src/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<GraphQLServer>(
Expand All @@ -17,6 +17,10 @@ export namespace GraphQLBindings {
'components.GraphQLComponent',
);

export const GRAPHQL_CONTEXT_RESOLVER = BindingKey.create<
ContextFunction<ExpressContext>
>('graphql.contextResolver');

export const RESOLVER_DATA = BindingKey.create<ResolverData<unknown>>(
'graphql.resolverData',
);
Expand Down

0 comments on commit 6497d81

Please sign in to comment.