Skip to content
Hugo Tiburtino edited this page Jan 2, 2023 · 3 revisions

This page explains the basic architecture the API is built upon. It explains the different types that are used in the GraphQl schemas as well as the rest of the software that is written in TypeScript. This article assumes you already know what Apollo Server is, have studied the most important articles of the Apollo Server documentation (as given in the introduction article of this wiki) as well as the introduction article to the API in this wiki.

GraphQL types

In order to understand our code you first need to understand the difference between GraphQL types and model types. Let's take the following two GraphQL types Article and ArticleRevision:

type Article implements AbstractUuid {
  id: Int!
  currentRevision: ArticleRevision
  ...
}

type ArticleRevision implements AbstractUuid {
  id: Int!
  title: String!
  content: String!
  ...
}

We will refer to those types which directly correspond to types in our GraphQL schema as GraphQL types. They live in ~/types and are automatically generated by the tool GraphQL code generator via the command yarn codegen. So each time you change the GraphQL schema you need to run yarn codegen as well so that ~/types gets updated. Those GraphQL types can be directly translated into TypeScript types:

interface Article extends AbstractUuid {
  id: number
  currentRevision: ArticleRevision | null
  ...
}

interface ArticleRevision implements AbstractUuid {
  id: number
  title: string
  content: string
  ...
}

Model types

In the Serlo software, GraphQL types are represented by so called model types or model objects. They hold all information necessary to dynamically get all properties of the GraphQL type they represent. They are located in ~/internals/graphql. As a parameter you give the name of the GraphQL type, union or interface. For the GraphQL type Article its model type is for example:

type Model<"Article"> = {
  id: number
  currentRevisionId: number | null
  ...
}

Sidenote: You see that instead of currentRevision being an object of type ArticleRevision it only holds the id of its currentRevision (or null in case there is no reviewed revisions yet). When currentRevision is requested by a GraphQL query we can use this id to retrieve the current revision dynamically. Thus we do not need to directly store the current revision inside the model. This has some advantages:

  1. Performance: When a model of an article is created we only need to retrieve the id of its current revision and therefore we do not need to query the whole revision object. This will end fewer database requests and thus a better performance.
  2. Caching: The model object of an article is smaller. When we cache it we have a smaller footprint in necessary space. Also the handling of updates is easier since we do not need to update the article model object when the revision model object changes.

Similarly Model<"ArticleRevision"> is the model type of ArticleRevision and Model<"User"> is the model type of User. The type function Model<"..."> also works for GraphQL unions / interfaces. In this case it will return the union of all model types whose GraphQL type are in the GraphQL union or implement the GraphQL interface. For example:

type Model<"AbstractUuid"> = Model<"Article"> | Model<"ArticleRevision"> | Model<"User"> ...

See https://graphql.org/learn/schema/#interfaces and https://graphql.org/learn/schema/#union-types for an introduction about GraphQL unions / interfaces and https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#union-types for an introduction about union types in TypeScript.

Resolver functions

Since model types are not the same as GraphQL types we need functions which can dynamically calculate the properties of a GraphQL type which are missing in the model. These functions are called resolver functions. In the above example we need a special function currentRevision() since we have decided that the current revision of an article shall be calculated "on the fly" when a GraphQL request is made. Those functions live inside the special object resolvers which holds all resolver functions of all GraphQL types:

const resolvers = {
  Article: {
    currentRevision(parent, args, context) {
      ...
    }
  }
}

The parent is the model object described in the previous section. For example the object { id: 123, currentRevisionId: 456, ... } might be passed as parent to currentRevision. args are arguments which might be passed to the GraphQL request for this particular property (see https://graphql.org/learn/queries/#arguments ). Since currentRevision has no arguments defined in our schema it will be an empty object {} at runtime. context is a dictionary with helper objects for calculating the property. For example context will have a dataSources object which which requests to the used services can be made (see the article data sources).

Since the property currentRevision is defined by

type Article {
  currentRevision: ArticleRevision
  ...
}

the return type of the resolver function currentRevision() need to be the model type of ArticleRevision or null (Note that there is no exclamation mark ! after ArticleRevision in the GraphQL schema and thus the property can be null). An implementation of currentRevision() is:

const resolvers = {
  Article: {
    async currentRevision(parent, _args, { dataSources }) {
      if (parent.currentRevisionId === null) return null
      return await dataSources.serlo.getUuid({ id: parent.currentRevisionId })
    }
  }
}

So in case currentRevsionId is not null we make a request to the database layer to resolve the uuid of the revision. We have programmed the database layer in a way that it already returns the model type for each uuid so that in this case the model type of an article revision is returned. If you want to know more, this wiki also contains an in-depth article about Resolvers and their type helpers.

Data Sources

As you have seen in the above example the function currentRevision() uses a special function getUuid() of the object dataSources.serlo to resolve an article revision. Those objects are called data sources. They abstract requests to services which we use in resolver function to retrieve data. Lets recall our software architecture:

For each of the used services we have a data source. They all live inside the directory ~/model. There is for example ~/model/serlo for requests to our database layer and ~/model/google-spreadsheet-api for accessing the Google Spreadsheet API. For more details about data sources see also this article

Conclusion

GraphQL types are the types of our GraphQL schema. Thus they refer to the type system the clients use to access our data and we encode with them the way "the outside world sees Serlo". Internally we use model types to represent GraphQL types. Any missing properties in the model types are calculated dynamically via resolver functions. Therefore we have

GraphQL types = model types + resolver functions

The above "equations" means that all properties of a GraphQL type are either already represented in the model type or they are calculated dynamically via resolver functions.

In order to retrieve data from other services we have data sources. A data source abstracts requests to the services and is given by the context variable to the resolver functions.

The next article in this series is this one about how a GraphQl request is resolved.

Clone this wiki locally