How does this resolver extension function work?

Viewed 254

I need help interpreting a piece of code.

I have been attempting to learn how to authenticate users with jwt and returning tokens and refresh tokens.

It has been a fun ride and I came across this repository where the user added an extension/chain function which as far as I understand combines multiple graphql resolvers:

// I don't understand this function
const createResolver = (resolver) => {
  const baseResolver = resolver;
  baseResolver.createResolver = (childResolver) => {
    const newResolver = async (parent, args, context, info) => {
      await resolver(parent, args, context, info);
      return childResolver(parent, args, context, info);
    };
    return createResolver(newResolver);
  };
  return baseResolver;
};

export const requiresAuth = createResolver((parent, args, context) => {
  if (!context.user || !context.user.id) {
    throw new Error('Not authenticated');
  }
});

export const requiresAdmin = requiresAuth.createResolver((parent, args, context) => {
  if (!context.user.isAdmin) {
    throw new Error('Requires admin access');
  }
});

it is used like this:

Query: { 
    getBook: requiresAuth.createResolver((parent, args, { models }, info) =>
       // do something fun
    ),
}

I don't understand the createResolver function and I am asking if this is some type of programming paradigm where there are articles that I can read and understand it better.

Googling I did find graphql-resolvers which as far as I understand does the same thing but in this case, I would need to install yet another package which I would like to avoid but at the same time, I don't want to use a function that I don't fully understand.

Edit:

What I understand of the function/think I understand:

chains multiple functions together by calling one function and then inside of that function passing in a new function: requiresAuth.createResolver Inside of the function itself I'm having trouble figuring it out. We get a new variable baseResolver which we chain a new createResolver where we then recursively call the parent function.

I can't understand the flow of this I guess.

2 Answers

I've taken the liberty of renaming some things, because the existing names are very confusing.

const augment = (resolver) => {
  resolver.add = (nextResolver) => {
    const wrapper = async (parent, args, context, info) => {
      await resolver(parent, args, context, info)
      return nextResolver(parent, args, context, info)
    }
    return augment(wrapper)
  }
  return resolver
}

At every step of the way an .add method is added to the returned function, to enable chaining like so:

augment(resolver1).add(resolver2).add(resolver3)...

Every time .add is called, a new async lambda function (wrapper) is created. wrapper closes over both resolver and nextResolver. When it is eventually run, wrapper will call resolver, wait for the result, and then call nextResolver, and return the result.

wrapper is then passed to augment to add an .add function to it (to enable chaining). This new add function closes over the argument supplied to augment (which is wrapper). The augmented wrapper function is returned.

So, when .add is subsequently called, the resolver argument is the wrapper function of the call to .add on the previous resolver. So:

await resolver(parent, args, context, info)
return nextResolver(parent, args, context, info)

...will await the previously created wrapper function, and then invoke the latest nextResolver.

In this way, successive calls to .add construct a chain of async functions.

And when you want to run the chain, you can simply invoke the returned function, ignoring the .add property on it.

Note that each resolver is passed same arguments, to avoid the user having to worry about passing them on down the chain.

The function can be re-written as:

const augment = (resolver) => {
  resolver.add = (nextResolver) => 
    augment((...args) => 
      resolver(...args)
        .then(() => nextResolver(...args)))
  return resolver
}

...or:

const inSequence = (resolvers) => 
    (...args) => 
        resolvers.reduce((acc, el) => 
            acc.then(() => el(...args)), Promise.resolve())

const getBook = inSequence([auth, admin, getBookResolver])
const query = { getBook }

...or:

const inSequence = (resolvers) => 
    async (...args) => {
        for(let el of resolvers) {
            await el(...args)
        }
    }
const getBook = inSequence([auth, admin, getBookResolver])
const query = { getBook }
const createResolver = (resolver) => {
  const baseResolver = resolver;
  baseResolver.createResolver = (childResolver) => {
    const newResolver = async (parent, args, context, info) => {
      await resolver(parent, args, context, info);
      return childResolver(parent, args, context, info);
    };
    return createResolver(newResolver);
  };
  return baseResolver;
};

export const requiresAuth = createResolver( /*resolver_A*/ (parent, args, context) => {
  if (!context.user || !context.user.id) {
    throw new Error('Not authenticated');
  }
});

requiresAuth returned by createResolver is a function (passed as parameter, marked with resolver_A) decorated with additional function inside ... available by .createResolver()

export const requiresAdmin = requiresAuth.createResolver( /*resolver_B*/ (parent, args, context) => {
  if (!context.user.isAdmin) {
    throw new Error('Requires admin access');
  }
});

requiresAuth.createResolver( invoked with another resolver passed as parameter (resolver_B is childResolver) creates and returns new async resolver newResolver. newResolver when called awaits for results of resolver (resolver_A in this case) and then calls currently passed resolver (resolver_B).

createResolver(newResolver); not only return chained async resolver - function again ('reccurency') is decorated with .createResolver allowing to chain resolvers. You can again use .createResolver():

Query: { 
  editBook: requiresAdmin.createResolver((parent, args, { models }, info) =>

editBook will sequentially check auth (if (!context.user || !context.user.id) ...) and rights (if (!context.user.isAdmin) ...) before running target resolver.

It's equal to

editBook: createResolver(authResolverFn).createResolver(adminResolverFn).createResolver(editBookResolverFn)

... but only first createResolver is a name of 'external' function.

Related