List Items System Update options in SharePoint Online

In this article, I expose some options for updating SharePoint list items in system modes, aka "System Update". Those who have been in SharePoint grounds for a while and especially who came from On-Premises are familiar with System Update from the server-side object model. SSOM is not touched in the article and even CSOM .Net will be skipped, the reading is all about JavaScript.

A few words about System Update concept in general. System update provides a way of updating items' metadata skipping new versions creations as well as keeping last modified by authorship and not changing previously modified timestamp. The system update also effects on triggering business logic and changes API results.

The use cases for system update are different scenarios with data migration, business processes and workflows, demo data preparation, mass-update scripts, and others.

The problem at hand is that client-side APIs has some limitations or, as I prefer more, nuances.

As mentioned before, system update has been there for ages in SSOM API, that’s On-Prem and not our case at all. System update was introduced into CSOM .Net API introduced in 16.1.5626.1200 build in August 2016. And not far ago it landed to JSOM library in SharePoint Online too.

Currently, in SPFx and Serverless era it’s not fashionable anymore using JSOM and loading heavy "sp.js" and all that stuff, so we consider other options like raw CSOM requests with crafted XML packages and REST as a default API when possible.

Native JSOM

const ctx = SP.ClientContext.get_current();

const list = ctx.get_web().get_lists().getByTitle('My list');
const item = list.getItemById(1);

item.set_item('Title', `Updated with JSOM request, ${new Date().toISOString()}`);
// item.update(); // -> creates new version, updates Editor and modified date
// item.updateOverwriteVersion(); // -> creates no new version, updates Editor and modified date
item.systemUpdate(); // -> creates no new version, Editor and modified date remains untouched

ctx.executeQuery(success, failure);

Full sample

JSOM via raw XML

Ok, JSOM is good, but it requires sp.js libraries to be loaded. Sometimes we’re in a context when limited with that. Yet, there is an option calling CSOM API without native SP libraries.

const listRelativeUrl = '/sites/site/Lists/MyList';
const itemId = 1;

const body = `
  <Request xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009" SchemaVersion="15.0.0.0" LibraryVersion="16.0.0.0" ApplicationName="Javascript Library">
    <Actions>
      <Method Name="SetFieldValue" Id="4" ObjectPathId="3">
        <Parameters>
          <Parameter Type="String">DataField01</Parameter>
          <Parameter Type="String">Updated with raw JSOM XML request, ${new Date().toISOString()}</Parameter>
        </Parameters>
      </Method>
      <Method Name="SystemUpdate" Id="5" ObjectPathId="3" />
    </Actions>
    <ObjectPaths>
      <Property Id="1" ParentId="0" Name="Web" />
      <Method Id="2" ParentId="1" Name="GetList">
        <Parameters>
          <Parameter Type="String">${listRelativeUrl}</Parameter>
        </Parameters>
      </Method>
      <Method Id="3" ParentId="2" Name="GetItemById">
        <Parameters>
          <Parameter Type="Number">${itemId}</Parameter>
        </Parameters>
      </Method>
      <StaticProperty Id="0" TypeId="{3747adcd-a3c3-41b9-bfab-4a64dd2f1e0a}" Name="Current" />
    </ObjectPaths>
  </Request>
`;

const endpoint = `${siteUrl}/_vti_bin/client.svc/ProcessQuery`;
const client = new SPHttpClient();

client.post(endpoint, {
  headers: {
    'Accept': '*/*',
    'Content-Type': 'text/xml;charset="UTF-8"',
    'X-Requested-With': 'XMLHttpRequest'
  },
  body
})
  .then(r => r.json())
  .then(r => {
    if (r[0].ErrorInfo) {
      throw new Error(r[0].ErrorInfo.ErrorMessage);
    }
    return r;
  });

Full sample

A raw HTTP request to client process "svc" endpoint can be used to send XML packages which are usually prepared with JSOM client library and are sent over the wires on execute query method. It’s not the most pleasant job to craft such XML bodies manually, but it shows the idea and possibility.

System update via REST API

