Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
August 25, 2021 04:29 pm GMT

How to configure GraphQL request with interceptors on the example of JWT authentication

  1. GraphQL request - minimalistic and simple graphql client that can be conveniently combined with any state manager.
  2. Interceptors - onvenient methods for modifying requests and responses that are widely used by http clients such as axios.
  3. As part of this tutorial, we will consider a configuration option for a GraphQL request using the example of forwarding a header with an access token to a request and intercepting a 401 response error to refresh this token.

Link to documentation: https://www.npmjs.com/package/graphql-request

So let's get started.

Step 1. Installing the package

yarn add graphql-request graphql

Step 2. Create a request context class

export class GQLContext {    private client: GraphQLClient    private snapshot: RequestSnapshot;    private readonly requestInterceptor = new RequestStrategy();    private readonly responseInterceptor = new ResponseStrategy();    public req: GQLRequest;    public res: GQLResponse;    public isRepeated = false;    constructor(client: GraphQLClient) {        this.client = client    }    async setRequest(req: GQLRequest) {        this.req = req        await this.requestInterceptor.handle(this)    }    async setResponse(res: GQLResponse) {        this.res = res        await this.responseInterceptor.handle(this)    }    async sendRequest(): Promise<GQLResponse> {        if (!this.snapshot) {            this.createSnapshot()        }        const res = await this.client.rawRequest.apply(this.client, new NativeRequestAdapter(this)) as GQLResponse        await this.setResponse(res)        return this.res    }    async redo(): Promise<GQLResponse> {        await this.snapshot.restore()        this.isRepeated = true        return await this.sendRequest()    }    createSnapshot() {        this.snapshot = new RequestSnapshot(this)    }}

This class will contain data about the request, response (upon receipt), as well as store the reference to the GQL client itself.
To set the request context, two methods are used: setRequest and setResponse. Each of them applies an appropriate strategy of using interceptors, each of which we will discuss below.

Let's take a look at the snapshot structure:

export class RequestSnapshot {    instance: GQLContext;    init: GQLRequest;    constructor(ctx: GQLContext) {        this.instance = ctx        this.init = ctx.req    }    async restore() {        await this.instance.setRequest(this.init)    }}

The snapshot receives a reference to the execution context, and also saves the state of the original request for subsequent restoration (if necessary) using the restore method

The sendRequest method will serve as a wrapper for gql-request, making it possible to create a snapshot of the original request using the createSnapshot method

NativeRequestAdapter is an adapter that serves to bring our context object to the form that the native gql-request can work with:

export function NativeRequestAdapter (ctx: GQLContext){    return Array.of(ctx.req.type, ctx.req.variables, ctx.req.headers)}

The redo method is used to repeat the original request and consists of three basic steps:
1) Reconstructing the context of the original request
2) Set the flag indicating that the request is repeated
3) Repeat the original request

Step 3. Registering our own error type

export class GraphQLError extends Error {    code: number;    constructor(message: string, code: number) {        super(message)        this.code = code    }}

In this case, we are simply extending the structure of a native JS error by adding a response code there.

Step 4. Writing an abstraction for an interceptor

For writing an abstraction of an interceptor, the "Chain of Responsibility (oR)" behavioral programming pattern is perfect. This pattern allows you to sequentially transfer objects along a chain of handlers, each of which independently decides how exactly the received object should be processed (in our case, the object will be our request context), as well as whether it is worth passing it further along the chain.
So let's take a closer look at this concept:

export type GQLRequest = {    type: string;    variables?: any;    headers?: Record<string, string>}export type GQLResponse = {    data: any    extensions?: any    headers: Headers,    status: number    errors?: any[];}interface Interceptor {    setNext(interceptor: Interceptor): Interceptor;    intercept(type: GQLContext): Promise<GQLContext>;}export abstract class AbstractInterceptor implements Interceptor {    private nextHandler: Interceptor;    public setNext(interceptor: Interceptor): Interceptor {        this.nextHandler = interceptor        return interceptor    }    public async intercept(ctx: GQLContext) {        if (this.nextHandler) return await this.nextHandler.intercept(ctx)        return ctx    }}

You can see two methods here:

  1. setNext - designed to set the next interceptor in the chain, a reference to which we will store in the nextHandler property
  2. intercept - the parent method is intended to transfer control to the next handler. This method will be used by child classes if necessary

