Giter Site home page Giter Site logo

gravemind-concept's Introduction

Gravemind Concept

A minimum reproduction of gravemind-apollo to demonstrate some of the required changes for upgrading to the latest versions of NestJS and Apollo Server.

The main goals of this repo are to demonstrate the following:

  1. Data sources accessing other data sources.
  2. Context access in the base data source class that avoids context "bleed" situations.

Importing other data sources

In current versions of gravemind-apollo, all data sources are available through the current context. This makes it easy for data sources to access each other for more complicated requests. This doesn't seem to be possible with the new request-scoped data sources. Instead you have to import them the way you would any other NestJS module.

For instance, to use the PokemonAPI from the AnimalsAPI, we need to first import the module which exports this service:

@Module({
  imports: [PokemonModule],
  providers: [AnimalsAPI, AnimalsResolver],
  exports: [AnimalsAPI],
})
export class AnimalsModule {}

Then, the AnimalsAPI needs to declare the PokemonAPI so Nest knows to inject this service from the imported module:

@Injectable({ scope: Scope.REQUEST })
export class AnimalsAPI extends GqlDataSource {
  constructor(
    @Inject(CONTEXT) context: GqlContext,
    private readonly pokemonAPI: PokemonAPI
  ) {
    super(context);
  }

  async getAnimalsAndPokemon() {
    const pokemon = await this.pokemonAPI.getPokemon();

    // rest of the method...
  }
}

Per-request context

By scoping each API class to REQUEST, we can ensure that the relevant services get created with each request. This also allows us to use the @Inject(CONTEXT) decorator in the class constructor which is then passed to the base class. This is important because some headers are automatically included with every outbound request.

@Injectable({ scope: Scope.REQUEST })
export class PokemonAPI extends GqlDataSource {
  constructor(@Inject(CONTEXT) context: GqlContext) {
    super(context);
  }

  // ...rest of the class
}
export class GqlDataSource extends RESTDataSource {
  constructor(public readonly context: GqlContext) {
    super();
  }

  protected willSendRequest(_path: string, options: AugmentedRequest) {
    // Automatically include the context's token with each outbound request
    options.headers[JWT_HEADER] = this.context.token;
  }

  // ...rest of the base class
}

Preventing context bleed 🩸

TokenAPI used for demonstration:

@Injectable()
export class TokenAPI extends GqlDataSource {
  async getToken() {
    return this.context.token;
  }
  async getTokenLong() {
    // Simulated delay before resolving the context's token
    await new Promise((resolve) => {
      setTimeout(resolve, 3000);
    });

    return this.context.token;
  }
}

Node.js script:

async function getToken(token) {
  const res = await query(token, 'query GetToken { token }');
  console.log(`Request token: ${token}; Result token: ${res.data.token}`);
}

async function getTokenLong(token) {
  const res = await query(token, 'query GetTokenLong { tokenLong }');
  console.log(`Request token: ${token}; Result token: ${res.data.tokenLong}`);
}

async function query(token, query) {
  const res = await fetch('http://localhost:8080/graphql', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${token}`,
      Accept: 'application/json',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ query }),
  });
  return res.json();
}

// Use Promise.all() so neither request blocks the other
await Promise.all([getTokenLong('abc456'), getToken('abc123')]);

In current versions of gravemind-apollo, these are the results:

Request token: abc123; Result token: abc123
Request token: abc456; Result token: abc123

Even though the request with token abc456 started before the request with token abc123, we received that request's token in response. This happens because the context in the current setup is essentially just a shared property that continuously gets overwritten by incoming requests. This means if your request performs any operations before trying to access the current context, it's highly likely that the context has been overwritten, especially in busier environments such as production.

By scoping the API classes per-request, the results are as expected:

Request token: abc123; Result token: abc123
Request token: abc456; Result token: abc456

gravemind-concept's People

Contributors

dtgreene avatar

Watchers

 avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.