codeflood logo

Experience Edge Schema for Content Hub Tenants

Experience Edge hosts tenants for a number of different Sitecore systems including XM, XM Cloud, Content Hub and Content Hub ONE. However, the data models of the Content Hub based systems is different to that of the XM based systems, so each requires a different GraphQL schema to access the content which has been published to Experience Edge. In this post I'm going to explore the GraphQL schema which is generated for a Content Hub tenant.

The Content Hub data model is quite similar to the Sitecore XM data model in some respects, but quite different in many others. In XM the "documents" are called items, and their fields are defined by a template. In Content Hub the "documents" are called entities, and their properties are defined by an Entity Definition. The terminology used inside Experience Edge is the same used in Content Hub as Experience Edge started out initially as a service for Content Hub. The data model used in Experience Edge is flexible enough to store both Content Hub and XM structured data.

XM structures items into a hierarchical tree and items can reference other items through special fields. Content Hub (and Experience Edge) structure the entities into a graph. There is no hierarchical tree in which the entities are stored. Entities reference other entities through relations, which are different to properties.

Given the more general nature of the Content Hub data model, the GraphQL schema used in Experience Edge for Content Hub tenants must also be a bit more general than the one we have for XM, which I discussed previously in my post Experience Edge Schema for XM Tenants.

The GraphQL schema for a Content Hub tenant is dynamic. It is generated based on the Entity Definitions which have been published by the Content Hub instance. For each entity definition, types are added to the GraphQL schema to support retrieving a single entity or a list of entities, including the ability to filter using different criteria. Even the predicates used in the filtering criteria are generated based on the Entity Definition as the properties of the predicate need to match those of the Entities being filtered.

To execute any of the queries in this post, you can use the IDE which is accessible at https://edge.sitecorecloud.io/api/graphql/ide or you can use the tips I provided in my previous post about Using the Experience Edge GraphQL API. In either case, you'll need a Content Hub tenant on Experience Edge and an API key for that tenant.

Top-level Query Fields

For each Entity Definition which is published to Experience Edge, the following fields are added to the top-level Query type:

  • {name} to find a single entity of that type, by it's ID.
  • all{name} to retrieve a list of entities of that type. Optional filtering can be applied using the where argument.

The {name} in the fields is derived from the Entity Definition name but transformed to be a safe GraphQL field name. Any leading uppercase letter is switched to lowercase, and invalid field characters like dot (.) are replaced with underscores (_). For example, the query name used for M.Asset is m_Asset.

Entity Definition Types

To support the top-level query fields, a few more types are also added to the GraphQL schema:

  • {name} which defines the fields available on the type.
  • {name}List which is a list of {name} types.
  • {name}Predicate to allow filtering entities of that type.
  • {name}Sorts to allow specifying how sorting should be done for the type.

Entity Definition types in the Experience Edge GraphQL schema.

The fields available on the {name} type depend on the properties and relations defined in the Entity Definition, and the publishing settings which have been set for those properties. By default, any new Entity Definition and any new properties and relations are enabled for publication to Experience Edge.

Let's assume we have the following Entity Definition defined in Content Hub:

MyDefinition

Field Type Field Name Configuration
Property Title String - Single-Line
Property Content String - HTML
Relation MyDefinitionToAsset ManyToMany M.Asset

The GraphQL schema includes a type for this Entity Definition:

type MyDefinition {
  content: String
  createdBy: String
  createdOn: DateTime
  id: ID
  modifiedBy: String
  modifiedOn: DateTime
  myDefinitionToAsset(
    after: String
    first: Int = 10
    orderBy: [M_AssetSorts]
    where: M_AssetPredicate
  ): M_AssetList!
  title: String
}

The fields of the type reflect the properties and relations defined on the Entity Definition. You might notice some extra fields on the type like createdBy and modifiedOn. These are standard fields which Content Hub includes when creating new Entity Definitions. The properties are scalar types like String and DateTime but the relations link through to the entities which are included in the relation. So when querying for an entity of type MyDefinition, the linked assets in the MyDefinitionToAsset relation can be accessed using the myDefinitionToAsset field.

query {
  myDefinition(id: "FeognuxPQ3Kma-UxBxkGzg") {
    id
    title
    myDefinitionToAsset {
      results {
        id
        fileName
      }
    }
  }
}

The MyDefinitionToAsset relation on the MyDefinition Entity Definition has many-to-many cardinality, which will be exposed as a list of entities, not just a single entity. As you can see from the MyDefinition type above, the type of the myDefinitionToAsset field is M_AssetList!. It's a list of M_Asset.

Find by ID

The query field named after the Entity Definition is used to locate an entity by it's identifier. Although the argument is named id, this is not to be confused with the Content Hub ID which is numeric. The id argument is required.

query MyEntity {
  myDefinition(id: "FeognuxPQ3Kma-UxBxkGzg") {
    id
    title
  }
}

The response is of type MyDefinition.

{
  "data": {
    "myDefinition": {
      "id": "FeognuxPQ3Kma-UxBxkGzg",
      "title": "Beta"
    }
  }
}

List by Type

