Introduction
At the end of this article, the system will provide the following
- A GraphQL endpoint
- The Introspection API
- A browsable schema, as discussed here in the first part (GraphQL + OWL: An “Ontologized” GraphQL Interface). Specifically, we should find the corresponding types, fields, queries, and mutations defined within each namespace.
Types and Input Types
GraphQL makes a clear distinction between the read and the write interface. Specifically,
- the read interface consists of retrieval fields, directly or indirectly set under the top-level type Query, whose signature has a return type and an optional list of input arguments
- the write interface is quite similar (i.e., mutation fields with optional input arguments and a return type), but everything must be defined under a top-level type called Mutation.
See the following example:
type Query {
// retrieval fields goes here (even nested under types)
cartDetails(owner: String!): Cart
}
type Mutation {
// mutation fields go here, here are some examples
addCart(name:String!, capacity: Int): Cart
renameCart(id:Int!, name:String!): Cart
removeCart(id:Int!): Cart
}
The Query and Mutation sections within the schema above are quite similar; actually, the difference is in the behavior:
- queries (i.e., everything within the Query type) are used for data retrieval
- mutations are used to manipulate data. In addition, they can have a return type, so there’s also a retrieval part.
In our schema definition, we want to maintain that similarity between the two sections. GraphOWL should allow the requestor to use the following semantic (which, as you can see, is quite close to the query interaction seen in the previous articles):
mutation {
dcterms {
identifierScheme(uri: "https://svde.org/938473894<7008898") {
// properties of IdentifierScheme instance we want to get back
}
}
}
Manipulating a type requires defining a mutation set consisting of inserting, updating, and deleting capabilities. Instead of creating three different mutation fields, we opted for the following semantics:
- the mutation field is just one (e.g., identifierScheme, like in the example above)
- there could be n optional arguments: uri, and properties, one for each namespace. At least one of them should be present because if, at mutation time:
- there’s only the uri field, that means the user wants to delete the instance associated with that identifier.
- there are only the namespace properties, that means the user wants to create a new instance whose state is represented by the input properties
- there are both (uri and namespace properties), that means the user wants to update the instance associated with the given uri using the input properties
The schema would look like this:
// The mutation set associated to a given namespace.
// Actually, it doesn't represent any mutation: it acts only as a namespace
// container for holding the mutations that belongs to a given namespace.
type DctermsMutationSet {
// Create, update or delete a given instance of DctermsIdentifierScheme,
// a set of resource identifier encoding schemes and/or formats.
identifierScheme (
// The instance URI
uri:String,
// The "dc" namespace properties
dc:DcIdentifierSchemePropertiesInput,
// The "dcterms" namespace properties
dcterms:DctermsIdentifierSchemePropertiesInput): DctermsIdentifierScheme
sourceScheme (
uri:String,
dc:DcSourceSchemePropertiesInput,
dcterms:DctermsSourceSchemePropertiesInput): DctermsSourceScheme
// ... other mutation fields (one for each type)
}
type Mutation {
dcterms: DctermsMutationSet
dc: DcMutationSet
// ... other namespaces
}
Here are three mutation examples that could be used at runtime for manipulating the IdentifierScheme type:
// Delete an IdentifierScheme instance.
mutation {
dcterms {
identifierScheme(uri: "https://graphowl.org/9384738") {
// properties of the deleted instance we want to get back in response.
...
}
}
}
// Create a new IdentifierScheme instance.
mutation (
$dcProperties: DcIdentifierSchemePropertiesInput,
$dctermsProperties: DctermsIdentifierSchemePropertiesInput) {
dcterms {
identifierScheme(dc: $dcProperties, dcterms: $dctermsProperties) {
// the system assigned URI for this new instance.
uri
// other properties of the new instance.
...
}
}
}
// Update an existing IdentifierScheme instance.
mutation (
$uri: String,
$dcProperties: DcIdentifierSchemePropertiesInput,
$dctermsProperties: DctermsIdentifierSchemePropertiesInput) {
dcterms {
identifierScheme(uri: $uri, dc: $dcProperties, dcterms: $dctermsProperties) {
// properties of the deleted instance we want to get back in response.
...
}
}
}
The idea should be intuitive: apart from the deletion, which accepts only the instance identifier, when a new or an existing instance is managed, the requestor should specify the namespace properties that represent the (persistent) state of such an instance.
As a general consideration, the system “moves” the readability more on the query side. That is, the schema starts to be a bit complicated (many types, input types with similar names, fields, namespace properties, and so on). At the same time, the query looks more immediate and understandable.
Although that means the Introspection API would offer a complex view of the system (the complexity grows linearly depending on the number of input ontologies we set at startup), at query/mutation time, the “operative” part of a system, things are easier.
Next Steps
There are still many things missing. Here are some points
- Semantic relationships between classes in the ontology (e.g. inverseOf, symmetric)
- Fields cardinality in mutations
- Constraints
- Persistence and retrieval logic implementation
- Persistence storage
They all contain cool challenges we will dive into in the next chapters.
We would love to hear questions, doubts, and feedback about this blog post!
Feel free to contact us or leave a message in the comment box below.