-
Notifications
You must be signed in to change notification settings - Fork 2
Overview of the Approach
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:
- an
id
field in every object type that enables the GraphQL queries to access the system-generated identifier of each object, - a query type that specifies the starting points of queries sent to the GraphQL API,
- additional fields in the object types that enable the GraphQL queries to traverse relationships between the objects in the reverse direction,
- additional fields in the object types that enable the GraphQL queries to access the data associated with these relationships, and
- a mutation type and corresponding input types that specify how data can be inserted and modified via the GraphQL API.
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!
.
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
}
}
}
}
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!]!
}
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
}
}
}
}
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.
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
}
}
}
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.
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.
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
}
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
}
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.