Pete Corey Writing Work Contact

Anonymizing GraphQL Resolvers with Decorators

Written by Pete Corey on Apr 22, 2019.

As software developers and application owners, we often want to show off what we’re working on to others, especially if there’s some financial incentive to do so. Maybe we want to give a demo of our application to a potential investor or a prospective client. The problem is that staging environments and mocked data are often lifeless and devoid of the magic that makes our project special.

In an ideal world, we could show off our application using production data without violating the privacy of our users.

On a recently client project we managed to do just that by modifying our GraphQL resolvers with decorators to automatically return anonymized data. I’m very happy with the final solution, so I’d like to give you a run-through.

Setting the Scene

Imagine that we’re working on a Node.js application that uses Mongoose to model its data on the back-end. For context, imagine that our User Mongoose model looks something like this:


const userSchema = new Schema({
  name: String,
  phone: String
});

const User = mongoose.model('User', userSchema);

As we mentioned before, we’re using GraphQL to build our client-facing API. The exact GraphQL implementation we’re using doesn’t matter. Let’s just assume that we’re assembling our resolver functions into a single nested object before passing them along to our GraphQL server.

For example, a simple resolver object that supports a user query might look something like this:


const resolvers = {
  Query: {
    user: (_root, { _id }, _context) => {
      return User.findById(_id);
    }
  }
};

Our goal is to return an anonymized user object from our resolver when we detect that we’re in “demo mode”.

Updating Our Resolvers

The most obvious way of anonymizing our users when in “demo mode” would be to find every resolver that returns a User and manually modify the result before returning:


const resolvers = {
  Query: {
    user: async (_root, { _id }, context) => {
      let user = await User.findById(_id);

      // If we're in "demo mode", anonymize our user:
      if (context.user.demoMode) {
        user.name = 'Jane Doe';
        user.phone = '(555) 867-5309';
      }

      return user;
    }
  }
};

This works, but it’s a high touch, high maintenance solution. Not only do we have to comb through our codebase modifying every resolver function that returns a User type, but we also have to remember to conditionally anonymize all future resolvers that return User data.

Also, what if our anonymization logic changes? For example, what if we want anonymous users to be given the name 'Joe Schmoe' rather than 'Jane Doe'? Doh!

Thankfully, a little cleverness and a little help from Mongoose opens the doors to an elegant solution to this problem.

Anonymizing from the Model

We can improve on our previous solution by moving the anonymization logic into our User model. Let’s write an anonymize Mongoose method on our User model that scrubs the current user’s name and phone fields and returns the newly anonymized model object:


userSchema.methods.anonymize = function() {
  return _.extend({}, this, {
    name: 'Jane Doe',
    phone: '(555) 867-5309'
  });
};

We can refactor our user resolver to make use of this new method:


async (_root, { _id }, context) => {
  let user = await User.findById(_id);

  // If we're in "demo mode", anonymize our user:
  if (context.user.demoMode) {
    return user.anonymize();
  }

  return user;
}

Similarly, if we had any other GraphQL/Mongoose types we wanted to anonymize, such as a Company, we could add an anonymize function to the corresponding Mongoose model:


companySchema.methods.anonymize = function() {
  return _.extend({}, this, {
    name: 'Initech'
  });
};

And we can refactor any resolvers that return a Company GraphQL type to use our new anonymizer before returning a result:


async (_root, { _id }, context) => {
  let company = await Company.findById(_id);

  // If we're in "demo mode", anonymize our company:
  if (context.user.demoMode) {
    return company.anonymize();
  }

  return company;
}

Going Hands-off with a Decorator

Our current solution still requires that we modify every resolver in our application that returns a User or a Company. We also need to remember to conditionally anonymize any users or companies we return from resolvers we write in the future.

This is far from ideal.

Thankfully, we can automate this entire process. If you look at our two resolver functions up above, you’ll notice that the anonymization process done by each of them is nearly identical.

We anonymize our User like so:


// If we're in "demo mode", anonymize our user:
if (context.user.demoMode) {
  return user.anonymize();
}

return user;

Similarly, we anonymize our Company like so:


// If we're in "demo mode", anonymize our company:
if (context.user.demoMode) {
  return company.anonymize();
}

return company;

Because both our User and Company Mongoose models implement an identical interface in our anonymize function, the process for anonymizing their data is the same.

In theory, we could crawl through our resolvers object, looking for any resolvers that return a model with an anonymize function, and conditionally anonymize that model before returning it to the client.

Let’s write a function that does exactly that:


