Experience Sitecore ! | All posts tagged 'GraphQL'

Experience Sitecore !

More than 200 articles about the best DXP by Martin Miles

GraphQL: not an ideal one!

You’ll find plenty of articles about how amazing GraphQL is (including mine), but after some time of using it, I’ve got some considerations with the technology and want to share some bitter thoughts about it.

GraphQL

History of GraphQL

How did it all start? The best way to answer this question is to go back to the original problem Facebook faced.

Back in 2012, we began an effort to rebuild Facebook’s native mobile applications. At the time, our iOS and Android apps were thin wrappers around views of our mobile website. While this brought us close to a platonic ideal of the “write once, run anywhere” mobile application, in practice, it pushed our mobile web view apps beyond their limits. As Facebook’s mobile apps became more complex, they suffered poor performance and frequently crashed. As we transitioned to natively implemented models and views, we found ourselves for the first time needing an API data version of News Feed — which up until that point had only been delivered as HTML.

We evaluated our options for delivering News Feed data to our mobile apps, including RESTful server resources and FQL tables (Facebook’s SQL-like API). We were frustrated with the differences between the data we wanted to use in our apps and the server queries they required. We don’t think of data in terms of resource URLs, secondary keys, or join tables; we think about it in terms of a graph of objects.

Facebook came across a specific problem and created its own solution: GraphQL. To represent data in the form of a graph, the company designed a hierarchical query language. In other words, GraphQL naturally follows the relationships between objects. You can now receive nested objects and return them all in a single HTTPS request. Back in the day, it was crucial for global users not to always have cheap/unlimited mobile tariff plans, so the GraphQL protocol was optimized, allowing only what users needed to be transmitted.

Therefore, GraphQL solves Facebook’s problems. Does it solve yours?

First, let’s recap the advantages

  • Single request, multiple resources: Compared to REST, which requires multiple network requests to each endpoint, GraphQL you can request all resources with a single call.
  • Receive accurate data: GraphQL minimizes the amount of data transferred over the wires, selectively selecting it based on the needs of the client application. Thus, a mobile client with a small screen may receive less information.
  • Strong typing: Every request, input, and response objects have a type. In web browsers, the lack of types in JavaScript has become a weakness that various tools (Google’s Dart, and Microsoft’s TypeScript) try to compensate for. GraphQL allows you to exchange types between the backend and frontend.
  • Better tooling and developer friendliness: The introspective server can be queried about the types it supports, allowing for API explorer, autocompletion, and editor warnings. No more relying on backend developers to document their APIs. Simply explore the endpoints and get the data you need.
  • Version independent: the type of data returned is determined solely by the client request, so servers become simpler. When new server-side features are added to the product, new fields can be added without affecting existing clients.

Thanks to the “single request, multiple resources” principle, front-end code has become much simpler with GraphQL. Imagine a situation where a user wants to get details about a specific writer, for example (name, id, books, etc.). In a traditional intuitive REST pattern, this would require a lot of cross-requests between the two endpoints /writers and /books, which the frontend would then have to merge. However, thanks to GraphQL, we can define all the necessary data in the request, as shown below:

writers(id: "1"){
    id
    name
    avatarUrl
    books(limit: 2){
        name
        urlSlug
    }
}

The main advantage of this pattern is to simplify the client code. However, some developers expected to use it to optimize network calls and speed up application startup. You don’t make the code faster; you simply transfer the complexity to the backend, which has more computing power. Also for many scenarios, metrics show that using the REST API appeared faster than GraphQL.

This is mostly relevant for mobile apps. If you’re working with a desktop app or a machine-to-machine API, there’s no added value in terms of performance.

Another point is that you may indeed save some kilobytes with GraphQL, but if you really want to optimize loading times, it’s better to focus on loading lower-quality images for mobile, as we’ll see, GraphQL doesn’t work very well with documents.

But let’s see what actually is wrong or could be better with GraphQL.

Strongly Typing

GraphQL defines all API types, commands, and queries in the graphql.schema file. However, I’ve found that typing with GraphQL can be confusing. First of all, there is a lot of duplication here. GraphQL defines the type in the schema, however, we need to override the types for our backend (TypeScript with node.js). You have to spend additional effort to make it all work with Zod or create some cumbersome code generation for types.

Debugging

It’s hard to find what you’re looking for in the Chrome inspector because all the endpoints look the same. In REST you can tell what data you’re getting just by looking at the URL:

Devtools 

compared to:

  Devtools

Do you see the difference?

No support for status codes

REST allows you to use HTTP error codes like “404 not found”, “500 server error” and so on, but GraphQL does not. GraphQL forces a 200 error code to be returned in the response payload. To understand which endpoint failed, you need to check each payload. Same applies for Monitoring: HTTP error monitoring is very easy compared to GraphQL because they all have their error code while troubleshooting GraphQL requires parsing JSON objects.

Additionally, some objects may be empty either because they cannot be found or because an error occurred. It can be difficult to distinguish the difference at a glance.

Versioning

Everything has its price. When modifying the GraphQL API, you can make some fields obsolete, but you are forced to maintain backward compatibility. They should still remain there for older clients who use them. You don’t need to support GraphQL versioning, at the price of maintaining each field.

