Scoped Providers in GraphQL-Modules

theguild_

TheGuildBot

Posted on July 29, 2021

Scoped Providers in GraphQL-Modules

This article was published on Friday, January 11, 2019 by Arda Tanrikulu @ The Guild Blog

We recently released a new version of GraphQL-Modules with a new feature called Scoped Providers in
the dependency injection system of GraphQL-Modules.

Dependency injection in GraphQL-Modules is optional, and you can use it according to your
preference. It provides a solution for writing Providers, which are just classes, you can use to
write your business-logic, and this way to separate it from your API declaration, reuse it, and
communicate between modules easily.

This new feature allows you to define Providers to be used in different scopes;

Application Scope

If you define a provider in this scope which is default, the provider will be instantiated on
application-start and will be same in the entire application and all the following requests. The
providers in this scope can be considered as a shared state across all users' interactions with our
application. It's basically means that the instance will be treated as
Singleton.

For example, you have a provider called ExampleApplicationProvider , then this provider has a
counter in it;

import { Injectable, ProviderScope } from '@graphql-modules/di'

@Injectable({
  scope: ProviderScope.Application
})
export class ExampleApplicationProvider {
  counter = 0
  increment() {
    counter++
    return counter
  }
}
Enter fullscreen mode Exit fullscreen mode

And let's assume that we have a module declaration something like below;

import { GraphQLModule } from '@graphql-modules/core'
import { ExampleApplicationProvider } from './ExampleApplicationProvider'

export const ExampleModule = new GraphQLModule({
  providers: [ExampleApplicationProvider],
  typeDefs: `
   type Mutation {
    increment: Int
   }
 `,
  resolvers: {
    Mutation: {
      increment: (root, args, context) =>
        context.injector.get(ExampleApplicationProvider).increment()
    }
  }
})
Enter fullscreen mode Exit fullscreen mode

Finally, let's try to call the following GraphQL Request in multiple times;

mutation ExampleMutation {
  increment
}
Enter fullscreen mode Exit fullscreen mode

In first call, you will get 1 , but in the second call you will get 2 . And in every request
this value will be incremented; because Application-Scoped Providers are kept in memory until the
application is terminated; and the same instance of that provider will be used on each request.
Let's see other types of providers to understand this one better.

Session Scope

When a network request is arriving to your GraphQL-Server, GraphQL-Server calls the context factory
of the parent module. The parent module creates a session injector together with instantiating
session-scoped providers with that session object which contains the current context, session
injector and network request. This session object is passed through module's resolvers using
module's context.

In other words, providers defined in the session scope are constructed in the beginning of the
network request, then kept until the network request is closed. While application-scoped providers
is kept during the application runtime, and shared between all the following network requests and
resolvers inside these requests, this type of providers would not be shared between different
requests but in resolver calls those belong to same network request.

Let's try the same thing in that scope by creating a new provider called ExampleSessionProvider .

import { Injectable, ProviderScope } from '@graphql-modules/di'

@Injectable({
  scope: ProviderScope.Session
})
export class ExampleSessionProvider {
  counter = 0
  increment() {
    counter++
    return counter
  }
}
Enter fullscreen mode Exit fullscreen mode

Then change our module declaration to use this provider;

import { GraphQLModule } from '@graphql-modules/core'
import { ExampleSessionProvider } from './ExampleApplicationProvider'

export const ExampleModule = new GraphQLModule({
  providers: [ExampleSessionProvider],
  typeDefs: `
   type Mutation {
    increment: Int
   }
 `,
  resolvers: {
    Mutation: {
      increment: (root, args, context) => context.injector.get(ExampleSessionProvider).increment()
    }
  }
})
Enter fullscreen mode Exit fullscreen mode

So at this time, in every mutation call our Session-Scoped Provider's increment method will be
called and its value will be returned as a result of that mutation.

mutation ExampleMutation {
  increment
}
Enter fullscreen mode Exit fullscreen mode

In first call, you will get 1 , but in the second call you will get 1 again. But why? Because on
each request ExampleSessionProvider is instantiated from scratch specifically for that network
request which is called session in our DI system. But to see the point of the session scope, let's
assume we have two more resolvers called multiply . First let's add another method called
multiply that takes one argument to be multiplied by our counter value;

import { Injectable, ProviderScope } from '@graphql-modules/di'

