Ceramic Feature Release: SET Account Relations, Immutable Fields and shouldIndex flag
The functionality of Ceramic and ComposeDB has been recently enhanced by a number of new features that give developers more control over account relation definitions and data accessibility. More specifically, you can now use the following tools to enhance your applications:
- SET account relation - enabling users to enforce a constraint where each user account (or DID) can create only one instance of a model for a specific record of another model.
- Immutable fields - allow specific data to be prevented from being altered.
- shouldIndex flag - gives developers an option to manage the data visibility by choosing which fields should be indexed.
In this blog post, we are going to dive into these features in more detail. For a video walkthrough, check out this video tutorial.
SET account relations
SET relations in ComposeDB enable developers to define relations between the data models that follow specific constraints and include the user as part of the relationship. SET account relation allows users to enforce the constraint that a specific account (DID) can have only one instance of a model for a specific record of another model.
The best example to illustrate the “like” feature of a social media application. SET relation can be utilized to make sure that the user (DID) can “like” a specific post only once, while at the same time allowing the user to like multiple posts.
Let’s have a look at how SET Relations can be used in practice.
Ceramic Layer
To use SET account relation in Ceramic, you will first have to define a SET accountRelation
in your model definition. An example below consists of two simple models - POST_MODEL
representing the model definition for social media posts, and LIKE_MODEL
representing the model definition for users liking the posts.
The model definition for POST_MODEL
has the accountRelation as a list, meaning that one user account will be allowed to create multiple posts.
The model definition for LIKE_MODEL
has a SET
accountRelation and includes the fields which should be used to create the unique relation - postID
and userID
. This defines that a specific user can create only one "like" record for a specific post.
const POST_MODEL: ModelDefinition = {
name: 'Post',
version: '2.0',
interface: false,
implements: [],
accountRelation: {type: 'list'},
schema: {
$schema: '<https://json-schema.org/draft/2020-12/schema>',
type: 'object',
additionalProperties: false,
properties: {
content: {type: 'string', maxLength: 500},
author: {type: 'string'},
},
required: ['content', 'author'],
},
}
const LIKE_MODEL: ModelDefinition = {
name: 'Like',
version: '2.0',
interface: false,
implements: [],
accountRelation: {type: 'set', fields: ['postID', 'userID']},
schema: {
$schema: '<https://json-schema.org/draft/2020-12/schema>',
type: 'object',
additionalProperties: false,
properties: {
postID: {type: 'string'},
userID: {type: 'string'},
},
required: ['postID', 'userID'],
},
}
ComposeDB Layer
Now let's see an example of how you can use SET account relations in ComposeDB. Similar to the example above, the key component that allows you to define the SET account relation for a specific model is the accountRelation
scalar alongside the fields that should be used to define the unique relation.
Take the example below. Here we have two models defined using GraphQL schema definition language. The first model is a model for storing data about a Picture - the source and the dimensions of the image. The model definition Favourite implements the behavior of the user setting a picture as a favorite. Note that this model has an accountRelation
defined as SET. The field that is used to define the relation is docID
, which refers to the document ID of the picture record.
type Picture @createModel(description: "A model for pictures", accountRelation: SINGLE) {
src: String! @string(maxLength: 150),
mimeType: String! @string(maxLength: 50),
width: Int! @int(min:1),
height: Int! @int(min:1),
size: Int! @int(min:1),
}
type Favourite @createModel(description: "A set of favourite documents", accountRelation: SET, accountRelationFields: ["docID"]){
docID: StreamID! @documentReference(model: "Picture")
doc: Node @relationDocument(property: "docID")
note: String @string(maxLength: 500)
}
All this means that the user will be able to set only one image as a favorite. They can set different pictures as favorites, but only one record per picture.
Immutable Fields
Another feature that has been recently added to Ceramic is Immutable Fields. With Immutable Fields, you are able to define which fields (for example, some critical data) should remain unchangeable no matter what and be accessible as read-only data. Any attempt to alter the data set as immutable would result in an error message.
Ceramic Layer
Defining specific fields as immutable is pretty simple. Below we have an example of a simple model defining a Person - their address, name, and other details. To make these fields immutable you simply need to include them into the immutableFields
array. In the example below, fields like address
, name
, myArray
, and myMultipleType
will be set as immutable, meaning that once this data is created, it will be unchangeable:
const example_model : ModelDefinition = {
name : 'Person',
views : {},
schema : {
type : 'object',
$defs : {
Address : {
type : 'object',
title : 'Address',
required : [ 'street', 'city', 'zipCode' ],
properties : {
city : {type : 'string', maxLength : 100, minLength : 5},
street : {type : 'string', maxLength : 100, minLength : 5},
zipCode : {type : 'string', maxLength : 100, minLength : 5},
},
additionalProperties : false,
},
},
$schema : '<https://json-schema.org/draft/2020-12/schema>',
required : [ 'name', 'address' ],
properties : {
name : {type : 'string', maxLength : 100, minLength : 10},
address : {$ref : '#/$defs/Address'},
myArray : {type : 'array', maxItems : 3, items : {type : 'integer'}},
myMultipleType : {oneOf : [ {type : 'integer'}, {type : 'string'} ]},
},
additionalProperties : false,
},
version : '2.0',
interface : false,
relations : {},
implements : [],
description : 'Simple person with immutable field',
accountRelation : {type : 'list'},
immutableFields : [ 'address', 'name', 'myArray', 'myMultipleType' ],
}
ComposeDB Layer
In ComposeDB, a specific field can be set as immutable by adding a directive @immutable
to the fields that should remain unchangeable. For example:
type ModelWithImmutableProp@createModel(
accountRelation: SINGLE,
description: "Test model with an immutable int property"
) {
uniqueValue: Int @immutable
uniqueValue2: Int @immutable
}
}
Here, we set that fields uniqueValue
and uniqueValue2
are going to be immutable.
shouldIndex Flag
Last but not least, let’s talk about the shouldIndex Flag available in Ceramic and ComposeDB. shouldIndex Flag allows you to control the stream indexing by toggling a boolean metadata flag. It enables you to manage data visibility and indexing. By setting the shouldIndex Flag to false
, you can disable the stream from being indexed, making it “invisible” for indexing operations. Let’s take a look at how you can use this feature.
Ceramic Layer
When working with model documents, ModelInstanceDocument
, there is a new method called shouldIndex(boolean-value)
where false would indicate the stream corresponding to this model should not be indexed, and can be called with value = true to reindex an existing document, e.g.:
const document = await ModelInstanceDocument.create(ceramic, CONTENT0, midMetadata)
// Unindex
await document.shouldIndex(false)
ComposeDB Layer
There are two ways to signal that a stream shouldn’t be indexed using ComposeDB. The first one is by including the shouldIndex
option in a mutation query, setting it to true
if it should be indexed and set to false
in the contrary:
const runtime = new ComposeRuntime({ ceramic, context, definition: composite.toRuntime() })
await runtime.executeQuery<{
updateProfile: { viewer: { profile: { name: string } } }
}>(`
mutation UpdateProfile($input: UpdateProfileInput!) {
updateProfile(input: $input) {
viewer {
profile {
name
}
}
}
}
`, { input: { id: profileID, content: {}, options: { shouldIndex: false } } },
)
The second way is to use a mutation type called enableIndexing, just like a create or update mutation, it should be paired with the model’s name, sending the streamId and shouldIndex value as part of the input, e.g.:
const enableIndexingPostMutation = `mutation EnableIndexingPost($input: EnableIndexingPostInput!) {
enableIndexingPost(input: $input) {
document {
id
}
}
}`
await runtime.executeQuery<{ enableIndexingPost: { document: { id: string } } }>
enableIndexingPostMutation, { input: { id, shouldIndex: false } },)
Note that the shouldIndex flag doesn’t delete the data. If set to false
, the stream will still exist on the network; however, it will not be indexed and will not be available for data interactions.
Summary
The powerful new features in Ceramic and ComposeDB offer users a sophisticated toolkit for data management. From enforcing unique constraints with SET account relations to securing key data with immutable fields and controlling indexing operations using the shouldIndex flag, these features empower developers to build robust and efficient data models for their applications. Check out the Ceramic documentation for more information and examples.
Let us know how you are using all of these new features by posting on our Ceramic developer community forum.
Ceramic Resources
Developer Documentation: https://developers.ceramic.network/
Discord: https://chat.ceramic.network/
Github: https://github.com/ceramicnetwork
Twitter: https://twitter.com/ceramicnetwork
Website: https://ceramic.network/