Возможности системного обновления (System Update) элементов списка в SharePoint Online

В данной статье я хочу рассмотерть основные возможности систмного обновления элементов списков в SharePoint Online. Кто давно работает в SharePoint и особенно работал с On-Premises очевидно, что такое Syatem Update. Системное обновление присутствует в объектной модели долгое время, изначально оно было только составом SSOM, не так давно появилось и в CSOM. Однако, в рамках статьи я не буду затрагивать .Net вовсе.

Пару слов о концепте системного обновления. Системное обновление элементов позволяет вносить изменения в метаданные минуя создание новой версии и оставляя информацию о последнем редактировании (пользователь и дата изменения) не тронутой. Так же системное обновление иначе учитывается в различных триггерах событийной модели.

Типовыми сцениариями, когда необходимо системное обновление является миграция данных, обогащение данных на основе бизнес-процессов, подготовка демо-данных, массового обновление, пр.

Теперь к проблеме. Она заключается в том, API имеет свои нюансы и ограничения.fer more, nuances. Как я упомянул ранее, системное обновление было очень долгое время только в составе SSOM, а SSOM у всех уважающих себя разработчиков SharePoint под запретом и в нему прибегать нужно только в самом последнем случае, а в контекте SharePoint Online так вообще, серверная объектная модель не доступна. В составе CSOM системное обновление было добавлено в версии 16.1.5626.1200 вышедшей в августе 2016. А не так давно SystemUpdate так же появился и в JSOM в SharePoint Online.

Сейчас, в эпоху SPFx и Serverless решений мало кто в принципе использует JSOM, который требует загрузку тяжелого "sp.js" и других скриптов, а часто и вовсе среда исполнения не предполагает такую возможность. Поэтому мы рассматриваем другие варианты, такие как низкоуровневые CSOM пакеты или же варианты на REST API.

Пример на 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(); // -> создастся новая версия, кем изменено и дата изменения обновятся
// item.updateOverwriteVersion(); // -> новая версия не создастся, кем изменено и дата изменения обновятся
item.systemUpdate(); // -> новая версия не создастся, кем изменено и дата изменения останутся без изменений

ctx.executeQuery(success, failure);

Пример целиком

JSOM через низкоуровневый запрос XML

Ладно, но JSOM требует загрузку sp.js и других библиотек. Не всегда это возможно или целесообразно. Однако с CSOM можно работать и без клиентских библиотек:

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;
  });

Пример целиком

Выполнив HTTP запрос на "/_vti_bin/client.svc/ProcessQuery" эндпойн содержащий низкоуровневый XML пакет завершится результатом, аналогичным с тем, когда используется JSOM, но без лишних библиотек. Конечно же, это на самое веселое занятие крафтить подобные XMLки вручную, но иногда полезно отследить аналогичное действие, которое выполняется в библиотеке и повторить его за рамками.

Ситемное обновление с помощью REST API

В качестве альтернативы, с которой гораздо проще работать, выступает REST. Однкпо проблема, в REST API нет "systemUpdate", как минимум пока что. Но есть действие, результат которого очень приближен к тому, что нам требуется.

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.');
    // При наличии прав на редактирование, но отсутствие прав управления, значение Editor и Modified будут проигнорированы API
    // и обновлены на актуальные, поэтому явно проверяет наличие прав на управление
  }

  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;
};

Данный пример обновляет элемент на чистом REST. Метод "validateUpdateListItem" не создает новой версии. Если предоставить значения для полей Editor и Modified, которые были на момент изменения и обладать правами управления списком, то результат будет почти сравним с системным обновлением.

С точки зрения прав, через CSOM достаточно прав редактора, но требуется подный доступ (права менеджера) для аналогичного действия в REST. Это конечно же нюанс, но допустимый.

Применение вполне разнообразное от админских интерфейсов без сервер-сайд обработчиков, например реализованных с помощью SPFx, до Azure Functions с кодом на JS, в случаях если нужна кросплатформенность или команда предпочитает .Net'у TypeScript. Так же, низкоуровневые HTTP запросы можно выполнять в декларативных средах, например в Microsoft Flow при этом не требуется даже бэкенда.

Формат полей для validateUpdateListItem

С помощью метода "validateUpdateListItem" можно задавать значения полей всех основных типов данных, однако, не всегда очевидно, в каом формате должны идти значения, т.к. они отличаются от привычных в OData:

.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;' }
]);

Удачной разработки!


Опубликовано: 18.02.2019
Автор: Андрей Кольтяков