To be fair, REST versioning is also a pain point, but it does provide an interesting feature for expiring functionality. In REST, everything is an endpoint, so you can easily block legacy endpoints for a new user and measure who is still using the old endpoint. Redirects could also simplify versioning from older to newer in some cases.

Pagination

GraphQL Best Practices suggests the following:

The GraphQL specification is deliberately silent on several important API-related issues, such as networking, authorization, and pagination.

How “convenient” (not!). In general, as it turns out, pagination in GraphQL is very painful.

Caching

The point of caching is to receive a server response faster by storing the results of previous calculations. In REST, the URLs are unique identifiers of the resources that users are trying to access. Therefore, you can perform caching at the resource level. Caching is a part of HTTP specification. Additionally, the browser and mobile device can also use this URL and cache the resources locally (same as they do with images and CSS).

In GraphQL this gets tricky because each query can be different even though it’s working on the same entity. It requires field-level caching, which is not easy to do with GraphQL because it uses a single endpoint. Libraries like Prisma and Dataloader have been developed to help with such scenarios, but they still fall short of REST capabilities.

Media types

GraphQL does not support uploading documents to the server, which is used multipart-form-data by default. Apollo developers have been working on a file-uploads solution, but it is difficult to set up. Additionally, GraphQL does not support a media types header when retrieving a document, which allows the browser to display the file correctly.

I previously made a post about the steps one must take in order to upload an image to Sitecore Media Library (either XM Cloud or XM 10.3 or newer) by using Authoring GraphQL API.

Security

When working with GraphQL, you can query exactly what you need, but you should be aware that this comes with complex security implications. If an attacker tries to send a costly request with attachments to overload the server, then may experience it as a DDoS attack.

They will also be able to access fields that are not intended for public access. When using REST, you can control permissions at the URL level. For GraphQL this should be the field level:

user {
    username <-- anyone can see that
    email <-- private field
    post {
      title <-- some of the posts are private
  }
}

Conclusion

REST has become the new SOAP; now GraphQL is the new REST. History repeats itself. It’s hard to say whether GraphQL will just be a popular new trend that will gradually be forgotten, or whether it will truly change the rules of the game. One thing is certain: it still requires some development to obtain more maturity.

A crash course of GraphQL – from Zero to Hero

Almost anyone attending XM Cloud sessions at SUGCON North America earlier saw GraphQL queries as a part of such presentations. For mature headless developers getting through each query requires some amount of time, while for newbies that “user-friendly” syntax stands as an unreachable barrier. Once in the past, I failed to find any good article about this without any doubt great query language – some of them were way too excessive while others missed out on a lot of basics. I decided to fill this gap by creating this article providing exactly that: the minimum required information for the maximum productive start. I write it for you in that exact manner as I wish it was written for me earlier.

Logo

What is QraphQL and why do I need it?

QraphQL is a query language and backend framework for open-source APIs introduced by Facebook in 2012 and was designed to make it easier managing endpoints for REST-based APIs. In 2015, GraphQL was made open source, and Airbnb, GitHub, Pinterest, Shopify, and many other companies now use GraphQL.

When Facebook developers created a mobile application, they looked for ways to speed things up. There was a difficulty: when simultaneously querying from different types of databases, for example from cloud Redis and MySQL, the application slowed down terribly. To solve the problem, Facebook came up with its own query language that addresses a single endpoint and simplifies the form of the requested data. This was especially valuable for a social network with lots of connections and requests for related elements: say, getting posts from all subscribers of user X.

REST is a good and functional technology, but it has some problems:

  • Firstly, there’s redundancy or lack of data in the response. In REST APIs, clients often receive either too much data that they don’t need, or too little, forcing them to make multiple requests to get the information they need. GraphQL allows clients to request only the data they need and receive it in a single request, making communication more efficient.
  • Also, in a REST API, each endpoint usually corresponds to a specific resource, which can lead to extensibility problems and support for multiple API versions. GraphQL, however, features a single endpoint for all requests, and the API schema is defined server-side. This makes the API more flexible and easier to develop.
  • When working with related data in many REST APIs, the N+1 requests problem arises, when obtaining related data makes you do additional request roundtrips to a server. GraphQL allows you to define relationships between requested data and retrieve everything required in a single query.

Coming back to the above use case – a social network has many users, and for each user, we are required to get a list of his latest posts. To obtain such data in a typical REST API, one needs to make several requests: one request to the users endpoint to obtain a list of users, followed by another request to the posts endpoint to obtain posts for all required users deriving from the previous request (in the worst case it implements as a request for each the desired user). GraphQL solves this problem more efficiently. You can request a list of users and at the same time specify what exactly you want to get with the user details, in our case – the latest posts for each user.

Take a look at the example of GraphQL query implementing exactly that: request users with their 5 most recent posts:

query {
  users {
    id
    name
    posts(last:5){
      id
      text
      timestamp
    }
  }
}

What makes this work? This works thanks to the GraphQL structure.