const anonymizeResolvers = resolvers => {
  return _.mapValues(resolvers, resolver => {
    if (_.isFunction(resolver)) {
      return decorateResolver(resolver);
    } else if (_.isObject(resolver)) {
      return anonymizeResolvers(resolver);
    } else if (_.isArray(resolver)) {
      return _.map(resolver, resolver => anonymizeResolvers(resolver));
    } else {
      return resolver;
    }
  });
};

Our new anonymizeResolvers function takes our resolvers map and maps over each of its values. If the value we’re mapping over is a function, we call a soon-to-be-written decorateResolver function that will wrap the function in our anonymization logic. Otherwise, we either recursively call anonymizeResolvers on the value if it’s an array or an object, or return it if it’s any other type of value.

Our decorateResolver function is where our anonymization magic happens:


const decorateResolver = resolver => {
  return async function(_root, _args, context) {
    let result = await resolver(...arguments);
    if (context.user.demoMode &&
        _.chain(result)
         .get('anonymize')
         .isFunction()
         .value()
    ) {
      return result.anonymize();
    } else {
      return result;
    }
  };
};

In decorateResolver we replace our original resolver function with a new function that first calls out to the original, passing through any arguments our new resolver received. Before returning the result, we check if we’re in demo mode and that the result of our call to resolver has an anonymize function. If both checks hold true, we return the anonymized result. Otherwise, we return the original result.

We can use our newly constructed anonymizeResolvers function by wrapping it around our original resolvers map before handing it off to our GraphQL server:


const resolvers = anonymizeResolvers({
  Query: {
    ...
  }
});

Now any GraphQL resolvers that return any Mongoose model with an anonymize function with return anonymized data when in demo mode, regardless of where the query lives, or when it’s written.

Final Thoughts

While I’ve been using Mongoose in this example, it’s not a requirement for implementing this type of solution. Any mechanism for “typing” objects and making them conform to an interface should get you where you need to go.

The real magic here is the automatic decoration of every resolver in our application. I’m incredibly happy with this solution, and thankful that GraphQL’s resolver architecture made it so easy to implement.

My mind is buzzing with other decorator possibilities. Authorization decorators? Logging decorators? The sky seems to be the limit. Well, the sky and the maximum call stack size.

FizzBuzz is Just a Three Against Five Polyrhythm

Written by Pete Corey on Apr 8, 2019.

Congratulations, you’re now the drummer in my band. Unfortunately, we don’t have any drums, so you’ll have to make due by snapping your fingers. Your first task, as my newly appointed drummer, is to play a steady beat with your left hand while I lay down some tasty licks on lead guitar.

Great! Now let’s add some spice to this dish. In the span of time it takes you to snap three times with your left hand, I want you to lay down five evenly spaced snaps with your right. You probably already know this as the drummer in our band, but this is called a polyrhythm.

Sound easy? Cool, give it a try!

Hmm, I guess being a drummer is harder than I thought. Let’s take a different approach. This time, just start counting up from one. Every time you land on a number divisible by three, snap your left hand. Every time you land on a number divisible by five, snap your right hand. If you land on a number divisible by both three and five, snap both hands.

Go!

You’ll notice that fifteen is the first number we hit that requires we snap both hands. After that, the snapping pattern repeats. Congratulations, you don’t even have that much to memorize!

Here, maybe it’ll help if I draw things out for you. Every character represents a tick of our count. "ı" represents a snap of our left hand, ":" represents a snap of our right hand, and "i" represents a snap of both hands simultaneously.

But man, I don’t want to have to manually draw out a new chart for you every time I come up with a sick new beat. Let’s write some code that does it for us!


_.chain(_.range(1, 15 + 1))
    .map(i => {
        if (i % 3 === 0 && i % 5 === 0) {
            return "i";
        } else if (i % 3 === 0) {
            return "ı";
        } else if (i % 5 === 0) {
            return ":";
        } else {
            return ".";
        }
    })
    .join("")
    .value();

Here’s the printout for the “three against five” polyrhythm I need you to play:

..ı.:ı..ı:.ı..i

But wait, this looks familiar. It’s FizzBuzz! Instead of printing "Fizz" for our multiples of three, we’re printing "i", and instead of printing "Buzz" for our multiples of five, we’re printing "ı".

FizzBuzz is just a three against five polyrhythm.

We could even generalize our code to produce charts for any kind of polyrhythm:


const polyrhythm = (pulse, counterpulse) =>
    _.chain(_.range(1, pulse * counterpulse + 1))
    .map(i => {
            if (i % pulse === 0 && i % counterpulse === 0) {
                return "i";
            } else if (i % pulse === 0) {
                return "ı";
            } else if (i % counterpulse === 0) {
                return ":";
            } else {
                return ".";
            }
        })
        .join("")
        .value();