Another option, which sounds much better for my ear is, of course, REST API. There is no "systemUpdate" in REST, at least yet. But, we can do something very close to what we need.

import { Item, ListItemFormUpdateValue, PermissionKind } from '@pnp/sp';
import { format } from 'date-fns';

export const dateToFormString = (dateTime: Date | string): string => {
  return format(dateTime, 'M/D/YYYY h:m A');
};

export const loginToFormString = (userName: string): string => {
  return JSON.stringify([{ Key: userName }]);
};

export const systemUpdate = async (item: Item, formUpdateValues: ListItemFormUpdateValue[]) => {

  const permissions = await item.getCurrentUserEffectivePermissions();
  if (!item.hasPermissions(permissions, PermissionKind.ManagePermissions)) {
    throw new Error('403 - Access denied. Full Control permissions level is required for performing this operation.');
    // Having write permission but not managing, the method will ignore Editor and Modified values and update them with actual data
  }

  const { Author: { Name }, Created: Modified } = await item.select('Created,Author/Name').expand('Author').get();

  const sysUpdateData = [
    { FieldName: 'Editor', FieldValue: loginToFormString(Name) },
    { FieldName: 'Modified', FieldValue: dateToFormString(new Date(Modified)) }
  ];

  const result = await item.validateUpdateListItem(formUpdateValues.concat(sysUpdateData), true);

  const errors = result.filter(field => field.ErrorMessage !== null);
  if (errors.length > 0) {
    throw new Error(JSON.stringify(errors));
  }

  return result;
};

This example updates item using nothing else but REST API. REST API "validateUpdateListItem" method creates no version on change. And if provide Editor and Modified fields with previously existed values the result will be close to system update.

Permissions wise, JSOM's system update requires Editor permissions, but you have to have Full Control over the items to do the same in REST. Which is definitely a nuance but we can live with that.

Such scripts can be used in client-side tools for admins, for example as SharePoint Framework web parts or can be consumed as Azure Functions when a team prefers JS over .Net for some reasons. Also, raw XML JSOM and REST are supported in Microsoft Flows and can be used even without Azure hosted code.

Field data types fingerprints for validateUpdateListItem

With "validateUpdateListItem" all major field data types update is supported, yet it can be difficult sometimes to find the correct format. So as a bonus section:

.validateUpdateListItem([
  // Text field (single line and note)
  { FieldName: 'TextField', FieldValue: '123' },
  // Number field
  { FieldName: 'NumberField', FieldValue: '123'  },
  // Yes/No field
  { FieldName: 'YesNoField', FieldValue: '1' /* Yes, No, 1, 2 */ },
  // Person or group, single and multiple
  { FieldName: 'PersonField', FieldValue: JSON.stringify([{ Key: LoginName }]) },
  // Dates should be in in the following formats
  { FieldName: 'DateTimeField', FieldValue: '6/23/2018 10:15 PM' },
  { FieldName: 'DateField', FieldValue: '6/23/2018' },
  // Choice field (single and multi-valued)
  { FieldName: 'ChoiceField', FieldValue: 'Choice 1' },
  { FieldName: 'MultiChoiceField', FieldValue: 'Choice 1;#Choice 2' },
  // Hyperlink or picture (after URL a description can go after ', ' delimeter)
  { FieldName: 'HyperlinkField', FieldValue: 'https://arvosys.com, ARVO Systems' },
  // Lookups fields (single and multi-valued)
  { FieldName: 'LookupField', FieldValue: '2' /* Item ID as string */ },
  { FieldName: 'MutliLookupField', FieldValue: [3, 4, 5].map(id => `${id};#`).join(';#') },
  // Mamnaged metadata fields (single and multi-valued)
  { FieldName: 'SingleMMDField', FieldValue: 'Department 2|220a3627-4cd3-453d-ac54-34e71483bb8a;' },
  { FieldName: 'MultiMMDField', FieldValue: 'Department 2|220a3627-4cd3-453d-ac54-34e71483bb8a;Department 3|700a1bc3-3ef6-41ba-8a10-d3054f58db4b;' }
]);

Happy coding!