Возможности системного обновления (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
Автор: Андрей Кольтяков