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 shouldIndexoption 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/

Forum: https://forum.ceramic.network/