Skip to main content

How Grats Works

For users who want to have a better mental model of how Grats works, or just for the curious, here's a high level overview of how Grats works under the hood. For a description of Grat's values and aspirations, see Design Principles.

At build time

When statically analyzing your code to infer GraphQL schema, grats first looks for your TypeScript config. From there it's able to ask the TypeScript compiler for all all the TypeScript files in your project, as well as the right configuration options to pass to the TypeScript compiler.

Grats then iterates over these files and checks via Regex to see if they contain any @gql* tags. If they do, Grats will parse the file and iterate over every @gql* tag. For each tag it finds that maps to a top-level GraphQL construct (type, interface, etc.), it asks the TypeScript compiler for the AST node to which that comment is attached. Now that Grats has an AST node and an expectation of what GraphQL construct it's trying to infer, it tries all the different inference strategies it has available to it. If it can't infer a GraphQL construct, it will report a diagnostic error to the user. That sounds a lot fancier than what the code looks like: a series of switch and if statements.

If it's able to infer a GraphQL construct, it will build up a GraphQL AST node representing the schema definition. In the case of @gqlType or similar, this may mean inspecting child elements of the AST node for child constructs like @gqlField and recursively inspecting those nodes. These GraphQL AST nodes are the exact shape the graphql-js builds when it parses a GraphQL SDL file, and thus are API-compatible with graphql-js utilities. However, we play one clever trick. When we construct the location information for each GraphQL AST node, which would usually contain the line and column number of the Schema Definition Language (SDL) text from which it was parsed, we instead use the location information from the TypeScript AST node, including its file path, line number, and column number.

By building up these AST nodes, Grats is able to use the same code that graphql-js uses to validate GraphQL schema to validate Grats' inferred schema. And because we have populated the location information with the TypeScript AST node, the diagnostics we get from graphql-js will actually "point" the the TypeScript source code that Grats uses as the source of truth for that AST node.

One final trick we employ is using TypeScript's representation of a diagnostic. This allows us to use TypeScript's error printer for free.

In a few cases, like when a field name does not match its property/method name, Grats must communicate this fact to the runtime (see below). To achieve this while still keeping the build and runtime steps separate, Grats annotates some constructs with custom server directives. On startup, these are used by the slim runtime portion of Grats to configure the GraphQL resolvers.

While this approach adds noise to the generated SDL, it allows Grats' build step to generate a single output file in a well known format, SDL.

At runtime

Grats aims to have a very slim runtime component. Currently, it takes the SDL file generated by the build step, pass it to graphql-js to parse and generate a set of default resolvers (GraphQLSchema). Finally, it uses the directives it added to the SDL to configure/wrap the default resolvers in order to ensure they match the user's code. This includes:

  • Ensuring the correct field/property is read if the field name differs from the concrete property name
  • Importing and calling the correct free function if the field was defined using the functional style of @gqlField where the resolver is defined as a named export in some module or other.

In the future we might also support positional arguments which would also require some runtime configuration.