But why at all it has a Graph in its name? That’s because it represents a data structure in the form of a graph, where the nodes of the graph represent objects, and there are connections between these objects. This reflects the way data and queries are organized in GraphQL, where clients can query related data as well as only the data they need.

A graph shows the relationships of say a social network:

Graph

How do we access a graph via GraphQL? GraphQL goes to a specific record, called the root node, and instructs it to get all the details of that record. We can take, for example, user 1, and get their subscriber data. Let’s write a GraphQL query snippet to show how to access it:

query {
    user(id:"1"){
        followers {
            tweets {
                content
            }
        }
    }
}

Here we are asking GraphQL to navigate to the graph from the root node, which is the user object with argument id: 1, and access the content of the follower’s tweet.

Graph Query

So far, so good. Let’s discuss the query types in GraphQL in more detail.

GraphQL Request Types

There are three main request types in GraphQL:

  • Query
  • Mutation
  • Subscription

Sitecore uses only the first two and does not support subscriptions, but to keep this guide full I will still mention how they work.

Queries in GraphQL

We have already become familiar with them from our earlier examples.

Using a query, GraphQL receives the necessary data from the server. This request type is an analog of what GET does in REST. Requests are string values sent in the body of an HTTP POST request. Please note that all GraphQL request types are sent via POST which is de-facto the most common option of HTTP data exchange. GraphQL can also work over Websockets, gRPC, and on top of other transport protocols.

We have already seen Query examples above, but let’s do it again to get the fname and age of all users:

query {
  users {
    fname
    age
  }
}

The server sends response data in JSON format so that the response structure matches the request structure:

data :{
    users [
        {
            "fname":"Mark",
            "age":23
        },
        {
            "fname":"David",
            "age":29
        }
    ]
}

The response contains JSON with the data key and also the errors key (in case there are any errors). Below is an example of a faulty response when an error occurred – due to the fact that Maria’s age was mistakenly passed as a string value:

{
    "errors":[
        {
        "message":"Error: 'age' field has incorrect value 'test'.",
        "locations":[
            {
                "line":5,
                "column":5
            }
        ],
        "path":["users",0,"age"]
        }
    ],
    "data":{
        "users":[
            {
                "fname":"Maria",
                "age":"test"
            },
            {
                "fname":"Megan",
                "age":32
            }
        ]
    }
}

Mutations in GraphQL

Using mutations you can add or modify the data. Mutation is an analogue of POST and PUT in REST. Here’s a mutation request example:

mutation createUser{
  addUser(fname:"Martin", age:42){
    id
  }
}

This createUser mutation adds a user with fname Martin and age 42. The server sends a JSON response to this request with the result record id. The answer may look like below:

data :{
  addUser :"a12e5d"
}

Subscription in GraphQL

With the help of subscriptions, the client receives database changes in real time. Under the hood, subscriptions use WebSockets. Here’s an example:

subscription listenLikes {
  listenLikes {
    fname
    likes
  }
}

The above query can, for example, return a list of users with their names and the count of likes every time it changes. Extremely helpful!

For example, when a user with fname Matt receives a like, the response would look like:

data:{
    listenLikes:{
        "fname":"Matt",
        "likes":245
    }
}

A similar request can be used to update the likes count in real-time, say for the voting form results.

GraphQL Concepts

Now that we know different query types, let’s figure out how to deal with elements that are used in GraphQL.

Concepts I am going to cover below:

  1. Fields
  2. Arguments
  3. Aliases
  4. Fragments
  5. Variables
  6. Directives

1. Fields

Look at a simple GraphQL query:

{
  user {
    name
  }
}

In this request, you see 2 fields. The user field returns an object containing another field of type String. GraphQL server will return a user object with only the user’s name. So simple, so let’s move on.

2. Arguments

In the example below, an argument is passed to indicate which user to refer to:

{
  user(id:"1"){
    name
  }
}

Here in particular we’re passing the user’s id, but we could also pass a name argument, assuming the API has a backend function to return such a response. We can also have a limit argument indicating how many subscribers we want to return in the response. The below query returns the name of the user with id=1 and their first 50 followers:

{
  user(id:"1"){
    name
    followers(limit:50)
  }
}

3. Aliases

GraphQL uses aliases to rename fields within a query response. It might be useful to retrieve data from multiple fields having the same names so that you ensure these fields will have different names in the response to distinguish. Here’s an example of a GraphQL query using aliases:

query {
  products {
    name
    description
  }
  users {
    userName: name
    userDescription: description
  }
}

as well as the response to it:

{
    "data":{
        "products":[
            {
            "name":"Product A",
            "description":"Description A"
            },
            {
            "name":"Product B",
            "description":"Description B"
            }
        ],
        "users":[
            {
            "userName":"User 1",
            "userDescription":"User Description 1"
            },
            {
            "userName":"User 2",
            "userDescription":"User Description 2"
            }
        ]
    }
}

This way we can distinguish the name and description of the product from the name and description of the user in the response. It reminds me of the way we did this in SQL when joining two tables, to distinguish between the same names of two joined columns. This problem most often occurs with the id and name columns.

4. Fragments

The fragments are often used to break up complex application data requirements into smaller chunks, especially when you need to combine many UI components with different fragments into one initial data sample.

