Skip to content

Overview of the Approach

Olaf Hartig edited this page Apr 18, 2020 · 7 revisions

The idea of the approach to generate the API schema from the DB schema is to copy the DB schema into a new file and, then, extend the schema in this new file with all the additional things needed for the API schema. These additional things needed are:

  1. an id field in every object type that enables the GraphQL queries to access the system-generated identifier of each object,
  2. a query type that specifies the starting points of queries sent to the GraphQL API,
  3. additional fields in the object types that enable the GraphQL queries to traverse relationships between the objects in the reverse direction,
  4. additional fields in the object types that enable the GraphQL queries to access the data associated with these relationships, and
  5. a mutation type and corresponding input types that specify how data can be inserted and modified via the GraphQL API.

1. ID Fields

When inserting a data object into the database, the database management system generates an identifier for it. While these identifiers do not need to be part of the DB schema, they should be contained in the schema for the GraphQL API so that they can be requested in GraphQL queries (and, then, used later in subsequent queries). Therefore, when extending the DB schema into the schema for the GraphQL API, each object type is augmented with a field named id whose value type is ID!.

2. Query Type

Every GraphQL schema for a GraphQL API must have one special type called the query type. The schema for the DB does not need such a query type and, in fact, it should not contain one. The purpose of the query type is to specify the possible starting points of any kind of query that can be sent to the API. For instance, consider the following snippet of a GraphQL API schema which defines the query type of the corresponding API.

type Query {
    blog(id: ID!): Blog
}

Based on this query type, it is (only) possible to write queries (API requests) that start from a Blog object specified by a given ID. For instance, it is possible to write the following query.

query {
    Blog(id:371) {
        title
        text
        author {
            id
            name
        }
    }
}

However, with a query type like the one above, it would not be possible to query directly for, say, a Blogger specified by its ID.

Now, when extending the DB schema into the API schema, the plan is to generate a query type that contains two fields (i.e., starting points for queries) for every object type in the DB schema: one of these fields can be used to query for one object of the type based on the ID of that object, and the second field can be used to access a paginated list of all objects of the corresponding type. The list is paginated, which means that it can be accessed in chunks.

For example, for the Blogger type that we have in our example DB schema, the generated query type would contain the following two fields.

    blogger(id: ID!): Blogger
    listOfBloggers(first:Int after:ID): _ListOfBloggers!

The additional type called _ListOfBloggers that is used here will be defined as follows.

type _ListOfBloggers {
    totalCount: Int
    isEndOfWholeList: Boolean
    content: [Blogger]
}

Then, it will be possible to write queries such as the following.

query {
    listOfBloggers(first:10 after:371) {
        totalCount
        isEndOfWholeList
        content {
            name
            blogs {
                title
                text
            }
        }
    }
}

3. Additional Fields For Traversal

In the DB schema, each type of relationships (edges) between objects of particular types is defined only in one of the two related object types. For instance, consider the two object types Blog and Blogger whose definition in the DB schema looks as follows.

type Blog {
    title: String!
    text: String!
}

type Blogger {
    name: String!
    blogs: [Blog]
}

Notice that the relationship (i.e., the possible edges) between Blogger objects and Blog objects are defined only in the definition of the type Blogger (see the field named blogs) but not in the type Blog. Specifying every edge type only once is sufficient for the purpose of defining the schema of a (graph) database. However, it is not sufficient for supporting bidirectional traversal of these edges in GraphQL queries. Hence, the schema for the API needs to mention possible edges twice; that is, in both of the corresponding object types. For the aforementioned example of the relationships between Blogger objects and Blog objects, the API schema, thus, needs to contain an additional field in the type Blog such that this field can be used to query from a Blog object to the Blogger objects that point to it via their blogs fields. Hence, when extending the aforementioned part of DB schema into the schema for the GraphQL API, the definition of the Blog type will be extended as follows.

type Blog {
    title: String!
    text: String!
    _blogsFromBlogger: [Blogger]
}

Observe that the value type of the added field named _blogsFromBloggers is a list of Blogger objects. This is because, according to the DB schema, multiple different Blogger objects may point to the same Blog object; i.e., the relationship between Blogger objects and Blog objects is a many-to-many relationship (N:1). Therefore, from a Blog object, we may come to multiple Blogger objects.

