Работа с API службы управляемых метаданных SharePoint с помощью Node.js

Программный интерфейс REST API в SharePoint решает массу задач автоматизации в SharePoint и является универсальным, платформанезависимым, однако зачастую можно "встрять" в ситацию ограничений и отсутствия нужной функциональности в REST API, которая есть в других объектных моделях (CSOM/JSOM). Не проблема, когда исполняемый код является частью клиентского приложения на странице SharePoint, т.к. отсутствующий в REST метод может быть в JSOM API. Но что, если контекст исполнения - не страница в браузере? Если код запускается не в браузере, самое очевидное использовать C# CSOM или PowerShell c SSOM или CSOM. Но что, если среда исполнения и используемый язык ограничены и .Net не вариант?

В данной статья я хочу рассмотреть возомжности Node.js и JavaScript "на сервере". А задачей будет обеспечить базовый слой функциональности для работы с Managed Metadata (Taxonomy). Необходимо реализовать, например, следующие методы и при этом не использовать дополнительных веб-сервисов на .Net:

  • Получение дочерних "термов" (term) в коллекции термов
  • Получение дочерних термов в родительском
  • Получение экземпляра терма
  • Добавление нового и обновление существующего терма
  • Пометка терма, как устаревшего
  • Получение всего набора термов в коллекции
  • ...

Самым простым решением для реализации большинства операций может быть SOAP сервис. Да, SOAP сервисы устарели и попахивают временами динозавров, но, тем не менее, это как вариант, работают же.

Для работы с MMD есть сервис таксономии /_vti_bin/TaxonomyClientService.asmx. Давайте взглянем на его возможности. Сервис предоставляет следующие методы:

  • AddTerms
  • GetChildTermsInTerm
  • GetChildTermsInTermSet
  • GetKeywordTermsByGuids
  • GetTermSets
  • GetTermsByLabel

Для того, чтобы вызвать "мыльные методы" в контексте Node.js можно воспользоваться любым модулем для http запросов, получать авторизационный cookie и вставлять в заголовки запросов. К счастью, уже существуют библиотеки, которые абстрагируют нюансы аутентификации и транспортный уровень. Я говорю о sp-request с node-sp-auth.

С использованием sp-request можно легко обращаться к SOAP службе, поменяв заголовки для работы с XML и отключив JSON. Примерно так:

const baseUrl = 'https://contoso.sharepoint.com/sites/site';
let authObject = { ... }; // формат авторизации node-sp-auth, см. описание модуля
let request = require('sp-request').create(authObject);
let headers = {};

let soapBody = `
   ... // XML тело SOAP-запроса
`;

headers['Accept'] = 'application/xml, text/xml, */*; q=0.01';
headers['Content-Type'] = 'text/xml;charset="UTF-8"';
headers['X-Requested-With'] = 'XMLHttpRequest';
headers['Content-Length'] = soapBody.length;

request.post(baseUrl + '/_vti_bin/TaxonomyClientService.asmx', {
  headers: headers,
  body: soapBody,
  json: false
})
  .then(response => {
    // Proceed the responce object
  })
  .catch(err => console.log(err));

Разобраться с форматом SOAP-пакетов и нюансами, которые не всегда фигурируют в описании, иногда бывает непросто. Но в итоге это всего лишь SOAP и в интернете много материалов по разным методам, тут можно смело абстрагироваться от Node.js как такового.

В ходе потребностей нескольких проектов мы обернули нужные нам методы в виде библиотеки NPM с открытым исходным кодом sp-screwdriver. Данная библиотека, по большей степени, - пример, т.к. она не оптимизирована для массового использования и формат методов, получающих XML и транслирующих JSON может поменяться.

С SOAP разобрались. Но далее другая незадача. Интерфейс SOAP, как минимум для MMD, крайне ущербен и нефункционален, многих нужных операций просто напросто нет. Что насчет редактирования существующих термов, что насчет запроса всех термов, удаления? К счастью, дело тут решенное. Мы применяем отчасти хак, но эффективный и позволяющий безприпятственно использовать всю мощь CSOM/JSOM. Подход следующий:

  • Пишется код на JSOM, выполняющий нужные действия
  • Код исполняется в родной для него среде в браузере и мониторятся сетевые запросы в fiddler
  • В списке запросов ищется отправленные в client.svc (/_vti_bin/client.svc/ProcessQuery)
  • Из тела запроса берется XML пакет, парсится и оборачивается в функцию на JavaScript для Node.js