{
  leftComparison: tweet(id:1){
    ...comparisonFields
  }
  rightComparison: tweet(id:2){
    ...comparisonFields
  }
}

fragment comparisonFields on tweet {
  userName
  userHandle
  date
  body
  repliesCount
  likes
}

What’s going on with this request?

  1. We sent two requests to obtain information about two different tweets: a tweet with id equal 1 and tweet with id equal 2.
  2. For each request, we create aliases: leftComparison and rightComparison.
  3. We use the fragment comparisonFields, which contains a set of fields that we want to get for each tweet. Fragments allow us to avoid duplicating code and reuse the same set of fields in multiple places in the request (DRY principle).

It returns the following response:

{
    "data":{
        "leftComparison":{
            userName:"foo",
            userHandle:"@foo",
            date:"2019-05-01",
            body:"Life is good",
            repliesCount:10,
            tweetsCount:200,
            likes:500,
        },
        "rightComparison":{
            userName:"boo",
            userHandle:"@boo",
            date:"2018-05-01",
            body:"This blog is awesome",
            repliesCount:15,
            tweetsCount:500,
            likes:700
        }
    }
}

5. Variables

GraphQL variables are a way to dynamically pass a value into a query. The example below provides a user id statically to the request:

{
  accholder: user(id:"1"){
    fullname: name
  }
}

Let’s now replace the static value by adding a variable. The above can be rewritten as:

query GetAccHolder($id: String){
  accholder: user(id: $id){
    fullname: name
  }
}
{
"id":"1"
}

In this example, GetAccHolder is a named function that is useful when you have plenty of requests in your application.

Then we declared the variable $id of type String. Well, then it’s exactly the same as in the original request, instead of a fixed id, we provided the variable $id to the request. The actual values of the variables are passed in a separate block.

We can also specify a default value for a variable:

query GetAccHolder($id: String = "1"){
  accholder: user(id: $id){
    fullname: name
  }
}

Additionally, it is possible to define a variable mandatory by adding ! to data type:

query GetAccHolder($id: String!){
  accholder: user(id: $id){
    fullname: name
  }
}

6. Directives

We can dynamically generate a query structure by using directives. They help us dynamically change the structure and form of our queries using variables. @include and @skip are two directives available in GraphQL.

Examples of directives:

  • @include(if: Boolean)include the field if the value of the boolean variable = true
  • @skip(if: Boolean) — skip field if boolean variable value = true
query GetFollowers($id: String){
  user(id: $id){
    fullname: name,
    followers: @include(if: $getFollowers){
      name
      userHandle
      tweets
    }
  }
}

{
"id":"1",
"$getFollowers":false
}

Since $getFollowers equals true, the followers field will get skipped, i.e. excluded from the response.

GraphQL Schema

In order to work with GraphQL on the server, you need to deploy a GraphQL Schema, which describes the logic of the GraphQL API, types, and data structure. A schema consists of two interrelated objects: typeDefs and resolvers.

In order for the server to work with GraphQL types, they must be defined. The typeDef object defines a list of available types, its code looks as below:

const typeDefs= gql`
  type User {
    id: Int
    fname: String
    age: Int
    likes: Int
    posts:[Post]
}
  type Post {
    id: Int
    user: User
    body: String
}
  type Query {
    users(id: Int!): User!
    posts(id: Int!): Post!
}
  type Mutation {
    incrementLike(fname: String!):[User!]
}
  type Subscription {
    listenLikes :[User]
}
`;

The above code defines a type User, which specifies fname, age, likes as well as other data. Each field defines a data type: String or Int, an exclamation point next to it means that a field is required. GraphQL supports four data types:

  1. String
  2. Int
  3. Float
  4. Boolean

The above example also defines all three types – Query, Mutation, and Subscription.

  • The first type which contains Query, is called users. It takes an id and returns an object with the user’s data, it is a required field. There is another Query type called posts which is designed the same way as users.
  • The Mutation type is called incrementLike. It takes a fname parameter and returns a list of users.
  • The Subscription type is called listenLikes. It returns a list of users.

After defining the types, you need to implement their logic so that the server knows how to respond to requests from a client. We use Resolvers to address that. Resolver is a function that returns specific field data of the type defined in the schema. Resolvers can be asynchronous. You can use resolvers to retrieve data from a REST API, database, or any other source.

So, let’s define resolvers:

const resolvers= {
    Query:{
        users(root, args){return users.filter(user=> user.id=== args.id)[0]},
        posts(root, args){return posts.filter(post=> post.id=== args.id)[0]}
    },
    User:{
        posts:(user)=> {
            return posts.filter(post=> post.userId=== user.id)
    }
    },
    Post:{
        user:(post)=> {
            return users.filter(user=> user.id=== post.userId)[0]
    }
    },
    Mutation:{
        incrementLike(parent, args){
        users.map((user)=> {
            if(user.fname=== args.fname) user.likes++return user
    })
        pubsub.publish('LIKES',{listenLikes: users});
        return users
    }
    },
    Subscription:{
        listenLikes:{
        subscribe:()=> pubsub.asyncIterator(['LIKES'])
    }
    }
};