The all query field is used to list all entities of that type. Filtering can also be applied using the where argument. Because the result will be a list, there are also arguments to support paging and sorting.

query MyEntities {
  allMyDefinition(first: 3) {
    results {
      id
      title
    }
  }
}

The response is of type MyDefinitionList.

{
  "data": {
    "allMyDefinition": {
      "results": [
        {
          "id": "RQetXvnuSsqFj0j-m1UbwQ",
          "title": "Gamma"
        },
        {
          "id": "FeognuxPQ3Kma-UxBxkGzg",
          "title": "Beta"
        },
        {
          "id": "X7Fw82g5SH-FTMVX9Q7MUA",
          "title": "Alpha"
        }
      ]
    }
  }
}

The above query will list the first page of entities which are based on the MyDefinition Entity Definition, sorted by the id field. The first argument specifies the number of results on the page and defaults to 20. The where argument can be used to filter the results returned based on criteria for each property and relation. The type of the where argument is {name}Predicate which includes fields for filtering on any of the fields defined by the Entity Definition. In this case, the where argument is of type MyDefinitionPredicate. The predicate type is also dynamically generated based on the Entity Definition and is defined in the GraphQL schema as:

input MyDefinitionPredicate {
  AND: [MyDefinitionPredicate!]
  content_allOf: [String]
  content_anyOf: [String]
  content_contains: String
  content_doesnotcontain: String
  content_doesnotendwith: String
  content_doesnotstartwith: String
  content_endswith: String
  content_eq: String
  content_neq: String
  content_noneOf: [String]
  content_startswith: String
  createdBy_allOf: [String]
  createdBy_anyOf: [String]
  createdBy_contains: String
  createdBy_doesnotcontain: String
  createdBy_doesnotendwith: String
  createdBy_doesnotstartwith: String
  createdBy_endswith: String
  createdBy_eq: String
  createdBy_neq: String
  createdBy_noneOf: [String]
  createdBy_startswith: String
  createdOn_between: [DateTime]
  createdOn_eq: DateTime
  createdOn_gt: DateTime
  createdOn_lt: DateTime
  createdOn_neq: DateTime
  id_anyOf: [ID]
  id_eq: ID
  id_neq: ID
  id_noneOf: [ID]
  modifiedBy_allOf: [String]
  modifiedBy_anyOf: [String]
  modifiedBy_contains: String
  modifiedBy_doesnotcontain: String
  modifiedBy_doesnotendwith: String
  modifiedBy_doesnotstartwith: String
  modifiedBy_endswith: String
  modifiedBy_eq: String
  modifiedBy_neq: String
  modifiedBy_noneOf: [String]
  modifiedBy_startswith: String
  modifiedOn_between: [DateTime]
  modifiedOn_eq: DateTime
  modifiedOn_gt: DateTime
  modifiedOn_lt: DateTime
  modifiedOn_neq: DateTime
  myDefinitionToAsset: MyDefinitionToAssetPredicate
  OR: [MyDefinitionPredicate!]
  title_allOf: [String]
  title_anyOf: [String]
  title_contains: String
  title_doesnotcontain: String
  title_doesnotendwith: String
  title_doesnotstartwith: String
  title_endswith: String
  title_eq: String
  title_neq: String
  title_noneOf: [String]
  title_startswith: String
}

As you can see, the predicate type includes fields to perform operations across all the fields of the MyDefinition type.

The predicate type supports several comparison operations per property:

  • *_allOf: Checks the field contains all of the provided values.
  • *_anyOf: Checks the field contains any of the provided values.
  • *_contains: Checks the field contains the provided value.
  • *_doesnotcontain: Checks the field does not contain the provided value.
  • *_doesnotendwith: Checks the field does not end with the provided value.
  • *_doesnotstartwith: Checks the field does not starts with the provided value.
  • *_endswith: Checks the field ends with the provided value.
  • *_eq: Checks the field is an exact match of the provided value.
  • *_neq: Checks the field is not an exact match of the provided value.
  • *_noneOf: Checks the field does not contain any of the provided values.
  • *_startswith: Checks the field starts with the provided value.

The predicate input type also includes fields to combine multiple predicates:

  • AND: All predicates must match
  • OR: Any predicate must match

The following query will filter the list of entities for those that have a title property equal to "alpha".

query MyAlphaEntities {
  allMyDefinition(where: {
    title_eq: "alpha"
  }) {
    results {
      id
      title
    }
  }
}

Now only the entity whose title is Alpha is returned.

{
  "data": {
    "allMyDefinition": {
      "results": [
        {
          "id": "X7Fw82g5SH-FTMVX9Q7MUA",
          "title": "Alpha"
        }
      ]
    }
  }
}

More complex predicates can be created by combining several predicates together. The following query will filter for MyDefinition entities that contain the word "lorem" in the Content field and that were created by the Administrator user.

query MyEntities {
  allMyDefinition(where: {
    AND: [
      { createdBy_eq: "Administrator" }
      { content_contains: "Lorem" }
    ] }) {
    results {
      id
      title
    }
  }
}