Например, запрос по изменению наименования терма, выглядит так:

<Request xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009" 
      SchemaVersion="15.0.0.0" LibraryVersion="15.0.0.0" 
      ApplicationName="Javascript Library">
    <Actions>
        <SetProperty Id="166" ObjectPathId="157" Name="Name">
            <Parameter Type="String">{{ newName }}</Parameter>
        </SetProperty>
    </Actions>
    <ObjectPaths>
        <StaticMethod Id="146" 
           Name="GetTaxonomySession" 
           TypeId="{981cbc68-9edc-4f8d-872f-71146fcbb84f}" />
        <Property Id="149" ParentId="146" Name="TermStores" />
        <Method Id="151" ParentId="149" Name="GetByName">
            <Parameters>
                <Parameter Type="String">{{ serviceName }}</Parameter>
            </Parameters>
        </Method>
        <Method Id="154" ParentId="151" Name="GetTermSet">
            <Parameters>
                <Parameter Type="String">{{ termSetId }}</Parameter>
            </Parameters>
        </Method>
        <Method Id="157" ParentId="154" Name="GetTerm">
            <Parameters>
                <Parameter Type="String">{{ termId }}</Parameter>
            </Parameters>
        </Method>
    </ObjectPaths>
</Request>

Немного по формату:

  • Версия схемы изменена на 15.0.0.0 это обеспечит обратную совместимость с On-Premises SharePoint версии 2013.
  • Часть лишнего кода вычищается. Глядя на пакет, скопированный из fiddler, в принципе ясно-понятно, что лишнее.
  • ИД-шники отражают последовательность, объеденяя блоки в цепочку методов клиентской объектной модели. От запроса к запросу они, естественно, разные, но, если их оставить такими же, как сформировались на момент отслеживания fiddler'ом, при повторении работать будет. За время эксплуатации месяц ошибок замечено не было. Так и порешили фиксировать статические идентификаторы.
  • Параметры в {{ фигурныхСкобках }} - это динамическая часть, которая меняется от запроса к запросу методом на JS. В отправляемом пакете, естественно, никаких {{ }} нет, это артифакт для Handlebars, который задействован в примере.

Примеры реализации getAllTerms, setTermName и deprecateTerm можно посмотреть в sp-screwdriver.

После того, как "проксирущие" методы реализованы, задача превращается в тривиальную, т.к. обращаться к такому API уже можно привычным смособом, например:

let Screwdriver = require('sp-scredriver');
let context = require('./path_to_private_settings');
let screw = new Screwdriver(context);

let data = {
    baseUrl: context.siteUrl,
    serviceName: config.mmd.serviceName,
    termSetId: config.mmd.termSetId,
    properties: [
        'Id', 'Name', 'Description', 'CustomProperties',
        'IsRoot', 'IsDeprecated', 'PathOfTerm',
        'IsAvailableForTagging', 'Parent'
    ]
};

screw.mmd.getAllTerms(data)
    .then(response => {
        let results = JSON.parse(response.body);
        console.log("Response:", results);
    })
    .catch(err => console.log('Error:', err.message));

Для себя я нашел очень полезным решением обращаться к JSOM API и изредка SOAP методам из Node.js. Такой подход на текущий момент задействован в ряде проектов в виде Web Job'ов, обрабатывающих срез данных, включая MMD, в ходе пакетной обработки и трансформации данных.

Так же данный подход обоснованно применен в одном из недавших решений с десктоп клиентом на Electron под macOS без необходимости дополнительных веб-сервисов.

Ни в коем случае не хочу перетягивать одеяло для решения аналогичных задач на .Net. Однако замечу, что есть ряд сценариев, где Node.js или любая другая платформа могут быть объективно необходимы, тогда подобного рода взаимодействие с SharePoint может быть весьма оправдано.


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