Perhaps this was not the intention and, instead, the relationship between Blogger objects and Blog objects was meant to be a one-to-many relationship. This could have been captured by adding the @uniqueForTarget directive to the field named blogs in the DB schema (as described in the text before Example 7 of http://blog.liu.se/olafhartig/documents/graphql-schemas-for-property-graphs/). Assuming that there would be such a @uniqueForTarget directive, then the new field named _blogsFromBlogger that is added when extending the DB schema into the API schema would be defined differently:

type Blog {
    title: String!
    text: String!
    _blogsFromBlogger: Blogger
}

This example demonstrates that the exact definition of the fields that are added when extending the DB schema into the API schema depends on the constraints that are captured by directives in the DB schema. To elaborate a bit further on this point, let us assume that the aforementioned field named blogs in the DB schema would additionally be annotated with the @requiredForTarget directive (in addition to the @uniqueForTarget directive). In this case, the extension of the type Blog for the API schema would look as follows (notice the additional exclamation marks at the end of the value type for the new Blogger field).

type Blog {
    title: String!
    text: String!
    _blogsFromBlogger: [Blogger!]!
}

4. Additional Fields and Types For Edges

Edges in a Property Graph database may have properties (key-value pairs) associated with them. When defining the DB schema, these properties can be defined as field arguments as demonstrated in the following snippet of a DB schema.

type Blogger {
    name: String!
    blogs(certainty:Int! comment:String): [Blog]  @uniqueForTarget @requiredForTarget
}

type Blog {
    title: String!
    text: String!
}

By this definition, every edge from a Blogger object to a Blog object has a certainty property and, optionally, it may have a comment property.

Field arguments such as certainty and comment would have a different meaning when used in a schema for the GraphQL API and, thus, they have to be removed from the field definitions when extending the DB schema into the API schema. Hence, after removing the field arguments (and adding the aforementioned id fields and the fields for traversing edges in the opposite direction), the API schema for the aforementioned DB schema would look as follows.

type Blogger {
    id: ID!
    name: String!
    blogs: [Blog]  @uniqueForTarget @requiredForTarget
}

type Blog {
    id: ID!
    title: String!
    text: String!
    _blogsFromBlogger: Blogger!
}

Although we have to remove the field arguments from the fields that define edge types in the DB schema, we may want to enable GraphQL queries to access the values of the edge properties that these edges of these types have. For instance, we may want to query for the certainty of edges between bloggers and blogs. To this end, the edges have to be represented as objects in the GraphQL API. Hence, it is necessary to generate an object type for each type of edges and integrate these object types into the schema for the API. For instance, for the edges between bloggers and blogs, an object type called _BlogsEdgeFromBlogger will be generated and access to objects of this new type will be integrated into the schema by adding a new field to the Blogger type and to the Blog type, respectively.

type Blogger {
    id: ID!
    name: String!
    blogs: [Blog]  @uniqueForTarget @requiredForTarget
    _outgoingBlogsEdges: [_BlogsEdgeFromBlogger]
}

type Blog {
    id: ID!
    title: String!
    text: String!
    _blogsFromBlogger: Blogger!
    _incomingBlogsEdgeFromBlogger: _BlogsEdgeFromBlogger!
}

type _BlogsEdgeFromBlogger {
    id: ID!
    source: Blogger!
    target: Blog!
    certainty:Int!
    comment:String
}

Given this extension, it is now possible to write GraphQL queries that access properties of the edges along which they are traversing. The following query demonstrates this option.

query {
    Blogger(ID:3991) {
        name
        _outgoingBlogsEdges {
            certainty
            target {
                title
                text
            }
        }
    }
}

5. Mutation Type and Corresponding Input Types

In addition to the aforementioned query type, another special type that a GraphQL API schema may contain is the mutation type. The fields of this type specify how data can be inserted and modified via the GraphQL API. For instance, the following snippet of a GraphQL API schema defines a mutation type.

type Mutation {
    setTitleOfBlog(ID:ID! Title:String!): Blog
}

Given this mutation type, it is possible to modify the title of a Blog object specified by a given ID; the result of this operation is defined to be an Blog object (we may assume that this is the modified Blog, which may then be retrieved as the response for the operation).

Now, when extending the DB schema into the API schema, the plan is to generate a mutation type that contains three operations for every object type in the DB schema and another three operations for every edge type. The three operations for an object type XYZ are called createXYZ, updateXYZ, and deleteXYZ; as their names suggest, these operations can be used to create, to update, and to delete an object of the corresponding type, respectively. In the following, we discuss the mutation operations in more detail.

5.1 Creating an Object

Consider the aforementioned object type Blogger of our DB schema. The create operation for Blogger objects will be defined as follows.

    createBlogger(data: _InputToCreateBlogger!): Blogger

The value of the argument data is a complex input object that provides the data for the Blogger object that is to be created. This input object must be of the type _InputToCreateBlogger. This input type, which will be generated from the object type Blogger of the DB schema, will be defined as follows.

input _InputToCreateBlogger {
    name: String!
    blogs: [_InputToConnectBlogsOfBlogger]
}

input _InputToConnectBlogsOfBlogger {
    connect: ID
    create: _InputToCreateBlog
}

Notice that all fields that are mandatory in Blogger are also mandatory in _InputToCreateBlogger (and optional fields remain optional). Moreover, fields whose value type in Blog is a scalar type (or a list thereof) have the same value type in _InputToCreateBlogger. In contrast, fields that represent outgoing edges in Blogger have a new input type that can be used to create the corresponding outgoing edge(s). This can be done in one of two ways: either by identifying the target node of the edge via the connect field or by creating a new target node via the create field.

Hence, the createBlogger operation then may be used as follows:

mutation {
    createBlogger(
        data: {
            name: "Robert"
        }
    ) {
        id
        name
    }
}

This example creates a new Blogger object with a name field but no edges to Blog objects, and then retrieves the ID and the name of this newly created Blogger object. Alternatively, we may also create a Blogger object with an edge to an existing Blog object:

mutation {
    createBlogger(
        data: {
            name: "Robert"
            blogs: [
                { connect: "361" }   # 361 is the ID of the Blog object that the new edge points to
            ]
        }
    ) {
        id
        name
    }
}

Note that this example creates not only the new Blogger object but also a new edge. Yet another alternative is to also create a new Blog object within the operation:

mutation {
    createBlogger(
        data: {
            name: "Robert"
            blogs: [
                {                  # creates a new edge as well
                    create: {      # as a new Blog object;
                        title: "new blog"   # data of the new
                        text: "..."         # Blog object
                    }
                }
            ]
        }
    ) {
        id
        blogs {
            id   # to also retrieve the ID of the newly created Blog object
        }
    }
}

5.2 Updating an Object

The update operations for Blog objects will be defined as follows.

    updateBlogger(id: ID! data: _InputToUpdateBlogger!): Blogger

The Blogger object to be updated can be identified by the argument id. The value of the argument data in this case is another input type that provides values for the fields to be modified. This input type is defined as follows.

input _InputToUpdateBlogger {
    name: String
    blogs: [_InputToConnectBlogsOfBlogger]
}

Notice that all fields in this input type are optional to allow users to freely choose the fields of the Blogger object that have to be updated. For instance, to update only the Name of the Blogger object with the ID 371 we may write the following.

mutation {
    updateBlogger(
           id:371
           data: {
              name:"John Doe"
           }
    ) {
        id
        name
      }

Updates like this override the previous value of the updated fields. In the case of outgoing edges this means that new edges replace all previously existing edges (without removing the target nodes of replaced edges). If you want to add edges instead, use the connect operations described below.

5.3 Deleting an Object

The delete operations for Blogger objects will be defined as follows.

    deleteBlogger(id: ID!): Blogger

The argument ID can be used to identify the Blogger object to be deleted.

Notice that deleting an object implicitly deletes all incoming and all outgoing edges of that object.

5.4 Creating an Edge

Consider the types of edges that represent the aforementioned relationship between bloggers and blogs. In the DB schema, this type of edges is defined implicitly by the field definition Blogs in object type Blogger. The create operation for these edges will be defined as follows.

    createBlogsEdgeFromBlogger(data: _InputToCreateBlogsEdgeFromBlogger!): _BlogsEdgeFromBlogger

The new input type for this operation, _InputToCreateBlogsEdgeFromBlogger, will be generated as follows.

input _InputToCreateBlogsEdgeFromBlogger {
    sourceID: ID!   # assumed to be the ID of a Blogger object
    targetID: ID!   # assumed to be the ID of a Blog object
    annotations: _InputToAnnotateBlogsEdgeFromBlogger!
}

input _InputToAnnotateBlogsEdgeFromBlogger {
    certainty: Int!
    comment: String
}

5.5 Updating an Edge

Update operations for edges will be generated only for the types of edges that have edge properties. The edges that represent the aforementioned relationship between bloggers and blogs are an example of such edges. The update operation that will be generated for these edges is:

    updateBlogsEdgeFromBlogger(id: ID! data: _InputToUpdateBlogsEdgeFromBlogger!): _BlogsEdgeFromBlogger

The argument id can be used to identify the _BlogsEdgeFromBlogger object that represents the edge to be updated. The new input type for this operation, _InputToUpdateBlogsEdgeFromBlogger, will be generated as follows.

input _InputToUpdateBlogsEdgeFromBlogger {
    certainty: Int
    comment: String
}

5.6 Deleting an Edge

The delete operations for the edges between bloggers and blogs will be defined as follows.

    deleteBlogsEdgeFromBlogger(id: ID!): _BlogsEdgeFromBlogger

The argument id can be used to identify the _BlogsEdgeFromBlogger object that represents the edge to be deleted.