Mark Gibbons
Published on

Sitecore GraphQL — Upserting Items

Authors

The new GraphQL API that is shipped as part of the Sitecore Headless Services offering from Sitecore is a game changer. It is a replacement for the decades old Item Rest API. For new development I’d highly recommend it. A good starting point to get your head around it: https://graphql.org/learn/

It comes with GraphQL mutations to create, update, and delete items. And of course querying items directly, as well as querying via the search indexes. In my case I wanted to be able to continuously create / update items which may or may not already exist. This is acheivable by doing a query/search for the existing item and then either call create / update as necessary. But why not cut out the middleman and add an upsert function which can do that hard work for us?

Step 1: Some code.

I decompiled the Sitecore.Services.GraphQL.Content.Mutations.CreateItemMutation and the Sitecore.Services.GraphQL.Content.Mutations.UpdateItemMutation and put it together like this:

UpsertItemMutation.cs
using System;
using System.Collections.Generic;
using GraphQL;
using GraphQL.Types;
using Sitecore;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Globalization;
using Sitecore.Services.GraphQL.Content;
using Sitecore.Services.GraphQL.Content.GraphTypes;
using Sitecore.Services.GraphQL.Content.Mutations;
using Sitecore.Services.GraphQL.Schemas;

namespace Feature.JssExtensions.GraphQL.Mutations
{
    public class UpsertItemMutation : RootFieldType<ItemInterfaceGraphType, Item>, IContentSchemaRootFieldType
    {
        private readonly FieldTypeToJsonTypeMapper _fieldTypeToJsonTypeMapper;

        public Database Database { get; set; }

        public UpsertItemMutation(FieldTypeToJsonTypeMapper fieldTypeToJsonTypeMapper)
            : base("upsertItem", "Create / update a Sitecore item")
        {
            Assert.ArgumentNotNull(fieldTypeToJsonTypeMapper, "fieldTypeToJsonTypeMapper");
            _fieldTypeToJsonTypeMapper = fieldTypeToJsonTypeMapper;
            base.Arguments = new QueryArguments(new QueryArgument<NonNullGraphType<StringGraphType>>
            {
                Name = "name",
                Description = "The name of the item to create / update"
            }, new QueryArgument<NonNullGraphType<StringGraphType>>
            {
                Name = "template",
                Description = "The template full path or ID of the template the item is based on"
            }, new QueryArgument<NonNullGraphType<StringGraphType>>
            {
                Name = "parent",
                Description = "The parent item path or ID to add the item under"
            }, new QueryArgument<StringGraphType>
            {
                Name = "language",
                Description = "The language to create the item in (defaults to context)"
            }, new QueryArgument<ListGraphType<NonNullGraphType<FieldValueInputType>>>
            {
                Name = "fields",
                Description = "The fields to set on the item. Optional."
            });
        }

        protected override Item Resolve(ResolveFieldContext context)
        {
            string name = context.GetArgument<string>("name");
            string template = context.GetArgument<string>("template");
            string parent = context.GetArgument<string>("parent");
            string languageName = context.GetArgument<string>("language");
            List<FieldValue> fields = context.GetArgument<List<FieldValue>>("fields");
            if (string.IsNullOrWhiteSpace(languageName))
            {
                languageName = Context.Language.Name;
            }
            if (!Language.TryParse(languageName, out var language))
            {
                throw new InvalidOperationException("Language value " + languageName + " was not a valid language.");
            }
            if (!ItemMutationHelper.TryResolveTemplate(Database, template, language, null, out var templateItem))
            {
                throw new InvalidOperationException("The template '" + template + "' was not a valid template path or ID, or you did not have access to read it.");
            }

            var path = $"{parent}/{name}";
            if (!IdHelper.TryResolveItem(Database, path, language, null, out var item))
            {
                try
                {
                    ItemMutationHelper.SetFields(item, fields, _fieldTypeToJsonTypeMapper);
                    return item;
                }
                catch (Exception ex)
                {
                    context.Errors.Add(new ExecutionError(ex.Message ?? ""));
                    throw;
                }
            }

            if (!IdHelper.TryResolveItem(Database, parent, language, null, out var parentItem))
            {
                throw new InvalidOperationException("The parent item '" + parent + "' was not a valid item path or ID, or you did not have access to read it.");
            }

            Item newItem = null;
            try
            {

                newItem = parentItem.Add(name, templateItem);
                if (newItem.Language != language)
                {
                    newItem = Database.GetItem(newItem.ID, language);
                }
                ItemMutationHelper.SetFields(newItem, fields, _fieldTypeToJsonTypeMapper);
                return newItem;
            }
            catch (Exception)
            {
                newItem?.Delete();
                throw;
            }
        }
    }
}
config.config
<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:role="http://www.sitecore.net/xmlconfig/role/">
  <sitecore>
    <api>
      <GraphQL>
        <defaults>
          <content>
            <schemaProviders>              
              <systemContent type="Sitecore.Services.GraphQL.Content.ContentSchemaProvider, Sitecore.Services.GraphQL.Content">               
                <mutations hint="raw:AddMutation">
                  <mutation name="upsertItem" type="Feature.JssExtensions.GraphQL.Mutations.UpsertItemMutation, Feature.JssExtensions" resolve="true"/>
                </mutations>                
              </systemContent>
            </schemaProviders>
          </content>
        </defaults>
      </GraphQL>
    </api>
  </sitecore>
</configuration>

Source: gist

Step 2: Try it out

Sitecore Headless also ships with the GraphQL UI Playground where you can send your queries https://sc.dev.local/sitecore/api/graph/items/master/ui

It generates all the schema docs automatically. So after deploying our new mutation — here it is ready to use!