@Injectable({
  scope: ProviderScope.Session
})
export class ExampleSessionProvider {
  counter = 0
  increment() {
    counter++
    return counter
  }
  multiply(times: number) {
    counter = counter * times
    return counter
  }
}
Enter fullscreen mode Exit fullscreen mode

After that, let's use this new method in our new resolver;

import { GraphQLModule } from '@graphql-modules/core'
import { ExampleApplicationProvider } from './ExampleApplicationProvider'

export const ExampleModule = new GraphQLModule({
  providers: [ExampleApplicationProvider],
  typeDefs: `
   type Mutation {
    increment: Int
    multiply(times: Int): Int
   }
 `,
  resolvers: {
    Mutation: {
      increment: (root, args, context) =>
        context.injector.get(ExampleApplicationProvider).increment(),
      multiply: (root, args, context) =>
        context.injector.get(ExampleApplicationProvider).multiply(args.times)
    }
  }
})
Enter fullscreen mode Exit fullscreen mode

When we call the following GraphQL request,

mutation ExampleMutation {
  increment
  multiply(times: 2)
}
Enter fullscreen mode Exit fullscreen mode

In first call, the initial counter value 0 will be incremented . Then the result becomes 1 and
this value will be returned as a result of increment mutation, but on the same request we have
multiply as well. So the same counter value will be multiplied by 2 , and this value will be
returned as a result of multiply mutation.

So, on every request the result will be:

{
  "increment": 1,
  "multiply": 2
}
Enter fullscreen mode Exit fullscreen mode

But if the provider is in ApplicationScope, the result will be [3,6] in the second call.

Request Scope

If you have request-scoped providers in your GraphQL Module, these providers are generated in every
injection. This means a request-scoped provider is never kept neither application state, nor session
state. So, this type of providers works just like
Factory. It creates an instance each time
you request from the injector.

Let's assume we do the similar changes for RequestScope. The results will be like below on each
request:

{
  "increment": 1,
  "multiply": 0
}
Enter fullscreen mode Exit fullscreen mode

Because request-scoped providers are constructed on each injector.get call. They are kept in the
current function scope, they're not kept in any session or DI container. That's why, we can consider
them factory .

As you can see the example and the results above, ExampleApplicationProvider is shared between
different network requests, while ExampleSessionProvider is shared between resolver-calls inside
the same network request. But, ExampleRequestProvider is only kept in the same resolver call.

New ModuleSessionInfo Built-In Provider

Let's assume that you have a token required for your authentication process. And this token is
probably stored in request header. And GraphQL-Modules provides you access to network request in a
built-in provider called ModuleSessionInfo. This session object is the raw request/response
object passed by your HTTP Server. For example, if you're using Express, you will get something like
{ req: IncommingMessage, res: Response } .

Every GraphQL-Module creates a ModuleSessionInfo instance in each network request that contains
raw Session from the GraphQL Server, SessionInjector that contains Session-scoped instances
together with Application-scoped ones and Context object which is constructed with
contextBuilder of the module. But, notice that you cannot use this built-in provider.

@Injectable({
  scope: ProviderScope.Session
})
class ExampleSessionScopeProvider {
  constructor(private moduleSessionInfo: ModuleSessionInfo) {
    moduleSessionInfo // { session, context, injector }
  }
}

@Injectable({
  scope: ProviderScope.Application
})
class ExampleInvalidApplicationScopeProvider {
  constructor(private moduleSessionInfo: ModuleSessionInfo) {} // Error: ModuleSessionInfo is not valid provider
}

@Injectable({
  scope: ProviderScope.Application
})
class ExampleApplicationScopeProvider implements OnRequest {
  onRequest(moduleSessionInfo) {
    moduleSessionInfo // { session, context, injector }
  }
}
Enter fullscreen mode Exit fullscreen mode

As you can see, you cannot use ModuleSessionInfo in Application Scope, because application-scoped
providers belong to the whole application, and application-scoped providers are completely
independent of the network request.

However, you can use onRequest hook to have ModuleSessionInfo in your Application-Scoped
provider which is called on each network request.

What's Next?

We are constantly trying to improve the set of tools provided by GraphQL-Modules, and we welcome
you to try it and share your experience and thoughts.

All Posts about GraphQL Modules

💖 💪 🙅 🚩
theguild_
TheGuildBot

Posted on July 29, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related