Leveraging GraphQL Schemas and Interfaces

By Todd Kaplinger

In a recent article Exploring Tokens with Stellar I mentioned in passing how we enhanced some of the Stellar data objects with additional context when we created our GraphQL schemas. This approach provided a great deal of flexibility in defining our data models. In this article, I will demonstrate an approach to leveraging GraphQL Interfaces to simplify the client programming model experience for these decorated data objects.

I find that using concrete examples typically help convey the overall concepts that I am trying to understand. Since my focus lately has been mainly on Stellar, I will use a core data type from Stellar called Asset. To create an asset, we define a mutation responsible for asset creation.

"Create a new asset owned by an existing Account"
createAsset(
"Alphanumeric code that uniquely identifies the asset"
asset_code: String!
"Public key of the asset issuer (creator)"
asset_issuer: String!
"The description of the asset"
description: String
): Asset

In the example above, we leverage the Stellar SDK to create the asset. The input of the payload consists of core Stellar data objects such as asset_code but also contains additional metadata that we store locally in our own database for use within our system. To allow for maximum flexibility later one, we defined a single interface called Asset that will allow us to return variations of the Asset depending upon the API that is being consumed while also provide some hints to our front end developers to denote which implementation of the interface is being returned.

The decision to move to interfaces was not done from the outset. In our early versions of our API strategy, we created custom data objects for each of our APIs and there was little reuse of schema objects. When we started to simplify our programming model, we felt that interfaces were the best approach. The rationale behind this was really around how we returned Asset information. Assets are core data objects that are included in many of Stellar’s APIs including payments, offers, and history queries. For these APIs, it was beneficial to return our fully decorated Asset implementation called TF_Asset while for more expensive queries, we felt it was sufficient to just return the lighter weight Core_Asset that mapped directly to Stellar.

Here are the two implementations of Asset for comparison purposes.

type Core_Asset implements Asset {
"Public key of the asset issuer (creator)"
asset_issuer: String
"Alphanumeric class that identifies the asset type"
asset_type: String!
"Alphanumeric code that uniquely identifies the asset"
asset_code: String
}
type TF_Asset implements Asset {
"Public key of the asset issuer (creator)"
asset_issuer: String
"Alphanumeric class that identifies the asset type"
asset_type: String!
"Alphanumeric code that uniquely identifies the asset"
asset_code: String
"The description of the asset"
description: String
"Email address for user. Will be used as display name"
email: String!
"Tenant id that the user is a member of"
tenantId: String!
"Creation date of the asset"
createdAt: Date!
"Last modified date of the asset"
updatedAt: Date!
}

With the existence of two possible implementations, the consumers of the APIs that contain Asset need hints to know what type of Asset is being returned. In our simple model, it was easy to identify based upon one of the required fields for Asset that would would only exist if the type is TF_Asset. GraphQL supports the ability to denote the type by defining a resolver. In our resolver, we created a basic if/else check to handle this.

Asset: {
__resolveType(obj, context, info){
if(obj.tenantId){
return 'TF_Asset';
}
return 'Core_Asset';
},
},

While outside of the scope of this article, let me show a more advanced version of this resolver. For our History API, we return a set of History objects of which each record could be a of different implementation of the interface. For that interface, the resolver becomes a bit more advanced but follows the same model.

History: {
__resolveType(obj, context, info){
if(obj.type === 'create_account'){
return 'Create_Account';
}
if(obj.type === 'change_trust'){
return 'Change_Trust';
}
if(obj.type === 'allow_trust'){
return 'Allow_Trust';
}
if(obj.type === 'payment'){
return 'Payment';
}
if(obj.type === 'manage_offer'){
return 'Manage_Offer';
}
if(obj.type === 'set_options'){
if(obj.signer_key || obj.signer_weight){
return 'Set_Signers';
}else if(obj.clear_flags || obj.set_flags){
return 'Account_Flags';
}else{
return 'Set_Threshold';
}
}
return null;
},
},

When performing GraphQL queries that deal with interfaces, there is a small modification in terms of how one accesses implementation specific data fields. Below is the query to get assets that have been created where we include not only core Asset data but also some additional data from TF_Asset

export const GET_ASSETS = gql`
query getAssets {
getAssets {
asset_code
asset_issuer
... on TF_Asset {
description
tenantId
email
createdAt
updatedAt
}
}
}
`;

Note the syntax around TF_Asset with the leading … This denotes to the GraphQL Apollo Client that these fields will only exist for data objects that are are of type TF_Asset. If the record is not of type TF_Asset, those fields will not be requested. This allows some flexibility around the data model where we can denote fields such as description, tenantId and email to always be required for TF_Asset but not require them for other Asset types.

Interfaces are a very powerful construct in GraphQL especially when trying to build a complex GraphQL data schema with many data types. The article above demonstrated a quick and easy to consume set of examples around creating an interface that maps quite well to Stellar. I have found that the interfaces greatly improved our overall design of our APIs and could probably write a chapter in a book on all of our usage of interfaces but that is beyond the scope of this article.