Apollo-link: [Feature Idea]: Provide client reference or cache reference so links can clean up

Created on 28 Feb 2018  路  7Comments  路  Source: apollographql/apollo-link

If you follow the hello-world example for apollo-link-context you make two links, a context link and an error link. These are great, but when a user logs out, you often need to invalidate more data than just that.

It would be great if you could get a reference to the client or cache that is passed along side. Possibly in the Operation Interface. Then you can directly writeFragment or whatever you need to work with the cache.

enhancement

Most helpful comment

This would be useful for:

  • apollo-link-context
  • apollo-link-state
  • apollo-link-error
  • apollo-link-rest
  • apollo-link-schema

All 7 comments

This would be useful for:

  • apollo-link-context
  • apollo-link-state
  • apollo-link-error
  • apollo-link-rest
  • apollo-link-schema

This is a really interesting idea! I would love to make that interop easier. The issue is that apollo-link is used as a universal GraphQL fetcher, namely in graphql-tools. I think the right route here is adding stronger typing to the context, so that we would know if client or cache is on the context. Then we can know if the link chain has access to a client or cache.

One way of adding the typings is with something similar to React PropTypes. Are there other strategies that might be better?

That's a really good point @evans. Because interop between all the use cases, it's not exactly obvious where we could wire this in. -- It looks like there is a bunch of default context provided with each request from user-provided context, maybe we can add an entry that ApolloClient would provide and wire through.

PropTypes is neat but not at all obvious how to wire in unless you mean ApolloLink would have a contextPropTypes hash, and would through at the link level there? What were you thinking about? It's probably the only obvious solution as I can't imagine how to make this with TypeScript generics.

You can always use setter injection to inject the client into your link after it has been created to solve the problem right now, but of course it鈥檚 a quite common usecase and would be easier if it was in context.

@MrLoh would you mind elaborating on "setter injection"? Googling "apollo graphql setter injection" didn't give me much.

If it's like dependency injection then my question is: is it theoretically safe to nest a second query inside the dispatch of a first one? (Obviously, I'm assuming the nested query doesn't recurse infinitely).

I mean you want to inject it, and there are three ways to do so. We can't use constructor injection, because the client is defined after the link, but we can use setter injection on the client. What you are advocating is to inject the client not into the link but into the context of the operation via automatic constructor injection by apollo.

My current solution for automatically refreshing an access token when it fails looks like this: The gist is just using the injected this.client wherever it is needed.

import {getToken, REFRESH_TOKEN_MUTATION} from './auth-service'


export class AuthLink extends ApolloLink {
    tokenRefreshingPromise: Promise < boolean > | null;

    injectClient = (client: ApolloClient): void => {
        this.client = client;
    };

    refreshToken = (): Promise < boolean > => {
        if (!this.tokenRefreshingPromise) {
            this.tokenRefreshingPromise = this.client.mutate(REFRESH_TOKEN_MUTATION);
        }
        return this.tokenRefreshingPromise;
    };

    setTokenHeader = (operation: Operation): void => {
        const token = getToken();
        if (token) operation.setContext({
            headers: {
                authorization: `Bearer ${token}`
            }
        });
    };

    request(operation: Operation, forward: NextLink) {
        // set token in header
        this.setTokenHeader(operation);
        // try refreshing token once if it has expired
        return new Observable(observer => {
            let subscription, innerSubscription;
            try {
                subscription = forward(operation).subscribe({
                    next: observer.next.bind(observer),
                    complete: observer.complete.bind(observer),
                    error: netowrkError => {
                        if (netowrkError.statusCode === 401) {
                            this.refreshToken().then(success => {
                                if (success) {
                                    // set new token and retry operation
                                    this.setTokenHeader(operation);
                                    innerSubscription = forward(operation).subscribe(observer);
                                } else {
                                    // throw error
                                    observer.error(new Error('jwt refresh failed'));
                                }
                            });
                        } else {
                            observer.error(netowrkError);
                        }
                    },
                });
            } catch (e) {
                observer.error(e);
            }
            return () => {
                if (subscription) subscription.unsubscribe();
                if (innerSubscription) innerSubscription.unsubscribe();
            };
        });
    }
}

and here I am injecting the client into the link

const authLink = new AuthLink();
const httpLink = new HttpLink();
const link = ApolloLink.from([authLink, httpLink]);

const client = new ApolloClient({ link, cache });

authLink.injectClient(client);
Was this page helpful?
0 / 5 - 0 ratings