How to copy and modify a document in MarkLogic Javascript?

Viewed 105

I want to copy a document and modify one of the nodes inside it, preferably in one transaction. It seems like the best way to do this is to clone the old document, use the in-mem-update library to modify it, then write it to a new URI. But what I have so far just writes the original document with no modifications.

doc1.xml:

<root>
  <foo>bar</foo>
</root>

What I have so far:

declareUpdate();  
const mem = require("/custom-modules/utils/in-mem-update.xqy");
let oldUri = "/test/doc1.xml";
let newUri = "/test/doc2.xml";
let oldDoc = cts.doc(oldUri);
let newDoc = fn.head(xdmp.unquote(JSON.parse(JSON.stringify(oldDoc))));
let nb = new NodeBuilder();
nb.startElement("foo");
nb.addText("baz");
nb.endElement();
mem.nodeReplace(fn.head(newDoc.xpath("/root/foo")), nb.toNode());
xdmp.documentInsert(newUri, newDoc);

Is there a better way to do this?

2 Answers

I agree with Mads that the in-mem-update library tends to be slow. It builds a new node tree with each change that you apply. There is a newer copy that does all changes in one go, making it much more efficient. That might be a good alternative to the XSLT approach described by Mads:

https://github.com/ryanjdew/XQuery-XML-Memory-Operations

That said, you were close. You missed the fact that the mem functions return an altered copy of your node tree, rather than altering it in place, like the xdmp node functions. It was a matter of adding a newDoc = in front of the mem.nodeReplace call to make your code work:

declareUpdate();
const mem = require('/MarkLogic/appservices/utils/in-mem-update.xqy');
let oldUri = "/test/doc1.xml";
let newUri = "/test/doc2.xml";
let oldDoc = cts.doc(oldUri);
let newDoc = fn.head(xdmp.unquote(JSON.parse(JSON.stringify(oldDoc))));
let nb = new NodeBuilder();
nb.startElement("foo");
nb.addText("baz");
nb.endElement();
newDoc = mem.nodeReplace(fn.head(newDoc.xpath("/root/foo")), nb.toNode());
//xdmp.documentInsert(newUri, newDoc);
newDoc

HTH!

The in-mem-update library can be slow and inefficient, especially if there are lots of changes and the input docs are large.

You can do it fairly easily via XSLT with an identity transform and a specialized template for the foo/text():

declareUpdate();  
    
const oldUri = "/test/doc1.xml";
const newUri = "/test/doc2.xml";
const oldDoc = cts.doc(oldUri);
    
const xslt = fn.head(xdmp.unquote(
  '<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">\n\
     <xsl:template match="foo/text()"><xsl:text>baz</xsl:text></xsl:template>\n\
     <xsl:template match="@*|node()"><xsl:copy><xsl:apply-templates select="@*|node()"/></xsl:copy></xsl:template>\n\
   </xsl:stylesheet>'));
const newDoc = xdmp.xsltEval(xslt, oldDoc.root);
    
xdmp.documentInsert(newUri, newDoc);

You could also use an xsl:param to send in the value to change the text to.

declareUpdate();  

const oldUri = "/test/doc1.xml";
const newUri = "/test/doc2.xml";
const oldDoc = cts.doc(oldUri);

const xslt = fn.head(xdmp.unquote(
 '<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">\n\
    <xsl:param name="foo-val"/>\n\
    <xsl:template match="foo/text()"><xsl:value-of select="$foo-val"/></xsl:template>\n\
    <xsl:template match="@*|node()"><xsl:copy><xsl:apply-templates select="@*|node()"/></xsl:copy></xsl:template>\n\
  </xsl:stylesheet>'));
const newDoc = xdmp.xsltEval(xslt, oldDoc.root, {"foo-val": "baz"});

xdmp.documentInsert(newUri, newDoc);

You might also consider saving the XSLT in the database and using xdmp.xsltInvoke instead of unquoting the serialized XSLT string from within JavaScript. Just putting it all inline to make it easier to demonstrate.

Related