The above example features six functions:

  1. The users request returns a user object having the passed id.
  2. The posts request returns a post object having the passed id.
  3. In the posts User field, the resolver accepts the user’s data and returns a list of his posts.
  4. In the user Posts field, the function accepts post data and returns the user who published the post.
  5. The incrementLike mutation changes the users object: it increases the number of likes for the user with the corresponding fname. After this, users get published in pubsub with the name LIKES.
  6. listenLikes subscription listens to LIKES and responds when pubsub is updated.

Two words about pubsub. This tool is a real-time information transfer system using WebSockets. pubsub is convenient to use, since everything related to WebSockets is placed in separate abstractions.

Why GraphQL is conceptually successful

  • Flexibility. GraphQL does not impose restrictions on query types, making it useful for both typical CRUD operations (create, read, update, delete) and queries with multiple data types.
  • Schema definition. GraphQL automatically creates a schema for the API, and the hierarchical code organization with object relationships reduces the complexity.
  • Query optimization. GraphQL allows clients to request the exact information they need. This reduces server response time and the volume of data to be transferred over the network.
  • Context. GraphQL takes care of the requests and responses implementation so that developers can focus on business logic. Strong Typing helps prevent errors before executing a request.
  • Extensibility. GraphQL allows extending the API schema and adding new data types along with reusing existing code and data sources to avoid code redundancy.

Sources and references

XM Cloud: a modern way to content management and import with Authoring GraphQL API

When it comes to content management, how would you deal with automating it?

You’ve probably thought of Data Exchange Framework for setting up content import from external sources on a regular basis, or Sitecore PowerShell Extensions as the universal Swiss Army knife that allows doing everything.

Sitecore XM Cloud is a modern SaaS solution and therefore offers you one more way of managing and importing content via GraphQL mutations. This is also an option for the latest 10.3 XM/XP platforms bringing them a step closer to the composable world of today.

There is a video walkthrough of the wholee exercise at the bottom of this post.

Please welcome: Authoring and Management API!

The documentation prompts a broad and awe-inspiring list of things you can do with Authoring API against your instance: create and delete items, templates, and media. It also empowers you to do some operations around site context and perform a content search on your CM instance.

Management API in addition gives you control over operations using queries and mutations for the following GraphQL types:

  • Archiving
  • Database
  • Indexing
  • Job
  • Language
  • Publishing
  • Security
  • Workflow
  • Rules

With that in mind, you can create and structure your content, reindex, and publish it to Experience Edge entirely using this API. So let’s take a look at how it works!

Uploading a picture to Media Library using GraphQL Authoring API

First of all, it is disabled by default, so we need to switch by setting Sitecore_GraphQL_ExposePlayground environmental variable to true. Since these variables expand at build time you also need to re-deploy the environments

Enable Api

Once deployment is complete, you can start playing with it. Security in a composable world typically works with OAuth, Authoring and Management API is not an exclusion here. In order to obtain an access token, you need to authorize it first with your client ID and client secret which you set up with XM Cloud Deploy app:

Token

There are different ways of authorization (for example, using CLI dotnet sitecore cloud login command), but since the need to fully automate the routine, I will be using /oauth/token endpoint. Also, it is worth mentioning that after getting initially already authorized with CLI, your client ID / secret pair is stored at .sitecore\user.json file so let’s take it from there. Here’s the code:

$userJson = "$PSScriptRoot/../../.sitecore/user.json"

if(-not(Test-Path$userJson)){
    Write-Error"The specified file '$userJson' does not exist."
    return
}

$userJson = Get-Content$userJson | ConvertFrom-Json
$clientId = $userJson.endpoints.xmCloud.clientId
$clientSecret = $userJson.endpoints.xmCloud.clientSecret
$authorityUrl = $userJson.endpoints.xmCloud.authority
$audience = $userJson.endpoints.xmCloud.audience
$grantType = "client_credentials"

$body = @{
    client_id = $clientId
    client_secret = $clientSecret
    audience = $audience
    grant_type = $grantType
}

$response = Invoke-RestMethod -Uri "${authorityUrl}oauth/token" -Method Post -ContentType "application/x-www-form-urlencoded" -Body $body
return $response.access_token

Now we got the access token and it should be passed as a header with every single request to GraphQL API:

“Authorization” = “Bearer <access_token>”

Next, let’s make a mutation query that returns us a pre-signed upload URL from the passing API endpoint and a target Sitecore path that you want to upload your media to. Here’s the code:

[CmdletBinding()]
Param(
    [Parameter(Mandatory=$true, HelpMessage="The URL of the endpoint where the file will be uploaded.")]
    [string]$EndpointUrl,
    [Parameter(Mandatory=$true, HelpMessage="The JWT token to use for authentication.")]
    [string]$JWT,
    [Parameter(Mandatory=$true, HelpMessage="The path of the file to be uploaded.")]
    [string]$UploadPath
)

$query = @"
mutation
{
  uploadMedia(input: { itemPath: "$UploadPath" }) {
    presignedUploadUrl
  }
}
"@

