Skip to content

Why We Developed a GraphQL Compiler

Introduction

We were quite excited to launch our first beta of basebox last week. If you've had a chance to read our whitepaper, you would have seen us talk about a GraphQL compiler. We really thought it was time to give people a taste of what we've been working on in terms of the compiler.

GraphQL Compiler You Say? Whatever Do You Mean?

Firstly, when we talk about our GraphQL compiler, we're specifically talking about a GraphQL-to-SQL compiler. The basebox compiler, written in Rust, allows you to, firstly, generate a database schema using the GraphQL Type System. And once we've created this database, we expose the database via the defined GraphQL API, with basebox' database proxy server happily converting GraphQL operations to the necessary SQL statements using the output of the compiler.

What we're really trying to achieve here is to create an entire back-end using just GraphQL, one that is not just easy to create and use, but one that it also fast, secure and compliant (we will talk more about regulatory compliance in a future article). This hopefully negates the need for advanced database creation and administration skills (some basic understanding of SQL terminology and how it works is needed).

GraphQL Data Definition

While GraphQL is primarily a query language, it does provide a Type System, that, while more limited than SQL Data Definition Language (DDL) in the structures and data types you can specify, has enough for us to build on. GraphQL object map quite nicely to SQL tables, and references to other objects can be quite nicely be translated to foreign key joins.

A simple example of a GraphQL object and the corresponding generated SQL table can be taken from the Todo App example we've released with the beta:

The GraphQL here

graphql
type Task {
  id: ID!
  title: String!
  description: String,
  completed: Boolean!
  user: User!
  list: List!
}
type Task {
  id: ID!
  title: String!
  description: String,
  completed: Boolean!
  user: User!
  list: List!
}

becomes

sql
CREATE TABLE "Task" (
  "id" UUID DEFAULT gen_random_uuid() NOT NULL,
  "title" VARCHAR NOT NULL,
  "description" VARCHAR,
  "completed" BOOLEAN NOT NULL,
  "user_username" VARCHAR NOT NULL,
  "list_id" UUID NOT NULL 
);

ALTER TABLE "Task" ADD PRIMARY KEY ("id");

ALTER TABLE "Task" ADD CONSTRAINT fk_task_2 FOREIGN KEY ("user_username") REFERENCES "User" ("username");

ALTER TABLE "Task" ADD CONSTRAINT fk_task_3 FOREIGN KEY ("list_id") REFERENCES "List" ("id");
CREATE TABLE "Task" (
  "id" UUID DEFAULT gen_random_uuid() NOT NULL,
  "title" VARCHAR NOT NULL,
  "description" VARCHAR,
  "completed" BOOLEAN NOT NULL,
  "user_username" VARCHAR NOT NULL,
  "list_id" UUID NOT NULL 
);

ALTER TABLE "Task" ADD PRIMARY KEY ("id");

ALTER TABLE "Task" ADD CONSTRAINT fk_task_2 FOREIGN KEY ("user_username") REFERENCES "User" ("username");

ALTER TABLE "Task" ADD CONSTRAINT fk_task_3 FOREIGN KEY ("list_id") REFERENCES "List" ("id");

in SQL DDL.

The SQL generated is quite similar to the initial GraphQL, we taken the liberty of adding a primary key and foreign keys based on the information provided. If the compiler finds one GraphQL ID field in an object, it presumes this to be the primary key of the corresponding table. We convert the GraphQL scalar types to standard SQL data types (refer to the Guide for the type mappings between GraphQL and SQL).

Also note that the GraphQL 'Task' object becomes is double-quoted in SQL. We have chosen PostgreSQL as our first database to support. GraphQL by default is case-sensitive while Postgres SQL by default is not - unless you double quote the names.

GraphQL Directives

Regarding the primary keys, as mentioned, we presume that one ID field in an object is the primary key. We actually do need primary keys in our tables, particularly if we'll be creating joins between tables. If we don't have an ID in an object (or we have multiple ID fields), we have created a @bb_primaryKey directive as an annotation to the object. For example, if we had a task name instead of an ID as an identifying field:

graphql
type Task {
  taskName: String! @bb_primaryKey
  title: String!
  description: String,
  completed: Boolean!
  user: User!
  list: List!
}
type Task {
  taskName: String! @bb_primaryKey
  title: String!
  description: String,
  completed: Boolean!
  user: User!
  list: List!
}

After looking at various methods of adding annotations and specifiers to our GraphQL (for example, special comments or defining annotations in a separate file) we decided that the best option is to use GraphQL's in-build directives for adding anything we might require additionally. This helps us as those familiar with GraphQL would be able to use something more recognizable in GraphQL directives, and we would not need to add an additional parsing phase (our GraphQL parser already covers directives).

Defining Resolvers in Your GraphQL Schema

Now, we've spoken about data definition, what about GraphQL queries and mutations?
We are also providing a mechanism for resolving these within the schema definition. Yes, we created another directive, called @bb_resolver, that allows you to define how a query or mutation is resolved.

Here's a quick example, again from the Todo App example we've provided:

graphql
  getUser(
    username: String!
  ): User @bb_resolver(_type: SELECT, _object: User, _filter: { username: { _eq: "$username" } })
  getUser(
    username: String!
  ): User @bb_resolver(_type: SELECT, _object: User, _filter: { username: { _eq: "$username" } })

The @bb_resolver provides a quick and easy language to resolve operations. In this example, the resolver tells the compiler that getUser is resolved by selecting from the User object by searching on the username field (using the username argument as a value in the query). While our users would now have to learn our new resolver language, we are keeping quite simple and GraphQL-esque as possible. And once we start supporting multiple SQL (and NoSQL) databases, this language will remain the same, leaving the compiler to do the database specific conversion.

Benefits of Data Definition in GraphQL

So we're getting a little into the data definition side of basebox, but some of you might be wondering why go from GraphQL-to-SQL, why not go the other way for defining the database and we can still generate queries and mutations from GraphQL-to-SQL?

Well, there are some benefits from doing it this way:

  • As mentioned, we are negating the need for a database administrator or more advanced SQL skills. So we're keeping as much as possible within the GraphQL schema.
  • Defining the data model and resolvers in GraphQL schema creates a single source of truth. No need to try and sync your database and API any longer, it's all defined within one file and the compiler will do the working of letting you know when something does not add up.
  • You also have more control over what you expose via your API. We have seen examples where tools generating GraphQL from existing databases expose a lot more functionality than is required, forcing you to then figure out what to stop. With our approach, we don't expose any functionality unless you explicitly define it so.

Business Logic Layer

A note on our business logic layer, this will be released in our next version (sign up to our mailing list to get news on then this is).

One of the primary questions that the business logic layer addresses is - 'What if you cannot resolve my query or mutation using the @bb_resolver directive?'. The business logic layer allows developers to re-direct some operations to their own functionality. We currently support code written in Python, with more to follow. So you can write a Python module that resolves some of the operations while others can be handled by the defined resolvers.

In Closing

We hope you've found this quite informative, please do get in contact it you have any questions or comments.

Also, if you're interested in more of the functionality we're supporting and what it looks like, please refer to the Guide.