Generation Methods

GraphGen makes it easy to create good data by default

In the section introducing fields, we touched very briefly on attaching custom generation methods to fields by using the @gen directive. It is important to know how this works, but it would not be great if generating your data meant attaching a @gen directive to every single. Fortunately, GraphGen allows you to paint with a much larger brush. In this section, we'll show how you can effeciently write "generation middleware" to quickly match against hundreds of fields at a time so you don't have to specify each one individually.

If you don't want to learn about the nitty gritty, but intsead learn how to use it to accomplish most of what you need, you can skip ahead to the section describing the full integration with faker

Generation Middlewares

Let's have another look at our example from the section on fields. We created our GraphGen with a generator function that receives information about what needs to be generated, and then make a decision about what to do with it:

import { faker } from "@faker-js/faker";

const graphgen = createGraphGen({
  source,
  sourcename: "world.graphql",
  generate(info) {
    switch (info.method) {
      case "human.fullName":
        return faker.name.fullName();
      case "human.age":
        return faker.datatype.number({ max: 100, min: 0 });
      default:
        // fallback to the next generation functions
        return info.next();
    }
  },
});

The switch statement decides if each case is one that it will handle. If it is, then we handle it, otherwise, we "pass it off" to the other generators further down the line.

If the template for this logic seems familiar, it is probably because it is based on the concept of "middleware" prevalent in many HTTP stacks.

But writing switch statements for what could be hundreds or even thousands of fields in your generation schema is no way to live your life. Instead, GraphGen has a way of specifying the way to generate a value and then apply that value to many different fields at once called Dispatch to use it, you specify a set of "generation methods" and then provide a set of patterns to match the fields of your schema to one of those generation methods. To get started, import the createDispatch function.

import { createDispatch } from '@frontside/graphgen';

We can use it to implement our example from before by first definting the name and age methods in the methods options, and then matching them to our fields in our schema with the patterns option.

let nameAndAgeGen = createDispatch({
  methods: {
    "human.fullName": () => faker.name.fullName(),
    "human.age": () => faker.datatype.number({ max: 100, min: 0 }),
  },
  patterns: {
    ".name": "human.fullName",
    "*.age": "human.age",
  }
});

This resulting function nameAndAgeGen is assignable to the generate option of createGraphGen:

let graphgen = createDispatch({
  source,
  sourcename: "world.graphql",
  generate: nameAndAgeGen
});

Notice that we can now specify our schema without explicity using @gen() directives because our generation dispatch matches the Person.name and Person.age fields and automatically applies our generation methods to them. Not only that, but because we used the glob patterns *.name and *.age, these will match not only Person.name and Person.age, but also the name and age fields of any type. So if we had a type:

type Citizen {
  name: String!
  age: Int!
}

It would use the human name and age generation methods.

Overriding Dispatch

It may not be appropriate however, to use the same generation method for every instance of a field. For example, what if we have a type Dog which has the same attributes as a person:

type Dog {
  name: String!
  age: Int!
}

Dogs do not live to the same age as humans, and the set of dog names does not ovelap overly much with human names. We could define an alternate set of generators using createDispatch() called dogGen

let dogGen = createDispatch({
  "methods": {
    "dog.name": () faker.helpers.arrayElement(['Fido', 'Rover', 'Rex']),
    "dog.age": () => faker.datatype.number({ max: 18, min: 1 }),
  },
  "patterns": {
    "Dog.name": "dog.name",
    "Dog.age": "dog.age",
  },
});

Now, we can create our graphgen with both generation methods:

createGraphGen({
  source,
  sourcename,
  generate: [dogGen, nameAndAgeGen],
});

Because we put dogGen first in line, its patterns will take priority.

💡GraphGen will use the first matching pattern it finds, so put your most specific generation methods first, and add your most generic *. patterns as the last generation middleware. That way they will act as a catch all rather than blocking more specific pattern matches.

Dispatch Context

If you need helpers to perform the generation, you can pass the context option to createDispatch(). This is a function which takes the generation context and returns a value. This value will be passed to all of your generation methods. To see why you might need this, we can take a sneak peek at the section on Faker integration. In it, we need to create an instance of Faker with a specific random seed. That way, the values that it generates will happen in a predictable fashion.

In this example, instead of using the default static faker methods, we create a faker instance that tracks the graphgen random seed, and use that inside our generation methods:

let dogGen = createDispatch({
  "methods": {
    "dog.name": ({ faker }) faker.helpers.arrayElement(['Fido', 'Rover', 'Rex']),
    "dog.age": ({ faker }) => faker.datatype.number({ max: 18, min: 1 }),
  },
  "patterns": {
    "Dog.name": "dog.name",
    "Dog.age": "dog.age",
  },
  context: (info) => ({ ...info, faker: createFaker(info.seed) }),
});