This Article has been rewritten to fit the new nuxeo-js-client.

Code

Last week we were asked if there is an easy way to rename a bunch of documents, located in some specific folders, in our intranet. After manually updating a lot of main files, the documents titles were no longer synced with the main file name. The question was how to update the document title to match the main file name.

As "eating your own dog food" is quite a good thing, let's use our own REST API and clients to do this.

I have chosen to use the node.js Nuxeo client. You can find documentation in the README on the nuxeo-js-client GitHub repository.

The full project for this blog post is available here.

Let's see, step by step, how we can do this.

Project Initialization


First, you need to initialize your node.js project through npm init:

$ npm init

Answer all questions and you're ready to go!

Then, add the nuxeo node module to your project:

$ npm install nuxeo

This will install the latest version of the node.js nuxeo module (at the time of writing, it's the 0.2.0 version).

Create the Client


Create an empty file in your project, named sample.js for instance.

First, we need to require the nuxeo module so that we can use it:

let Nuxeo = require('nuxeo');

From here, we can create a new Client object targeting a Nuxeo Platform server:

let nuxeo = new Nuxeo({
baseURL: 'http://localhost:8080/nuxeo/',
auth: {
method: 'basic',
username: 'Administrator',
password: 'Administrator'
}
});

Those are the default values, replace them with the values for your own Nuxeo Platform server.

If you don't want to change them you can also write:

var client = new nuxeo.Client();

The Query


Let's say we want to rename all the File documents which have a main file, inside the /default-domain/workspaces/ document (recursively). We can create the following query (ignoring proxies, versions and deleted documents):

let query = "SELECT * FROM Document"

  • " WHERE ecm:primaryType = 'File'"
  • " AND ecm:path STARTSWITH '/default-domain/workspaces/'"
  • " AND content/data IS NOT NULL"
  • " AND dc:title <> content/name"
  • " AND ecm:isProxy = 0 AND ecm:isCheckedInVersion = 0"
  • " AND ecm:currentLifeCycleState != 'deleted'";

Build the Request


Now that we have our query, here is how we can build a Request object that will be executed later:
nuxeo.repository().schemas(['dublincore', 'file']).query({query: query}).then((docs) => {
docs.entries.forEach((doc) => {
console.log(${doc.uid} - ${doc.title})
})
});

Some explanations on what we use:

  • nuxeo.repository(): actually builds the Repository object that is the entry point of any Document actions,

  • schemas(['dublincore', 'file']): we will retrieve only the 'dublincore' and 'file' schemas in the returned JSON object,

  • query(query): use the query function to execute it,

  • .then((docs) => {...}): query returns a Promise, this callback is called when the query responds,


Here is the JSON response of executing it on my own server, where one file needs to be renamed:
{ 'entity-type': 'documents',
isPaginable: true,
resultsCount: 1,
pageSize: 50,
maxPageSize: 100,
currentPageSize: 1,
currentPageIndex: 0,
numberOfPages: 1,
isPreviousPageAvailable: false,
isNextPageAvailable: false,
isLastPageAvailable: false,
isSortable: true,
hasError: false,
errorMessage: null,
totalSize: 1,
pageIndex: 0,
pageCount: 1,
entries:
[ { 'entity-type': 'document',
repository: 'default',
uid: '91e4bf57-bc80-4178-9604-b02ccd6cca8c',
path: '/default-domain/workspaces/test/afile',
type: 'File',
state: 'project',
versionLabel: '0.0',
isCheckedOut: true,
title: 'afile',
lastModified: '2014-06-10T13:48:51.00Z',
properties: [Object],
facets: [Object],
changeToken: '1402408131000',
contextParameters: [Object] } ] }

As you can see, the results are paginated, by default with 50 documents. We will process the first page of entries (forced to a pageSize of 5), and then each subsequent page:
let repository = nuxeo.repository().schemas(['dublincore', 'file']);
let queryOpts = {
query: query,
pageSize: 5
};

let processData = (data) => {
// process the first page
data.entries.forEach(processEntry)
// iterate over all the next pages (using the currentPageIndex parameter
// to retrieve the right page)
if (data.isNextPageAvailable) {
console.log(New page is called: ${data.pageIndex})
queryOpts.currentPageIndex = data.pageIndex + 1;
repository.query(queryOpts).then(processData);
// Catch block is global; no need to add another one here.
}
}

repository.query(queryOpts).then(processData).catch((error) => {
console.log(error);
});

Update the Documents


Let's see the implementation of the processEntry function:

function processEntry(doc) {
let content = doc.properties['file:content'];
if (content && content.name) {
doc.set({'dc:title' : content.name })
doc.save().then((data) => {
console.log(Successfully renamed '${data.title}');
});
}
}

Here, as the default behavior, we manipulate the Document object abstraction over a simple JSON document to help us update the properties and save the doc. Otherwise, we would have directly updated the properties field of the JSON document, and saved it through a PUT with a Request object.

Updating the properties of a document is as easy as calling the set method on the Document object:

doc.set(propertiesObject)

Only the properties existing on the propertiesObject will be updated on the document, the properties object of the document is not completely replaced. Here we update only the title, using the dc:title property.

After updating the properties, we can now save the document on the server through the save method:

doc.save().then((data) => {
console.log(Successfully renamed '${data.title}');
});

Full Project

The Github project is still using the old version of Nuxeo JS CLient.


I've set up this full project, with more logs, comments and options, on the nuxeo-node-sample GitHub repository.

To run it, simply do:

$ git clone https://github.com/troger/nuxeo-node-sample.git
$ cd nuxeo-node-sample
$ node sample.js [--dry-run]

You can setup the client to use your own Nuxeo Platform server in the sample.js file, as well as the query you want to use to find documents.

The --dry-run option allow you to just see which files will be updated, without updating them.