And while we’re at it, we could drop this into a React project and create a little tool that does all the hard work for us:

Anyways, we should get back to work. We have a Junior Developer interview lined up for this afternoon. Maybe we should have them play us a polyrhythm to gauge their programming abilities?

Bending Jest to Our Will: Restoring Node's Require Behavior

Written by Pete Corey on Mar 25, 2019.

Jest does some interesting things to Node’s default require behavior. In an attempt to encourage test independence and concurrent test execution, Jest resets the module cache after every test.

You may remember one of my previous articles about “bending Jest to our will” and caching instances of modules across multiple tests. While that solution works for single modules on a case-by-case basis, sometimes that’s not quite enough. Sometimes we just want to completely restore Node’s original require behavior across the board.

After sleuthing through support tickets, blog posts, and “official statements” from Jest core developers, this seems to be entirely unsupported and largely impossible.

However, with some highly motivated hacking I’ve managed to find a way.

Our Goal

If you’re unfamiliar with how require works under the hood, here’s a quick rundown. The first time a module is required, its contents are executed and the resulting exported data is cached. Any subsequent require calls of the same module return a reference to that cached data.

That’s all there is to it.

Jest overrides this behavior and maintains its own “module registry” which is blown away after every test. If one test requires a module, the module’s contents are executed and cached. If that same test requires the same module, the cached result will be returned, as we’d expect. However, other tests don’t have access to our first test’s module registry. If another test tries to require that same module, it’ll have to execute the module’s contents and store the result in its own private module registry.

Our goal is to find a way to reverse Jest’s monkey-patching of Node’s default require behavior and restore it’s original behavior.

This change, or reversal of a change, will have some unavoidable consequences. Our Jest test suite won’t be able to support concurrent test processes. This means that all our tests will have to run “in band”(--runInBand). More interestingly, Jest’s “watch mode” will no longer work, as it uses multiple processes to run tests and maintain a responsive command line interface.

Accepting these limitations and acknowledging that this is likely a very bad idea, let’s press on.

Dependency Hacking

After several long code reading and debugging sessions, I realized that the heart of the problem resides in Jest’s jest-runtime module. Specifically, the requireModuleOrMock function, which is responsible for Jest’s out-of-the-box require behavior. Jest internally calls this method whenever a module is required by a test or by any code under test.

Short circuiting this method with a quick and dirty require causes the require statements throughout our test suites and causes our code under test to behave exactly as we’d expect:


requireModuleOrMock(from: Path, moduleName: string) {
+ return require(this._resolveModule(from, moduleName));
  try {
    if (this._shouldMock(from, moduleName)) {
      return this.requireMock(from, moduleName);
    } else {
      return this.requireModule(from, moduleName);
    }
  } catch (e) {
    if (e.code === 'MODULE_NOT_FOUND') {
      const appendedMessage = findSiblingsWithFileExtension(
        this._config.moduleFileExtensions,
        from,
        moduleName,
      );

      if (appendedMessage) {
        e.message += appendedMessage;
      }
    }
    throw e;
  }
}

Whenever Jest reaches for a module, we relieve it of the decision to use a cached module from it’s internally maintained moduleRegistry, and instead have it always return the result of requiring the module through Node’s standard mechanisms.

Patching Jest

Our fix works, but in an ideal world we wouldn’t have to fork jest-runtime just to make our change. Thankfully, the requireModuleOrMock function isn’t hidden within a closure or made inaccessible through other means. This means we’re free to monkey-patch it ourselves!

Let’s start by creating a test/globalSetup.js file in our project to hold our patch. Once created, we’ll add the following lines:


const jestRuntime = require('jest-runtime');

jestRuntime.prototype.requireModuleOrMock = function(from, moduleName) {
    return require(this._resolveModule(from, moduleName));
};

We’ll tell our Jest setup to use this config file by listing it in our jest.config.js file:


module.exports = {
    globalSetup: './test/globalSetup.js',
    ...
};

And that’s all there is to it! Jest will now execute our globalSetup.js file once, before all of our test suites, and restore the original behavior of require.

Being the future-minded developers that we are, it’s probably wise to document this small and easily overlooked bit of black magic:


/*
 * This requireModuleOrMock override is _very experimental_. It affects
 * how Jest works at a very low level and most likely breaks Jest-style
 * module mocks.
 *
 * The upside is that it lets us evaluate heavy modules once, rather
 * that once per test.
 */

jestRuntime.prototype.requireModuleOrMock = function(from, moduleName) {
    return require(this._resolveModule(from, moduleName));
};

If you find yourself with no other choice but to perform this incantation on your test suite, I wish you luck. You’re most likely going to need it.