MUTATION GRAPH QL

GraphQL + OWL: Mutations

Introduction

In the first part (GraphQL + OWL: An “Ontologized” GraphQL Interface), we introduced and discussed a design for a GraphQL schema that provides a query interface (e.g., types, fields) compliant with an arbitrary set of ontologies.  
 
In the second part (GraphQL + OWL: Namespaces), we started coding the software module that realizes the above. Specifically, we discussed and illustrated how to deal with namespaces.
 
In the third part (GraphQL + Types and Fields), we defined types and properties, which are the building blocks of ontology and GraphQL schema
 
Now it’s time to design the “write” side of the system: in GraphQL, Mutations. Mutations, as the name suggests, allow the manipulation of data.  

At the end of this article, the system will provide the following

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.

Share this post

Leave a Reply