If the data set is larger, the results can be paged. Experience Edge supports cursor paging where each page of results returns an opaque cursor which can be passed back in the next query to get the next page of results. The cursor is available alongside other paging information in the pageInfo field of the {name}List type.

query MyEntities {
  allMyDefinition(first: 200) {
    pageInfo {
      hasNext
      endCursor
    }
    results {
      id
      title
    }
  }
}

The response now includes information on the current page of results.

{
  "data": {
    "allMyDefinition": {
      "pageInfo": {
        "hasNext": true,
        "endCursor": "eyJzZWFyY2hBZnRlciI6WzE2ODQ4MDAxMjY3MTksIkZlb2dudXhQUTNLbWEtVXhCe...=="
      },
      "results": [
        {
          "id": "RQetXvnuSsqFj0j-m1UbwQ",
          "title": "Gamma"
        },
        {
          "id": "FeognuxPQ3Kma-UxBxkGzg",
          "title": "Beta"
        },
        ...
      ]
    }
  }
}

To get the next page of results, pass the pageInfo.endCursor string returned in the above results into the after argument of the all query.

query MyEntities {
  allMyDefinition(first: 200 after:"eyJzZWFyY2hBZnRlciI6WzE2ODQ4MDAxMjY3MTksIkZlb2dudXhQUTNLbWEtVXhCe...==") {
    pageInfo {
      hasNext
      endCursor
    }
    results {
      id
      title
    }
  }
}

By default, the entities returned in a list are sorted by their ID. The orderBy argument of the all query can be specified to change the sort order. The orderBy argument is of type [{name}Sorts]. The sort type is generated based on the properties (not relations) of the Entity Definition.

enum MyDefinitionSorts {
  CONTENT_ASC
  CONTENT_DESC
  CREATEDBY_ASC
  CREATEDBY_DESC
  CREATEDON_ASC
  CREATEDON_DESC
  ID_ASC
  ID_DESC
  MODIFIEDBY_ASC
  MODIFIEDBY_DESC
  MODIFIEDON_ASC
  MODIFIEDON_DESC
  TITLE_ASC
  TITLE_DESC
}

Sorting can be specified either ascending or descending for each property.

query MyEntities {
  allMyDefinition(orderBy: CREATEDON_ASC) {
    results {
      id
      title
    }
  }
}

The orderBy argument accepts an array of {name}Sorts, so multiple sorts can also be defined.

query MyEntities {
  allMyDefinition(orderBy: [TITLE_ASC CREATEDON_DESC]) {
    results {
      id
      title
    }
  }
}

CMP Interfaces

The Content Hub Content Marketing Platform (CMP) functionality allows authors to easily define new content types. Under the hood, the content type UI is simply adding additional fields to the M_Content entity definition. Having so many fields on a single type can make querying and discoverability a bit messy, so Experience Edge emits interfaces into the GraphQL schema for each CMP content type so CMP content can be treated as a specific type rather than just M_Content all the time.

Content type definitions are treated the same way as entity definitions, with the following fields added to the Query type in the GraphQL schema:

  • {ctname} to find a single entity of that content type, by it's ID.
  • all_{ctname} to retrieve a list of entities of that content type. Optional filtering can be applied using the where argument.

The following types are also added to the GraphQL schema to support the querying capabilities.

  • {ctname} which defines all fields available on the content type, including those from M_Content.
  • I{ctname} which defines only the fields of the content type.
  • {ctname}List which is a list of {ctname} types.

In this case {ctname} is the name of the content type, such as M_Content_Blog.

The fields on the I{ctname} interface type are only the fields defined on the content type. So for the OOTB Blog content type the interface is defined as such:

interface IM_Content_Blog {
  blog_Body: String
  blog_Quote: String
  blog_Title: String
}

In addition to the content type specific interfaces, the IM_Content interface defines all the default properties for M_Content. To provide access to these, the {ctname} type implements both the I{ctname} interface and the IM_Content interface. When an entity based on M_Content is going to be returned, the concrete {ctname} is used instead. If querying over an array of M_Content (such as the result of an allM_Content query) inline fragments can be used to cast the IM_Content to the proper type.

query {
  allM_Content {
    results {
      id
      content_Name
      ... on M_Content_Blog {
        blog_Body
      }
      ... on M_Content_Advertisement {
        advertisement_Body
      }
    }
  }
}

The additional query fields also make querying for a specific content type easier. Without the query fields, to find all blogs, a where argument must be used on the allM_Content query:

query {
  allM_Content(where: {
    contentTypeToContent: {
      m_ContentType_ids: "M.ContentType.Blog"
    }
  }) {
    results {
      id
      content_Name
    }
  }
}

The allM_Content_Blog query field removes the need for the where argument.

query {
  allM_Content_Blog {
    results {
      id
      content_Name
    }
  }
}

Conclusion

The GraphQL schema used by Content Hub tenants on Experience Edge is dynamically generated based on the Entity Definitions which have been enabled for publishing to Experience Edge in the Content Hub instance. Various types and added to the GraphQL schema for each Entity Definition to support the querying capabilities of the Delivery API.

Comments

Leave a comment

All fields are required.