Step 5. Request Interceptor Implementation

export class AuthInterceptor extends AbstractInterceptor{    intercept(ctx: GQLContext): Promise<GQLContext> {        if (typeof window !== 'undefined') {            const token = window.localStorage.getItem('token')            if (!!token && token !== 'undefined') {                ctx.req.headers = {                ...ctx.req.headers,                 Authorization: `Bearer ${token}`                }            }        }        return super.intercept(ctx)     }}

This interceptor gets the access token from localStorage and adds a header with the token to the request context

Step 6. Response Interceptor Implementation

Here we will implement interception of 401 errors and, if received, we will make a request to refresh the token and repeat the original request.

export const REFRESH_TOKEN = gql`    query refreshToken {        refreshToken{            access_token        }    }`export class HandleRefreshToken extends AbstractInterceptor {    async intercept(ctx: GQLContext): Promise<GQLContext> {        if ( !('errors' in ctx.res)) return await super.intercept(ctx)        const exception = ctx.res.errors[0]?.extensions?.exception        if (!exception) return await super.intercept(ctx)        const Error = new GraphQLError(exception.message, exception.status)        if (Error.code === 401 && !ctx.isRepeated && typeof window !== 'undefined') {            try {                await ctx.setRequest({type: REFRESH_TOKEN})                const res = await ctx.sendRequest()                localStorage.setItem('token', res.refreshToken.access_token)                await ctx.redo()                return await super.intercept(ctx)            } catch (e) {                throw Error            }        }        throw Error    }}
  1. First, we check if there are any errors in the request. If not, then we transfer control to the next handler. If so, we are trying to get the exeption.

  2. From the exeption we get the response status and the error code

  3. heck if the error code is 401, then we make a request to refresh the token, and write a new access token in localStorage

  4. Then we repeat the original request using the redo method, which we discussed earlier.

  5. If this operation is successful, then we pass the request to the next handler. Otherwise, throw an error and stop processing.

Step 7. Writing a strategy abstraction

export abstract class InterceptStrategy {    protected makeChain(collection: AbstractInterceptor[]) {        collection.forEach((handler, index) => collection[index + 1] && handler.setNext(collection[index + 1]))    }    abstract handle(ctx: GQLContext): any;}

Strategy abstraction is represented by two methods:

  1. makeChain - a helper that allows you to conveniently assemble a chain of handlers from an array
  2. handle - a method that implements the main logic of the processing strategy, we will describe it in the implementations

Step 8. Implementing request and response interception strategies

export class RequestStrategy extends InterceptStrategy{    async handle(ctx: GQLContext): Promise<GQLContext> {        const handlersOrder: AbstractInterceptor[] = [            new AuthInterceptor(),        ]        this.makeChain(handlersOrder)        return await handlersOrder[0].intercept(ctx)    }}export class ResponseStrategy extends InterceptStrategy{    async handle(ctx: GQLContext): Promise<GQLResponse['data']> {        const handlersOrder: AbstractInterceptor[] = [            new HandleRefreshToken(),            new RetrieveDataInterceptor(),        ]        this.makeChain(handlersOrder)        return await handlersOrder[0].intercept(ctx)    }}

As we can see, both strategies look absolutely identical in structure. Notice the handle method, which:

  1. Determines the order of invocation of handlers
  2. Creates a chain of them using the parent makeChain method
  3. And starts the processing

Step 9. Putting it all together.

const request = async function (this: GraphQLClient, type: string, variables: any, headers = {}): Promise<any> {    const ctx = new GQLContext(this)    await ctx.setRequest({type, variables, headers})    try {        await ctx.sendRequest()    } catch (e) {        await ctx.setResponse(e.response)    }    return ctx.res}GraphQLClient.prototype.request = requestexport const client = new GraphQLClient('http://localhost:4000/graphql', {    credentials: 'include',})
  1. Override the base request method supplied by the package.
  2. Inside our method, create a context
  3. Set the initial parameters of the request
  4. Send a request and set a response
  5. Returning response data
  6. Export the created client

Thanks for reading. I would be glad to receive your feedback.
Link to repository: https://github.com/IAlexanderI1994/gql-request-article


Original Link: https://dev.to/ialexanderi1994/how-to-configure-graphql-request-with-interceptors-on-the-example-of-jwt-authentication-2o86

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To