Working with SharePoint MMD in Node.js

SharePoint REST API is nice, powerful and universal to deal with, but sometimes you face barriers and recall that the desired API is absent in REST, yet, at the same time, is a part of CSOM/JSOM. It's not an issue when your code lives in a context of a page, but what if not? C# CSOM will be the most obvious answer probably. But what if you are limited in choice language to use or a runtime environment, in other words, when .Net is not an option?n?

In a context of this story, the limitations are Node.js and JavaScript. And the task is to own a basic layer of functionality for manipulating Managed Metadata (Taxonomy), such options as:

  • Get child terms in a term set
  • Get child terms in a parent term
  • Get a specific term
  • Add new term
  • Update a term
  • Deprecate a term
  • Get all terms (for relatively small dictionaries)

The easiest solution for some basic operations can be SOAP services. Yes, SharePoint SOAP services are deprecated and they smell like mammoth fossils. But still there and work.

There is a SOAP service for dealing with MMD /_vti_bin/TaxonomyClientService.asmx. Let's take a look at it. The service represents the following methods:

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

One can consume these in Node.js using almost any http request module with authentication cookie encapsulation into request headers. Likely, there is no need to figure out in low level transport, all these authentication and SharePoint requests libraries exist and are here in our disposal, abstracting scary moments away. I'm describing sp-request with node-sp-auth.

sp-request module can easily talk to SharePoint SOAP service like this:

const baseUrl = 'https://contoso.sharepoint.com/sites/site';  
let authObject = { ... }; // node-sp-auth authentication format  
let request = require('sp-request').create(authObject);  
let headers = {};

let soapBody = `  
   ... // XML SOAP body for a specific method
`;

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

It can be tricky to figure out with SOAP packages for some methods sometimes, some SOAP operations descriptions are not perfect. Good thing is that any related to SharePoint SOAP services information also is relevant for Node.js use cases.

I wrapped some methods in an library with the name sp-screwdriver. It is not for production purposes as-is, it can act as a start point or example how to consume APIs.

Here we're standing in front of another wall, those SOAP interfaces are sometimes featureless and offer only a limited number of actions, it's true with MMD at least. What about editing existing terms, how to request all them in a single call, how to do more?

Here I use a little hack, which includes the following workflow:

  • Create the desired operation in JSOM
  • Run it in the browser monitoring actual request body in fiddler
  • Parse a package which was generated and sent to /_vti_bin/client.svc/ProcessQuery
  • Re-create the package for re-usability and wrap it inside a Node.js method talking to client.svc

For example, request, to set term name, looks like:

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

Some comments to the format:

  • Library version was changed to 15.0.0.0 to support backwards compatibility with On-Premises SharePoint 2013, as well as SharePoint 2016 and Online.
  • Some redundant tags should be deleted from the package received in fiddler (it's almost clear from the start what is redundant when observing the package).
  • IDs represent the flow or chain and reflect the CSOM sequence. They are different from a request to request, but if to freeze them static and request again and again request works. So I decided to leave IDs static.
  • Parameters in {{ doubleCurlyBraces }} are dynamic, they replaced in runtime with actual values by JavaScript code. Package for sure should not include any {{ }}. It's an artifact of Handlebars templating engine used.

getAllTerms, setTermName and deprecateTerm example wrappers can be found in sp-screwdriver library sources on GitHub.

After the wrapper methods are implemented, the usage can be straightforward:

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

For myself, it was a nice way extending Node.js solutions capabilities to use not only the REST services but CSOM too. The approach was used on a couple of projects with Web Jobs, proceeding information in SharePoint, including MMD.

Also, working with MMD directly within Node.js code was viable in a recent Electron application I got to develop.

The article doesn't try to say that anyone should use Node.js for similar purposes as I do, as the same can be done in .Net, actually it should. But, at the same time, there are cases where it is nice to have an ability of extending server-side JavaScript to consume not only the REST API, but potentially any CSOM/JSOM functionality.