$body = @{ query = $query} | ConvertTo-Json
$headers = @{
    "Content-Type" = "application/json"
    "Authorization" = "Bearer $JWT"
}

# Invoke the GraphQL endpoint using Invoke-RestMethod and pass in the query and headers
$response = Invoke-RestMethod -Method POST -Uri $EndpointUrl -Headers $headers -Body $body
$result = $response.data.uploadMedia
return $result.presignedUploadUrl

Now that we have the pre-signed upload URL, we can perform media upload passing the local file to process:

[CmdletBinding()]
Param(
    [Parameter(Mandatory=$true, HelpMessage="The URL to upload the file to.")]
    [string]$UploadUrl,
    [Parameter(Mandatory=$true, HelpMessage="The JWT token to use for authentication.")]
    [string]$JWT,
    [Parameter(Mandatory=$true, HelpMessage="The path to the file to be uploaded.")]
    [string]$FilePath
)

if(-not(Test-Path$FilePath)){
    Write-Error"The specified file '$FilePath' does not exist."
    return
}

$result = & curl.exe --request POST $UploadUrl --header "Authorization: Bearer $JWT" --form =@"$FilePath" -s
$result = $result | ConvertFrom-Json
return $result

This script will return the details of a newly uploaded media item, such as:

  • item name
  • item full path
  • item ID

I combined all the above cmdlets into a single Demo-UploadPicture.ps1 script that cares about passing all the parameters and performs the upload operation:

Powershell

The upload immediately results in Media Library at the requested path:

Result

Pros:

  • a modern platform agnostic approach
  • works nicely with webhooks
  • allows automating pretty much everything
  • excellent management options making DevOps easier

Cons:

  • cumbersome token operations
  • doesn’t allow batching, therefore takes a request per each operation

Verdict

It is great to have a variety of different tools in your belt rather than having a single hammer in a hand with everything around turning into nails. Hope this new tool brings your automation skills to a new level!

Content Hub One full review: good, bad and ugly

Most of my readers know me as a dedicated Sitecore professional, however, those who are close to me are aware of the variety of my hobbies. Some of them also know me as a Scotch whisky expert and collector. After living for almost 15 years in the UK I got a pretty decent collection of these spirits and learned hundreds of facts from attending dozens of whisky distilleries in Scotland.

Once I got my hands on a new SaaS offering from Sitecore - Content Hub One, I decided to give it a try on a practical example and try its capabilities as I was doing a real application. What would I use for the demo purposes? Something I know much about  - that's how exposing my whisky collection was chosen. Let's go through all the way starting with content modeling, going through actual data and media authoring and publishing, and eventually creating a headless app for content delivery.

Content

First look

Once I got access to Content Hub ONE, I felt curious about what can I do using it. After logging through the portal, I got the ascetic main interface:

It exactly mimics your expected activities here: Content Types is used for Content modeling, Media - is for uploading media assets and Content is for creating content from your types and referencing uploaded media.

Content Hub One comes with handy documentation that helps understand the operations.


Content Modeling

For my purpose, I need to set up two content types - a listing type featuring items from the collection and item types itself to be used on the corresponding pages (marketers also know them as PLP and PDP).

Let's start with a Whisky type which represents the actual item from my collection. You can only choose from these basic field types:

  • Text type can be either short single-line value or multi-line long text up to 50,000 characters
  • Rich text includes markup and can take even more - 200,000 characters. It does not accept raw HTML.
  • Number, Boolean, and Date/Time are obvious and speak for themselves.
  • Reference gives the ability to link other content records to this item, with the unfortunate limit of max 10 items per field
  • Media is similar to the above with the difference that it allows referencing uploaded media items.

Unfortunately, some crucial fields are missing, such as those used for storing Links, URLs, and email addresses.

I ended up with the following structure for a Whisky item type that features as many of various field types as possible:

Next, let's create a Collection type to include a collection of items as well as some descriptive content within Rich text type:

Pay attention to the Archive field. From the home page, I want to distribute a zip archive with all 50 images of my collection, so I included this media field. The challenges of this implementation are described below.


Media

Content Hub ONE users can upload media so that it gets published to Experience Edge CDN. However, its usage is very limited to only images of GIF, JPG, PNG, and WEBP formats.

That is not sufficient for my demo purposes. I also need to upload videos of creative ads for each of my whisky items, as is referenced at Whisky type and I also want to upload a ZIP archive with all the 50 images featuring my entire collection, referenced at Collection type. This is not something extraordinary and is very common for content-powered websites.

So, the question is - can I upload archives and videos? Officially - no, you cannot. However, nothing stops you from renaming your asses to something like video.mp4.jpg or archive.zip.jpg so that it successfully passes upload validation and actually gets uploaded and later published to Edge. With 70Mb limit per media item, it can host pretty much reasonably converted videos, archives, or whatever you may want to put there.

Note: please be aware that since anything else than images isn't officially supported, you may lose access to that content once. Use it at your own risk!

Further below I will show how to build a head application that can consume such content, including "alternative" non-supported media types.


Development

There is the documentation for the developers, a good start at least.

CLI

