Kickstart your full stack GraphQL application with GRelDAL starter - II

In the previous post of this series, we tried out the GRelDAL starter and bootstrapped a simple application.

In this (and the next) post, we dig deeper into what is happening in the scaffolded application.

This post assumes a basic familiarity with GraphQL. If you entirely new to GraphQL, you may want to first go through the excellent introductory tutorial at howtographql.com.

Building the backend

In this post, we will start out by exploring the backend of our scaffold.

Usually the first thing we would want to do when creating an API is identifying our data sources.

While GraphQL is agnostic about where and how we store our data, GRelDAL is designed to work with relational databases. In this case, we will use a postgres database. However, GRelDAL also works pretty well with MySQL, SQLite etc.

Managing the data sources

GRelDAL is quite unopinionated about how we manage our databases. If you are a seasoned database administrator and have tools that you are productive in, you are more than welcome to continue using them. One of the key goals in building GRelDAL is to provide database experts fine grained control over the lifecycle of data access and retrieval.

For this example, we will use some simple CLI utilities provided by Knex to manage our database. This is a natural choice here because:

  1. GRelDAL uses Knex for data access, so we already have that as a dependency.
  2. Knex CLI is fairly simple and easy to get started with.

We will touch more upon Knex CLI when extending our app, but for now we just need to know that the files in our src/seeds and src/migrations directory are intended to work well with knex CLI.

When we run the migrate:latest script, knex will find and execute any of the migrations in src/migrations that hasn't been run before. (The associated bookkeeping is done in the migrations table in the database).

When we run the seed:run script, knex executes all the seed files in src/seeds every time.

Wizard data source

Our application shows a list of wizards and their accomplishments.

Screenshot

For now, let us focus on what we need to show the list of wizards.

To fetch wizards we need a Wizard data source, which connects to our wizards table which we are creating in create_wizards migration and populating through populate_wizards seed.

In backend/src/data-sources/wizard.ts we will find our Wizard data source implemented as follows:

export const wizards = mapDataSource({
  name: "Wizard",
  fields: mapFields({
    id: {
      type: types.integer,
      to: GraphQLID,
      isPrimary: true
    },
    name: {
      type: types.string
    },
    age: {
      type: types.integer
    }
  })
});

The naming convention of plural underscored table name (wizards) and singular DataSource name (Wizard) is assumed by default. But it is not mandatory. We could have defined our data source like this to be more explicit:

export const wizards = mapDataSource({
  name: {
      mapped: 'Wizard',
      stored: 'wizards'
  },
  // ....
})

Or, used entirely different mapped/stored names which can be useful when dealing with legacy databases.

In this case our fields map one-to-one to our table columns, but as we will see later, they don't have to. We can skip columns we don't want to expose, and we can also define computed fields which derive their value from multiple fields.

The guide on mapping data sources in GRelDAL docs elaborates on this in more detail.

Operations against data sources

Data sources are not very useful in themselves, unless we also define operations on a data source. Operations are what enable us to query the data sources or modify the data stored.

We can define our own operations, and in the later parts of the series we will, but for most common scenarios we can simply use (and tweak) the operation presets available in the library.

import { operationPresets } from 'greldal';

const wizardQueryOperations = operationPresets.query.defaults(wizards)

We can map a set of operations to a GraphQL schema:

const schema = mapSchema(wizardQueryOperations);

mapSchema maps the operations to a graphql-js schema. Now we can make GraphQL queries against this schema:

import { graphql } from "graphql";

graphql(
    schema,
    `
        query {
            findOneWizard(where: { id: 1 }) {
                id
                name
            }
        }
    `,
); 

// Returns a promise which resolves to something like: 

{
  "data": {
    "findOneWizard": {
      "id": "1",
      "name": "Nokolai DrekSpoonWorth"
    }
  }
}

A closer look at the operation presets:

At this point it may not be obvious where this findOneWizard query is coming from.

When we used operationPresets.query.defaults(wizards) above, we were essentially using mapping two build-in operations:

  1. findOne operation (which exposes a GraphQL query findOneWizard for our Wizard data source)
  2. findMany operation (which exposes a GraphQL query findManyWizard)

We could have been been more explicit and used them both individually:

const schema = mapSchema([
    operationPresets.query.findOne(wizard),
    operationPresets.query.findMany(wizard)
]);

We can also combine multiple operations on different data sources in the same schema. From the schema.ts file in our scaffold:

export const schema = mapSchema([
    ...operationPresets.query.defaults(wizards),
    ...operationPresets.query.defaults(accomplishments),
    ...operationPresets.mutation.defaults(accomplishments)
]);

Just like query operations are mapped to GraphQL queries, the mutation operations are mapped to GraphQL mutations.

Inclusion of operationPresets.mutation.defaults(accomplishments) above exposes following GraphQL mutations:

  • insertOneAccomplishment(entity: AccomplishmentInput): ShallowAccomplishment
  • insertManyAccomplishments(entities: [AccomplishmentInput]): [ShallowAccomplishment]
  • updateOneAccomplishment(where: AccomplishmentInput!update: AccomplishmentInput!): ShallowAccomplishment
  • updateManyAccomplishments(where: AccomplishmentInput!update: AccomplishmentInput!): [ShallowAccomplishment]
  • deleteOneAccomplishment(where: AccomplishmentInput!): ShallowAccomplishment
  • deleteManyAccomplishments(where: AccomplishmentInput!): [ShallowAccomplishment]

Mapping of operations is elaborated in more detail in the official docs.

