dotansimha / graphql-binding

Auto-generated SDK for your GraphQL API (supports schema stitching & codegen)
MIT License
378 stars 59 forks source link

How to dynamically update the link headers in the binding instance ? #164

Closed cRicateau closed 4 years ago

cRicateau commented 6 years ago

I use graphql-binding in an express server to query another graphql server. On each request, I need to pass a header whose value changes everytime (it comes from the express req object and it is used for tracing ). I create a new HttpLink and a new GraphQLServiceBinding instance each time, but it creates a memory leak on my server (see code below).

Is there a way to dynamically change the headers in my link instance without re-creating a new instance of GraphQLServiceBinding each time ?

I use the function getGraphqlClient to create a graphql client in all my requests:

import { introspectSchema } from 'graphql-tools'
import fetch from 'node-fetch'
import { HttpLink } from 'apollo-link-http'
import { Binding } from 'graphql-binding'
import { makeRemoteExecutableSchema } from 'graphql-tools'

const graphqlServerUrl = process.env.GRAPHQL_URL

class GraphQLServiceBinding extends Binding {
  constructor(introspectedSchema, link) {
    const schema = makeRemoteExecutableSchema({
      link,
      schema: introspectedSchema,
    })

    super({ schema })
  }
}

let cachedSchema = null

async function getCachedSchema(link) {
  if (cachedSchema) return cachedSchema
  cachedSchema = await introspectSchema(link)
  return cachedSchema
}

// function getGraphqlClient is used to instantiate a new binding with specific headers
export const getGraphqlClient = async (graphQLServerUrl, headers = {}) => {
  const uri = graphQLServerUrl || process.env.GRAPHQL_URL
  if (!uri) {
    throw new Error('GRAPHQL_URL variable is mandatory')
  }

  try {
    // the headers change on every request
    const link = new HttpLink({ uri, fetch, headers })
    // the schema doesn't change
    const schema = await getCachedSchema(link)
    const binding = new GraphQLServiceBinding(schema, link)

    return binding
  } catch (err) {
    console.error(
      `Error introspecting remote schema on url ${graphQLServerUrl}`
    )
    throw err
  }
}
kojuka commented 6 years ago

@cRicateau were you able to figure this out? I have the same question

nemcek commented 5 years ago

I had similar use-case and I was able to figure it out. Basically instead of passing fetch from node-fetch or cross-fetch to the constructor of HttpLink you can pass function which modifies the headers and then calls the mentioned fetch.

You can extend HttpLink class and do something like this:

import { HttpLink } from 'apollo-link-http';
import { fetch } from 'cross-fetch';
import { Request } from 'express';

class HttpLinkWithHeaders extends HttpLink {
  // Whole request or only headers if you want
  private req: Request = null;

  constructor() {
    super({
      uri: `....`,
      fetch: (input: RequestInfo, init?: RequestInit) => {
        // Modify init.headers based on this.req here

        return fetch(input, init);
      },
    });
  }

  // Call this on every request
  public setHtppRequest = (req: Request) => {
    this.req = req;
  };
}
lambrojos commented 5 years ago

You can use the apollo-link-context module to set headers based on the request context:

const fetch = require('node-fetch');
const { Binding } = require('graphql-binding');
const { HttpLink } = require('apollo-link-http');
const { makeRemoteExecutableSchema, introspectSchema } = require('graphql-tools');
const { setContext } = require('apollo-link-context');

const http = new HttpLink({ uri: process.env.GRAPHQL_ENDPOINT, fetch });
const link = setContext((request, { graphqlContext }) => (
  graphqlContext ? {
    headers: {
      Authorization: `Bearer ${graphqlContext.auth}`,
    },
  } : {})).concat(http);

const schemaPromise = introspectSchema(link);

module.exports.graphql = schemaPromise
  .then(typeDefs => new class GraphQLBinding extends Binding {
    constructor() {
      super({
        schema: makeRemoteExecutableSchema({ link, schema: typeDefs }),
      });
    }
  }());

You can specify the context as a property of the third parameter in your bindings:

  const auth = 'auth token';
  const result = await bindings.query.whatevers({
    skip: 0,
    limit: 10,
  }, `{ items { id, body }}`, { context: { auth } });
Urigo commented 4 years ago

Thank you for reporting.

In the last few months, since the transition of many libraries under The Guild's leadership, We've reviewed and released many improvements and versions to graphql-cli, graphql-config and graphql-import.

We've reviewed graphql-binding, had many meetings with current users and engaged the community also through the roadmap issue.

What we've found is that the new GraphQL Mesh library is covering not only all the current capabilities of GraphQL Binding, but also the future ideas that were introduced in the original GraphQL Binding blog post and haven't come to life yet.

And the best thing - GraphQL Mesh gives you all those capabilities, even if your source is not a GraphQL service at all!
it can be GraphQL, OpenAPI/Swagger, gRPC, SQL or any other source! And of course you can even merge all those sources into a single SDK.

Just like GraphQL Binding, you get a fully typed SDK (thanks to the protocols SDKs and the GraphQL Code Generator), but from any source, and that SDK can run anywhere, as a connector or as a full blown gateway. And you can share your own "Mesh Modules" (which you would probably call "your own binding") and our community already created many of those! Also, we decided to simply expose regular GraphQL, so you can choose how to consume it using all the awesome fluent client SDKs out there.

If you think that we've missed anything from GraphQL Binding that is not supported in a better way in GraphQL Mesh, please let us know!


In the context of that particular issue - GraphQL Mesh transform API to add and update the requests at any point in the process, so setting headers dynamically is easy.

We're looking forward for your feedback of how we can make your experience even better!

timotgl commented 4 years ago

@cRicateau We also observed the memory leak when creating an instance of Binding on every request. The solution suggested by @lambrojos to dynamically set headers did not work for us (not 100% sure if we did it correctly though). Our solution was to use https://github.com/prisma-labs/graphql-request which makes it very easy to set headers on the client instance at any time. We haven't found any graphql client that provides the same auto-generated methods for queries and mutations though, like this project does. Haven't really investigated if https://the-guild.dev/blog/graphql-mesh does this, as it seems to covering different needs and we just needed a simple nodejs graphql client.

Urigo commented 4 years ago

@timotgl could you share a simple example in a repo? then we could PR it with a couple of options so you could compare