Content Hub One comes with helpful CLI and useful documentation. It has support for docker installation, but when speaking about local installation I personally enjoy support for installing using my favorite Chocolatey package management tool:

choco install Sitecore.ContentHubOne.Cli --source https://nuget.sitecore.com/resources/v2

With CLI you execute commands against the tenants with only one active at the moment. Adding a tenant is easy, but in order to do you must provide the following four parameters:

  • organization-id
  • tenant-id
  • client-id
  • client-secret

Using CLI you can do serialization the same as with XP/XM platforms and see the difference and that is a pretty important feature here. I pulled all my content into a folder using ch-one-cli serialization pull content-item -c pdp command where pdp is my type for whisky items:

The serialized item looks as below:

id: kghzWaTk20i2ZZO3USdEaQ
name: Glenkinchie
fields:
  vendor:
    value: 'Glenkinchie '
    type: ShortText
  brand:
    value: 
    type: ShortText
  years:
    value: 12
    type: Integer
  description:
    value: >
      The flagship expression from the Glenkinchie distillery, one of the stalwarts of the Lowlands. A fantastic introduction to the region, Glenkinchie 12 Year Old shows off the characteristic lightness and grassy elements that Lowland whiskies are known for, with nods to cooked fruit and Sauternes wine along the way. A brilliant single malt to enjoy as an aperitif on a warm evening.
    type: LongText
  picture:
    value:
    - >-
      {
        "type": "Link",
        "relatedType": "Media",
        "id": "lMMd0sL2mE6MkWxFPWiJqg",
        "uri": "http://content-api-weu.sitecorecloud.io/api/content/v1/media/lMMd0sL2mE6MkWxFPWiJqg"
      }
    type: Media
  video:
    value:
    - >-
      {
        "type": "Link",
        "relatedType": "Media",
        "id": "Vo5NteSyGUml53YH67qMTA",
        "uri": "http://content-api-weu.sitecorecloud.io/api/content/v1/media/Vo5NteSyGUml53YH67qMTA"
      }
    type: Media
After modifying it locally and saving the changes, it is possible to validate and promote these changes back to Content Hub ONE CMS. With that in mind, you can automate all the things about for your CI/CD pipelines using PowerShell, for example. I would also recommend watching this walkthrough video to familiarize yourself with Content Hub ONE CLI in action.


SDK

There is a client SDK available with the support of two languages: JavaScript and C#. For the sake of simplicity and speed, I decided to use C# SDK for my ASP.NET head application. At a first glance, SDK looked decent and promising:

And quite easy to deal with:

var content = await _client.ContentItems.GetAsync();

var collection = content.Data
    .FirstOrDefault(i => i.System.ContentType.Id == "collection");

var whiskies = content.Data
    .Where(i => i.System.ContentType.Id == "pdp")
    .ToList();
However, it has one significant drawback: the only way to get media content for use in a head application is via Experience Edge & GraphQL. After spending a few hours troubleshooting and doing various attempts I came to this conclusion. Unfortunately, I did not find anything about that if the documentation. In any case with GraphQL querying Edge my client code looks nicer and more pretty, with fewer queries and fewer dependencies. The one and only dependency I got for this is a GraphQL.Client library. The additional thing to add for querying Edge is setting X-GQL-Token with a value, you obtain from the Settings menu.

The advantage of GraphQL is that you can query against the endpoints specifying quite complex structures of what you want to get back as a single response and receive only that without any unwanted overhead. I ended up having two queries:

For the whole collection:

{
  collection(id: ""zTa0ARbEZ06uIGNABSCIvw"") {
    intro
    rich
    archive {
        results {
        fileUrl
        name
        }
    }
    items{
    results{
        ... on Pdp {
        id
        vendor
        brand
        years
        description
        picture {
            results {
            fileUrl
            name
            }
            }
        }
      }
    }
  }
}

And for specific whisky record item requested from a PDP page:

{
  pdp(id: $id) {
    id
    vendor
    brand
    years
    description
    picture {
        results {
        fileUrl
        name
          }
        }
    video {
        results {
        fileUrl
        name
            }
        }
    }
}

The last query results get easily retrieved in the code as:

var response = await Client.SendQueryAsync<Data>(request);
var whiskyItem = response.Data.pdp;

 

Some challenges occur at the front-end part of the head application. 

When dealing with Rich text fields you have to come up with building your own logic (my inline oversimplified example, lines 9-50) for rendering HTML output from a JSON structure you got for that field. The good news is that .NET gets it nicely deserialized so that you can at least iterate through this markup:

Sitecore provided an extremely helpful GraphQL IDE tool for us to test and craft queries, so below is how the same Rich text filed value looks in a JSON format:

You may end up wrapping all clumsy business logic for rendering Rich text fields into a single Html Helper producing HTML output for the entire Rich text field, which may accept several customization parameters. I did not do that as it is labor-heavy, but for the sake of example, produced such a helper for Long Text field type:

public static class TextHelper
{
    public static IHtmlContent ToParagraphs(this IHtmlHelper htmlHelper, string text)
    {
        var modifiedText = text.Replace("\n", "<br>");
        var p = new TagBuilder("p");
        p.InnerHtml.AppendHtml(modifiedText);
        return p;
    }
}