Exposing the schema through an HTTP API

Having a schema we can programmatically query is great, but in practice we would want to expose this schema through a web API. Thankfully there are already good solutions available to bridge graphql-js to popular node.js web frameworks.

In the starter scaffold, we use express-graphql in our index.ts to expose our schema through an express middleware:

import express from "express";
import graphqlHTTP from "express-graphql";

app.use('/graphql', graphqlHTTP({
  schema
}));

app.listen(4000);

This is really all we need to make the schema available at http://localhost:4000. Now we can use curl, any rest client (eg. postman) or the apollo dev tools to make graphql requests against this API.

apollo-devtools-gql-query.PNG

The starter application also comes with graphiql pre-configured so you can access http://localhost:4000/graphql in browser to explore the API through the in-browser IDE.

A closer look at the generated types

One of the great features of GraphQL is that we can interactively explore the API schema. If we use graphiql's schema explorer sidebar we will notice something interesting:

documentation-explorer.gif

We see that there are many GraphQL types that are available in our API eg. Wizard, WizardInput, ShallowWizard etc.

We didn't define each of them explicitly, we didn't write any GraphQL schemas - where are they coming from ?

You may have guessed that these types are auto-generated from our data source specification. GRelDAL automatically maps the data source fields to multiple GraphQL types:

  • Default output types eg. Wizard which expose both data source fields as well as associations
  • Shallow output types eg. ShallowWizard which expose only the data source fields
  • Shallow input types eg. ShallowWizardInput which can be used as arguments in mutations.

While in most cases our types will be auto-inferred, we can be more specific about the type of a field. For example, in our wizard data source above, we had these fields:

id: {
      type: types.integer,
      to: GraphQLID,
      isPrimary: true
},
name: {
      type: types.string
},

While name is automatically mapped to GraphQLString, we have explicitly instructed GRelDAL to use GraphQLID type for the id column.

All data sources should have one or a combination of multiple primary fields. In above data source, we have identified id as our primary field. We would usually want to use GraphQLID type for our primary fields.

Associations

Associations allow us to link together data sources.

An example in this case is, when we fetch the list of wizards, we want to fetch the list of accomplishments for each wizard as well.

First of all, we need another data source for Accomplishments. That implementation is nothing too different from our Wizard data source above.

Now we can define an association in our wizard data source to enable operations that start from the wizard data source and can cover other data sources as well.

export const wizards = mapDataSource({
  name: "Wizard",
  fields: mapFields({
    id: {
      type: types.integer,
      to: GraphQLID,
      isPrimary: true
    },
    name: {
      type: types.string
    },
    age: {
      type: types.integer
    }
  }),
  associations: mapAssociations({
    accomplishments: {
      exposed: true,
      target: () => accomplishments,
      singular: false,
      fetchThrough: [{join: 'leftOuterJoin'}],
      associatorColumns: {
        inSource: 'id',
        inRelated: 'wizard_id'
      }
    }
  })
});

Knex internally uses the excellent debug module. So we can inspect what Knex is doing under the hood by setting the DEBUG env variable when running our server.

DEBUG=knex* yarn run start

Now if we run a query like below:

query {
    findManyWizards(where: { }) {
        id
        name
        accomplishments {
          title
          id
        }
    }
}

We will see that knex is performing a left outer join as we configured in our association:

knex-join-query-1.PNG

This query can be slightly hard to read because of the aliasing, but adding some formatting makes things a lot clearer:

select 
    "GQL_DAL_wizards__27"."id" as "GQL_DAL_wizards__27__id", 
    "GQL_DAL_wizards__27"."name" as "GQL_DAL_wizards__27__name", 
    "GQL_DAL_accomplishments__28"."title" as "GQL_DAL_accomplishments__28__title", 
    "GQL_DAL_accomplishments__28"."id" as "GQL_DAL_accomplishments__28__id" 
from 
    "wizards" as "GQL_DAL_wizards__27" 
    left outer join "accomplishments" as "GQL_DAL_accomplishments__28" 
        on "GQL_DAL_wizards__27"."id" = "GQL_DAL_accomplishments__28"."wizard_id"

If we remove the aliases this will get simplified to something very close to what we might have written ourselves if we were to hand-roll an SQL query for this use case:

select 
    "wizards"."id",
    "wizards"."name", 
    "accomplishments"."title", 
    "accomplishments"."id"
from 
    "wizards"
    left outer join "accomplishments"
        on "wizards"."id" = "accomplishments"."wizard_id"

If you are familiar with SQL you might expect the result of the above query to be something like this:

tabular-result-set.PNG

However, sharp-eyed readers would note that in the response from our GraphQL query, we don't get back a tabular response like the above. Instead we get a hierarchical response that matches the shape of our query:

hierarchical-result-set.PNG

So how does the tabular result get transformed into the above GraphQL compliant response ? GRelDAL takes care of it automatically for you. It transforms and reshapes the data to match your API request and you don't have to deal with any of this data-wrangling yourself.


This concludes our high level overview of the backend scaffold in greldal-starter. In the next post of this series, we will explore the React based frontend that talks to the backend and presents an interactive web interface to application users.

While GRelDAL is still in beta, we have stopped making breaking changes to the API often, and almost all aspects are documented. We encourage you to try it out and are excited to see what you build with it.

Lorefnon

Gaurab Paul

Individualist. Atheist. Technophile. Fiction Aficionado. Believer in the Open Web.

Write your comment…

Be the first one to comment