which can be called from a view as:

@Html.ToParagraphs(Model.Description)


Supporting ZIP downloads

On the home page, there is a download link sitting within Rich text content. This link references a controller action that returns zip archive with the correct mime types.

public async Task<IActionResult> Download()
{
    // that method id overkill, ideally
    var collection = await _graphQl.GetCollection();

    if (collection.Archive.Results.Any())
    {
        var url = collection.Archive.Results[0].FileUrl;
        var name = collection.Archive.Results[0].Name;
        name = Path.GetFileNameWithoutExtension(name);

        // gets actual bytes from ZIP binary stored as CH1 media
        var binaryData = await Download(url);
        if (binaryData != null)
        {
            // Set the correct MIME type for a zip file
            Response.Headers.Add("Content-Disposition", $"attachment; filename={name}");
            Response.ContentType = "application/zip";

            // Return the binary data as a FileContentResult
            return File(binaryData, "application/zip");
        }
    }

    return StatusCode(404);
}


Supporting video

For the sake of a demo, I simply embedded a video player to a page and referenced the URL of published media from CDN:

<video width="100%" style="margin-top: 20px;" controls>
    <source src="@Model.Video.Results[0].FileUrl" type="video/mp4">
    Your browser does not support the video tag.
</video>


Bringing it all together

I built and deployed the demo at https://whisky.martinmiles.net. You can also find the source code of the resulting .NET 7 head application project at this GitHub link.

Now, run it in a browser. All the content seen on a page is editable from Content Hub ONE, as modeled and submitted earlier. Here's what it looks like:

Criticism

Content Hub ONE developers did a great job in the shortest time and there should not be any questions to them. However, from my point of view, there is a big number of both minor and major issues that prevent using this platform in its current stage for commercial usage. Let's take a look at them.

1. Lack of official support for media items other than four types of images is a big blocker. Especially given that there is no technical barrier to doing that in principle. Hopefully, that gets sorted with time.

2. Many times while working with CH1 I got phantom errors, without understanding the cause. For example, I want to upload media but got Cannot read properties of undefined (reading 'error') in return. Later I realized that was caused by session expiration, which for some reason is not handled well in the cases. What is more frustrating - I got these session issues even after just navigating the site, as if navigation did not reset the session expiration timer. But since that is SaaS product - it's only my guesses without having access to internals.

3. Another issue experienced today was CH1 got down with UI showing me a Failed to fetch error. That also occurred with my cloud-deployed head app which also failed to fetch content from CH1. Unannounced/planned maintenance?

4. Not being able to reference more than 10 other records seriously limits platform usage. With my specific example, I had around 50 items of whisky to be exposed through this app but was able to include only max 10 of them. What is worse - there are no error messages around it or UI informing me about the limitation in any other way.

5. When playing around with the existing type I cannot change the field type, and that limitation is understood. The obvious solution would be deleting that field instead and regrating it with the same name but another type (let's assume there's no content to be affected). Wrong, that's not possible and eds with Failed entity definition saving with name: 'HC.C.collection' error. You can only recreate the field with a new name, not the same one you've just deleted. If you got lots of queries in your client code - need to locate them and update them correspondingly.

6. Not enough field types. For example, URL could be simply placed into a small text field, but without proper validation, editors may end up having broken links if they put a faulty URL value on a page.


There is some of UI/UX to be improved

1. Content Hub One demands more clicks for content modeling creation compared to let's say XP. For example, if you publish a content item, related media does not get published automatically. You need manually click through media, locate it and publish explicitly. On large volume of content that annoys and adds unwanted labor.

2. To help with the above, why not add a "Publish" menu item into the context menu upon an uploaded item in a Draft state? That eliminates the unwanted step of clicking into the item for publishing.

3. On the big monitors the name of a record is mislocated in the top left corner making it unobvious to edit it, given that, it is not located with a form field, so not immediately obvious that is editable. That is especially important for records that are not possible to rename after creation. Bringing the name close to the other fields would definitely help!

4. Lack of drag&drop. It would be much easier to upload media by simply dragging the files onto a media listbox, or any other reasonable control.

5. Speaking about media, UI does not support selecting multiple files for an upload. Users have to click one after another.

6. Need better UI around grouping and managing assets. Currently, there are facets but need something more than that, maybe the ability to group records into folders. I don't have a desired view on that, but definitely see the need for such a feature, as my ultra-simple demo case already requires navigational effort.


Conclusion

I don't want to end up with the criticism only leaving a negative impression about this product: there are plenty of positives as well. I would only mention just a few: decent SDKs, attention to the details where the feature is actually implemented (like the order of referenced items follows up the order you select them), nice idea of a modern asynchronous UI that can notify you about when the resource gets published to Edge (just need to sort out the session expiration issues).

Content Hub One is definitely in the early stages of its career. I would wish the development team and product managers to eventually overcome the "child sickness" stage of the product and deliver us a lightweight but reasonably powerful headless CMS that will speed up content modeling and content delivery experience. The foot is already in the door, so the team needs to push on it!