Loading TOC...
Node.js Application Developer's Guide (PDF)

MarkLogic Server 11.0 Product Documentation
Node.js Application Developer's Guide
— Chapter 3

Patching Document Content or Metadata

This chapter covers the following topics related to updating selected portions of the content or metadata of a document using the db.documents.patch function.

Introduction to Content and Metadata Patching

A partial update is an update you apply to a portion of a document or metadata, rather than replacing an entire document or all of its metadata. For example, inserting a JSON property or XML element, or changing the value of a JSON property. You can only apply partial content updates to JSON and XML documents. You can apply partial metadata updates to any document type.

A patch is a partial update descriptor, expressed in JSON or XML. A patch tells MarkLogic Server what update to apply and where to apply it. Four operations are available in a patch: insert, replace, replace-insert, and delete. (A replace-insert operation functions as a replace if there is at least one match for the target content; if there are no matches, then the operation functions as an insert.)

Please note that statements of the following form {'$ml.xml': '<prop:properties ......} are not allowed in metadata patching.

Use the partial update feature for the following operations:

  • Add or delete a JSON property, property value, or array item in an existing document.
  • Add, replace, or delete the value of an array item or JSON property.
  • Add, replace, or delete a subset of the metadata of an existing document. For example, modify a permission or insert a document property.
  • Dynamically generate replacement content or metadata on MarkLogic Server using builtin or custom, user-supplied functions. For details, see Constructing Replacement Data on MarkLogic Server.

You can apply multiple updates in a single patch, and you can update both content and metadata in the same patch.

Patch operations can target JSON properties, XML elements and attributes, and data values such as JSON array items and the data in an XML element or attribute. You identify the target of an operation using XPath or JSONPath expressions. When inserting new content or metadata, the insertion point is further defined by specifying the position; for details, see How Position Affects the Insertion Point in the REST Application Developer's Guide.

When applying a patch to document content, the patch format must match the document format: An XML patch for an XML document, a JSON patch for a JSON document. You cannot patch the content of other document types. You can patch metadata for all document types. A metadata-only patch can be in either XML or JSON. A patch that modifies both content and metadata must match the document content type.

The Node.js Client API provides the following interfaces for building and applying patches:

  • marklogic.patchBuilder
  • DatabaseClient.documents.patch

Apply a patch by calling DatabaseClient.documents.patch with a URI and one or more patch operations. Build patch operations using marklogic.patchBuilder.

The following example patches the JSON document /patch/example.json by inserting a property named child3 in the last child position under the property named theTop. For a complete example, see Example: Adding a JSON Property.

const pb = marklogic.patchBuilder;
db.documents.patch('/patch/example.json',
  pb.insert('/object-node("theTop")', 'last-child', 
    {child3: 'INSERTED'})
);

For additional examples of building patch operations, see Patch Examples.

If a patch contains multiple operations, they are applied independently to the target document. That is, within the same patch, one operation does not affect the context path or select path results or the content changes of another. Each operation in a patch is applied independently to every matched node. If any operation in a patch fails with an error, the entire patch fails.

Content transformations are not directly supported in a partial update. However, you can implement a custom replacement content generation function to achieve the same effect. For details, see Constructing Replacement Data on MarkLogic Server.

Before patching JSON documents, you should familiarize yourself with the restrictions outlined in Limitations of JSON Path Expressions in the REST Application Developer's Guide.

Example: Adding a JSON Property

The following example inserts a new property into a JSON document using the DatabaseClient.documents.patch function and a patch builder. The example program does the following:

  1. Insert the example document into the database with the URI /patch/example.json.
  2. Patch the example document by inserting a new property under theTop named child3 in the last child position. The parent node is identified using an XPath expression.
  3. Read the patched document from the database and display it on the console.

The db.documents.patch function accepts a document URI and one or more operations. In this case, we pass only one operation: A property insertion constructed by calling the patch builder insert method.

pb.insert('/object-node("theTop")',   // path to parent node
          'last-child',               // insertion position
          {child3: 'INSERTED'})       // content to insert

The promise pattern is used to chain together the write, patch, and read operations. For details, see Promise Result Handling Pattern.

The example script is shown below. The initial write operation is included in the example only to encapsulate the example document and ensure consistent behavior across runs. Usually, the document targeted by a patch would have been loaded into the database separately.

const marklogic = require('marklogic');
const my = require('./my-connection.js');
const db = marklogic.createDatabaseClient(my.connInfo);

// (1) Insert the base document into the database
db.documents.write({
  uri: '/patch/example.json',
  contentType: 'application/json',
  content: {
    theTop: {
      child1: { grandchild: 'gc-value' },
      child2: 'c2-value'
  } }
}).result().then(function(response) {
  // (2) Patch the document
  const pb = marklogic.patchBuilder;
  return db.documents.patch(response.documents[0].uri,
    pb.insert('/object-node("theTop")', 'last-child', 
              {child3: 'INSERTED'})
  ).result();
}).then(function(response) {
  // (3) Read the patched document
  return db.documents.read(response.uri).result();
}).then(function(response) {
  console.log(response[0].content);
});

The example program results in the following document transformation:

Before Patching After Patching
{"theTop" : {
  "child1" : {
    "grandchild" : "gc-value"
  },
  "child2" : "c2-value"
}}
{"theTop" : {
  "child1" : {
    "grandchild" : "gc-value"
  },
  "child2" : "c2-value",
  "child3" : "INSERTED"
}}

On success, DatabaseClient.documents.patch returns the URI of the patched document. For example, the result of the db.documents.patch call above is:

{ uri: '/patch/example.json' }

For more examples, see Patch Examples.

Patch Reference

You can include one or more of the following operations in a call to DatabaseClient.documents.patch: insert, replace, replace-insert, remove. These operations can occur multiple times in a given patch call. Use the marklogic.patchBuilder interface to construct each operation. For example:

db.documents.patch(uri,
  pb.insert(path, position, newContent)
)

The patch builder includes special purposes interfaces for constructing patch operations that target metadata: patchBuilder.collections, patchBuilder.permissions, patchBuilder.properties, patchBuilder.quality, and patchBuilder.metadataValues. These interfaces enable you to patch collections, permissions, quality, document properties, and values metadata without knowing the internal representation of metadata. For an example of how to use these interfaces, see Example: Patching Metadata.

In addition, you can include patch configuration directives, such as patchBuilder.library and patchbuilder.pathLanguage. You can only include one library directive, and it is only needed if you use custom functions to generate replacement content; for details, see Constructing Replacement Data on MarkLogic Server.

The table below summarizes the patch builder methods for constructing operations on content:

Builder Method Descriptions
insert
Insert a new property or array item.
replace
Replace an existing property or array item.
replaceInsert
Replace a JSON property or array item; if there are no existing matching properties, perform an insertion instead.
remove
Remove a JSON property or array item.
library
Identify a server-side XQuery library module that contains custom replacement content generation functions that can be used in the apply operation that is part of a replace or replaceInsert operation.
apply
Specify a server-side replacement content generation function. The patch operation must include a library operation, and the named function must be in that module.
pathLanguage
Configure a patch to parse select and context expressions as either XPath (default) or JSONPath expressions.

The following table summarizes the patch builder interfaces for constructing operations on metadata.

Builder Method Descriptions
collections
Construct patch operations for modifying collections metadata.
permissions
Construct patch operations for modifying permissions metadata.
properties
Construct patch operations for modifying document properties metadata.
quality
Construct patch operations for modifying quality metadata.
metadataValues
Construct patch operations for modifying values metadata.

The following table summarizes the patch builder methods for constructing cofiguration directives:

Builder Method Descriptions
library
Identify a server-side XQuery library module that contains custom replacement content generation functions that can be used in the apply operation that is part of a replace or replaceInsert operation.
apply
Specify a server-side replacement content generation function. The patch operation must include a library operation, and the named function must be in that module.
pathLanguage
Configure a patch to parse select and context expressions as either XPath (default) or JSONPath expressions.

insert

Use patchbuild.insert to create an operation that inserts a new JSON property or array item. Build an insert operation with a call of the following form:

marklogic.patchBuilder.insert(
  context, 
  position, 
  content, 
  cardinality)

The following table summarizes the parameters of the patchBuilder.insert function.

Parameter Req'd Description
context Y

An XPath or JSONPath expression that selects an existing JSON property or array element on which to operate. The expression can select multiple items.

If no matches are found for the context expression, the operation is silently ignored.

The path expression is restricted to the subset of XPath for which cts:valid-document-patch-path (XQuery) or cts.validDocumentPatchPath (JavaScript) returns true. For details, see Path Expressions Usable in Patch Operations in the REST Application Developer's Guide.

position Y Where to insert the content, relative to the JSON property or value selected by context. The pos-selector must be one of "before", "after", or "last-child". For details, see How Position Affects the Insertion Point.
content Y The new content to be inserted, expressed as a JSON object, array, or atomic value.
cardinality N

The required occurrence of matches to position. If the number of matches does not meet the expectation, the operation fails and an error is returned. Allowed values:

  • Zero or one matches required: "?" (question mark)
  • Exactly one match required: "." (period)
  • Zero or more matches required: "*" (asterisk)
  • One or more matches required: "+" (plus)

Default: "*" (The occurrence requirement is always met).

replace

Use patchBuilder.replace to create an operation that replaces an existing JSON property value or array item. If no matching JSON property or array item exists, the operation is silently ignored. Build a replace operation with a call of the following form:

marklogic.patchBuilder.replace(
  select, 
  content, 
  cardinality, 
  apply)

You can use apply to specify a content generation builtin or custom function for generating dynamic content. For details, see Constructing Replacement Data on MarkLogic Server.

The following table summarizes the parameters of the patchBuilder.replace function.

Parameter Req'd Description
select Y

An XPath or JSONPath expression that selects the JSON property or array element to replace. If no matches are found for the select expression, the operation is silently ignored.

The path expression is restricted to the subset of XPath for which cts:valid-document-patch-path (XQuery) or cts.validDocumentPatchPath (JavaScript) returns true. For details, see Path Expressions Usable in Patch Operations in the REST Application Developer's Guide.

The selected item(s) cannot be the target of any other operation in the patch. The ancestor of the selected item may not be modified by a delete, replace, or replace-insert operation in the same patch.

content N

The replacement value. If you omit this parameter, you must specify a content generation function in the apply parameter.

If select targets a property and content is an object, the operation replaces the entire target property. Otherwise, the operation replaces just the value.

cardinality N

The required occurrence of matches to select. If the number of matches does not meet the expectation, the operation fails and an error is returned. Allowed values:

  • Zero or one matches required: "?" (question mark)
  • Exactly one match required: "." (period)
  • Zero or more matches required: "*" (asterisk)
  • One or more matches required: "+" (plus)

Default: "*" (The occurrence requirement is always met).

apply N

The local name of a replacement content generation function. If you do not specify a function, the operation must include a content parameter.

If you name a custom function, the patch must include a replace-library operation that describes the XQuery library module containing the function implementation. To construct such an operation, use the patchBuilder.library function.

For details, see Constructing Replacement Data on MarkLogic Server.

replaceInsert

Use patchBuilder.replaceInsert to create an operation that replaces a property value or array item if it exists, or insert a property or array item if does not. Build a replace-insert operation with a call of the following form:

replaceInsert(
  select, 
  context, 
  position, 
  content, 
  cardinality, 
  apply)

You can omit content if you use apply to specify a content generation builtin or custom function for generating dynamic content. For details, see Constructing Replacement Data on MarkLogic Server.

The following table summarizes the parts of a replace-insert operation.

Parameter Req'd Description
select
Y

An XPath or JSONPath expression that selects the JSON property or array item to replace. If no matches are found for the select expression, context and position are used to attempt an insert. If no match is found for context, the operation does nothing.

The path expression is restricted to the subset of XPath for which cts:valid-document-patch-path (XQuery) or cts.validDocumentPatchPath (JavaScript) returns true.. For details, see Path Expressions Usable in Patch Operations in the REST Application Developer's Guide.

The selected item(s) cannot be the target of any other operation in the patch. The ancestor of the selected item may not be modified by a delete, replace, or replace-insert operation in the same patch.

content
N

The content with which to replace the selected value. If there is no content, you must specify a content generation function using apply.

If select targets a property and content is an object, the operation replaces the entire target property. Otherwise, the operation replaces just the value.

context
Y

An XPath or JSONPath expression that selects an existing property or array element on which to operate. The expression can select multiple items.

If no matches are found for the either the select or context expression, the operation is silently ignored.

The path expression is restricted to the subset of XPath for which cts:valid-document-patch-path (XQuery) or cts.validDocumentPatchPath (JavaScript) returns true. For details, see Path Expressions Usable in Patch Operations in the REST Application Developer's Guide.

The ancestor of the selected node may not be modified by a delete, replace, or replace-insert operation in the same patch.

position N

If select does not match anything, where to insert the content, relative to the key-value pair or value selected by context. The pos-selector must be one of "before", "after", or "last-child". For details, see How Position Affects the Insertion Point.

Default: last-child.

cardinality N

The required occurrence of matches to position. If the number of matches does not meet the expectation, the operation fails and an error is returned. Allowed values:

  • Zero or one matches required: "?" (question mark)
  • Exactly one match required: "." (period)
  • Zero or more matches required: "*" (asterisk)
  • One or more matches required: "+" (plus)

Default: * (The occurrence requirement is always met).

apply N

The local name of a replacement content generation function. If you do not specify a function, the operation must include a content parameter.

If you name a custom function, the patch must include a replace-library operation that describes the XQuery library module containing the function implementation. Create such an operation using the patchBuilder.library function.

For details, see Constructing Replacement Data on MarkLogic Server.

remove

Use patchBuilder.remove to create an operation that removes a JSON property or array element. A call to the patchBuilder.remove function has the following form:

marklogic.patchBuilder.remove(
  select,
  cardinality)

The following table summarizes the parameters of patchBuilder.remove.

Component Req'd Description
select Y

An XPath or JSONPath expression that selects the JSON property or array element to remove. If no matches are found for the select expression, the operation is silently ignored.

The path expression is restricted to the subset of XPath for which cts:valid-document-patch-path (XQuery) or cts.validDocumentPatchPath (JavaScript) returns true. For details, see Path Expressions Usable in Patch Operations in the REST Application Developer's Guide..

The selected item(s) cannot be the target of any other operation in the patch. The ancestor of the selected item may not be modified by a delete, replace, or replace-insert operation in the same patch.

cardinality N

The required occurrence of matches to select. If the number of matches does not meet the expectation, the operation fails and an error is returned. Allowed values:

  • Zero or one matches required: "?" (question mark)
  • Exactly one match required: "." (period)
  • Zero or more matches required: "*" (asterisk)
  • One or more matches required: "+" (plus)

Default: "*" (The occurrence requirement is always met).

apply

Use patchBuilder.apply to create a configuration directive that specifies a server-side function with which to generate replacement conent for a replace or replaceInsert operation. The named function must be in a server-side module identified by a library directive included in the same call to db.documents.patch.

A call to the patchBuilder.apply function has the following form:

marklogic.patchBuilder.apply(functionName)

For details, see Constructing Replacement Data on MarkLogic Server.

library

Use patchBuilder.library to create a configuration directive that identifies a server-side library module containing one or more functions for generating replacement content. The module must conform to the conventions described in Writing a Custom Replacement Constructor. To make use of the functions in your library, include the generated library directive in your call to db.documents.patch, and include the output from patchBuilder.apply when calling the replace or replaceInsert builder functions.

A call to the patchBuilder.library function has the following form:

marklogic.patchBuilder.library(moduleName)

For details, see Constructing Replacement Data on MarkLogic Server.

pathLanguage

By default, all path expressions in patches are expressed using XPath. JSONPath is also supported for compatibility with previous releases, but it is deprecated. You do not need to use a pathLanguage directive unless you use JSONPath.

Use patchBuilder.pathLanguage to create a configuration directive that specifies the path language in which you express context and select expressions in your patch operations. A call to the patchBuilder.pathLanguage function must have one of the following forms:

marklogic.patchBuilder.pathLanguage('xpath')
marklogic.patchBuilder.pathLanguage('jsonpath')

Include the resulting directive in your call to db.documents.patch.

You must use the same path language for all operations in a DatabaseClient.documents.patch call.

collections

Use the patchBuilderCollections methods to construct a patch operation for collections metadata. Use patchBuiler.collections to access these methods. You can add or remove a collection, using the following methods:

marklogic.patchBuilder.collections.add(collName)

marklogic.patchBuilder.collections.remove(collName)

For an example, see Example: Patching Metadata.

permissions

Use patchBuilderPermissions methods to construct a patch operation for permissions metadata. Use patchBuilder.permissions to access these methods. You can add or remove a permission or replace the capabilities of a role using this interface.

marklogic.patchBuilder.permissions.add(role, capabilities)

marklogic.patchBuilder.permissions.remove(role)

marklogic.patchBuilder.permissions.replace(role, capabilities)

Where role is a string that contains the role name, and capabilities is either a single string or an array of strings with the following possible values: read, insert, update, execute.

Note that replace replaces all the capabilities associated with a role. You cannot use it to selectively replace a single capability. Rather, you must operate on a role and the associated capabilities as a single unit.

For an example, see Example: Patching Metadata.

properties

Use patchBuilderProperties methods to construct a patch operation for document properties metadata. Use patchBuilder.properties to access these methods. You can add or remove a property, or replace the value of a property.

marklogic.patchBuilder.properties.add(name, value)

marklogic.patchBuilder.properties.remove(name)

marklogic.patchBuilder.properties.replace(name, value)

For an example, see Example: Patching Metadata.

quality

Use patchBuilderQuality methods to construct a patch operation that sets the value of quality metadata. Use patchBuilder.quality to access these methods.

marklogic.patchBuilder.quality.set(value)

For an example, see Example: Patching Metadata.

metadataValues

Use patchBuilderMetadataValues methods to construct a patch operation on values metadata. Use patchBuilder.metdataValues to access these methods. You can add or remove a key-value pair, or replace the value of an existing key.

marklogic.patchBuilder.metadataValues.add(name, value)

marklogic.patchBuilder.metadataValues.remove(name)

marklogic.patchBuilder.metadataValues.replace(name, value)

For an example, see Example: Patching Metadata.

Defining the Context for a Patch Operation

When you insert, replace, or delete content or metadata, the patch definition must include enough context to tell MarkLogic Server what JSON or XML components to operate on. For example, which json property or XML element to delete, where to insert a new property or element, or which JSON property, XML element, or value to replace.

When you create a patch using a builder, you specify the context through the contextPath and selectPath parameters of builder methods such as DocumentPatchBuilder.insertFragment() or DocumentPatchBuilder.replaceValue(). When you create a patch from raw XML or JSON, you specify the operation context through the context and select XML attributes or JSON property.

Use XPath expressions to define the operation context. For security and performance reasons, your XPath expressions are restricted to the subset of XPath for which cts:valid-document-patch-path (XQuery) or cts.validDocumentPatchPath (JavaScript) returns true. For details, see Patch Feature of the Client APIs in the XQuery and XSLT Reference Guide.

Insertion operations have an additional position parameter/property that defines where to insert new content relative to the context, such as before or after the selected node. For more details, seeHow Position Affects the Insertion Point.

How Position Affects the Insertion Point

The insert and replace-insert patch operations include a position parameter that defines the point of insertion when coupled with a context expression. This section describes the details of how position affects the insertion point.

This topic focuses on patching JSON documents. For details on XML, see Specifying Position in XML in the REST Application Developer's Guide.

The position parameter to a PatchBuilder operation (or the position property of a raw patch) specifies where to insert new content relative to the item selected by the context XPath expression. Position can be one of the following values:

  • before: Insert before the item selected by context.
  • after: Insert after the item selected by context.
  • last-child: Insert into the value of the target property or array, in the position of last child. The value selected by context must be an object or array node.

The same usage applies whether inserting on behalf of an insert operation or a replace-insert operation.

The following table shows example combinations of context and position and the resulting insertion point for new content. The insertion point is indicated by ***.

Context Position Example Insertion Point

/theTop/node("child1")

a property

after
{"theTop" : {
    "child1" : [ "val1", "val2" ],
    ***
    "child2" : [ 
      {"one": "val1"}, 
      {"two": "val2"} 
    ]
} }

/theTop/child1[1]

an array item

before
{"theTop" : {
    "child1" : [ *** "val1", "val2" ],
    "child2" : [ 
      {"one": "val1"}, 
      {"two": "val2"} 
    ]
} }

/theTop/array-node("child1")

an array

last-child
{"theTop" : {
    "child1" : [ "val1", "val2" ***],
    "child2" : [ 
      {"one": "val1"}, 
      {"two": "val2"} 
    ]
} }

/node("theTop")

an object

last-child
{"theTop" : {
    "child1" : [ "val1", "val2"],
    "child2" : [ 
      {"one": "val1"}, 
      {"two": "val2"} 
    ]
    ***
} }

/theTop/child1

a value

last-child

Error because the selected value is not an object or array. For example, an error results if the target document has the following contents:

{"theTop" : {     "child1" : "val1",     "child2" : [...] } }

For more information about XPath over JSON, see Traversing JSON Documents Using XPath in the Application Developer's Guide.

Patch Examples

This section contains examples of constructing and applying patches to metadata and JSON documents. The following topics are covered:

To patch XML documents, you must construct a raw patch. For details, see Patching XML Documents and Creating a Patch Without a Builder.

Preparing to Run the Examples

The examples are self-contained, except for the database connection information. Each example assumes your connection information is encapsulated in a module named my-connection.js that is co-located with the example script.

This module is expected contain contents similar to the following:

module.exports = {
  connInfo: {
    host: 'localhost',
    port: 8000,
    user: your-ml-username,
    password: your-ml-user-password
  }
};

For details, see Using the Examples in This Guide.

Example: Insert

This example demonstrates the insert operation by making the following changes to the base JSON document:

  • Insert a new property of the unnamed root object (INSERTED1).
  • Insert a new property in a sub-object, relative to a sibling property (INSERTED2).
  • Insert a new array item relative to an item with a specific value (INSERTED3).
  • Insert a new array item relative to a specific position in the array (INSERTED4).
  • Insert a new item at the end of an array (INSERTED5).
  • Insert a new property in the last child position (INSERTED6).

The example first inserts the base document into the database, then builds and applies a patch. Finally, the modified document is retrieved from the database and displayed on the console. The example uses a Promise pattern to sequence these operations; for details, see Promise Result Handling Pattern.

To run the example, copy the following code into a file, then supply it to the node command. For example: node insert.js.

const marklogic = require('marklogic');
const my = require('./my-connection.js');
const db = marklogic.createDatabaseClient(my.connInfo);

// (1) Insert the base document into the database
db.documents.write({
  uri: '/patch/insert.json',
  contentType: 'application/json',
  content: {
    theTop: {
      child1: { grandchild: 'gcv' },
      child2: [ 'c2v1', 'c2v2' ],
      child3: [ {c3k1: 'c3v1'}, {c3k2: 'c3v2'} ]
  } }
}).result().then(function(response) {
  // (2) Patch the document
  const pb = marklogic.patchBuilder;
  return db.documents.patch(response.documents[0].uri,
    // insert a sibling of theTop 
    pb.insert('/theTop', 'before',
              {INSERTED1: [ 'i1v1', 'i1v2']}),

    // insert a property in child1's value
    pb.insert('/theTop/child1/grandchild', 'before',
              {INSERTED2: 'i2v'}),

    // insert an array item in child2 by value
    pb.insert('/theTop/child2[. = "c2v1"]', 'after',
              'INSERTED3'),

    // insert an array item in child2 by position
    pb.insert('/theTop/child2[2]', 'after',
              'INSERTED4'),

    // insert an object in child3
    pb.insert('/theTop/array-node("child3")', 'last-child',
              {INSERTED5 : 'i5v'}),

    // insert a new child of theTop
    pb.insert('/theTop', 'last-child',
              {INSERTED6: 'i6v'})
  ).result();
}).then(function(response) {
  // (3) Emit the resulting document
  return db.documents.read(response.uri).result();
}).then(function(documents) {
  console.log(JSON.stringify(documents[0].content));
});

The following table shows how applying the patch changes the target document.

Before Update After Update
{"theTop": {
  "child1": {"grandchild": "gcv"},
  "child2": ["c2v1", "c2v2"],
  "child3": [ 
    {"c3k1": "c3v1"}, 
    {"c3k2": "c3v2"} 
  ]
} }
{ "INSERTED1": [ "i1v1", "i1v2" ],
  "theTop": {
    "child1": {
      "INSERTED2": "i2v",
      "grandchild": "gcv"
    },
    "child2": [
      "c2v1",
      "INSERTED3",
      "c2v2",
      "INSERTED4"
    ],
    "child3": [
      { "c3k1": "c3v1" },
      { "c3k2": "c3v2" }
      { "INSERTED5": "i5v" }
    ],
    "INSERTED6": "i6v"
} }

Note that when you insert something using last-child for the position, the insertion context XPath expression must refer to the containing object or array. When you construct an XPath expression such as /a/b, the expression references a value (or sequence of values), rather than the container. To reference the container, use the node() and array-node() operators. This is why the operation that creates INSERTED5 under child3 uses array-node("child3"), as shown below:

pb.insert('/theTop/array-node("child3")', 'last-child',
              {INSERTED5 : 'i5v'})

If you change the insertion context expression to /theTop/child3, you're referencing the sequence of values in the array, not the array object to which you want to add a child.

To insert a property if and only if it does not already exist, use a predicate in the context path expression to test for existence. For example, the following patch will insert a property named TARGET only if the object does not already contain such a property:

pb.insert('/theTop[fn:empty(TARGET)]', 'last-child', 
          {TARGET: 'INSERTED'})

This patch must use the last-child position because the context selects the node that will contain the new property.

For more information about XPath over JSON, see Traversing JSON Documents Using XPath in the Application Developer's Guide.

Example: Replace

This example demonstrates the replace operation by making the following changes to a JSON document:

  • Replace the value of a property with a simple value (child1), object value (child2), or array value (child3).
  • Replace the value of an array item by position or value (child4).
  • Replace the value of all items in an array with the same value (child5).
  • Replace the value of an object in an array by property name (child6).
  • Replace an array item that is an object by property name (child6).
  • Replace the value of an array item that is a nested array (child7).
  • Replace the value of an item in a nested array (child8).

The example first inserts the base document into the database, then builds and applies a patch. Finally, the modified document is retrieved from the database and displayed on the console. The example uses a Promise pattern to sequence these operations; for details, see Promise Result Handling Pattern.

To run the example, copy the following code into a file, then supply it to the node command. For example: node replace.js.

const marklogic = require('marklogic');  
const my = require('./my-connection.js');  
const db = marklogic.createDatabaseClient(my.connInfo);  
  
// (1) Insert the base document into the database  
db.documents.write({
  uri: '/patch/replace.json',
  contentType: 'application/json',
  content: {
    theTop: {
      child1: 'c1v',
      child2: { gc: 'gcv' },
      child3: [ 'c3v1', 'c3v2' ],
      child4: [ 'c4v1', 'c4v2' ],
      child5: [ 'c5v1', 'c5v2' ],
      child6: [ {gc1: 'gc1v'}, {gc2:'gc2v'}],
      child7: [ 'av1', ['nav1', 'nav2'], 'av2'],
      child8: [ 'av1', ['nav1', 'nav2'], 'av2']
  } }
}).result().then(function(response) {
  // (2) Patch the document
  const pb = marklogic.patchBuilder;
  return db.documents.patch(response.documents[0].uri,
    // replace the simple value of a property
    pb.replace('/theTop/child1', 'REPLACED1'),
  
    // replace a property value that is an object
    pb.replace('/theTop/child2', {REPLACE2: 'gc2'}),
  
    // replace the value of a property that is an array
    pb.replace('/theTop/array-node("child3")',
              ['REPLACED3a', 'REPLACED3b']),

    // replace an array item by position
    pb.replace('/theTop/child4[1]', 'REPLACED4a'),

    // replace an array item by value
    pb.replace('/theTop/child4[.="c4v2"]', 'REPLACED4b'),

    // replace the value of all items in an array with the same value
    pb.replace('/theTop/child5', 'REPLACED5'),

    // replace the value of a property in an array item
    pb.replace('/theTop/child6/gc1', 'REPLACED6a'),

    // replace an object-valued array item by property name
    pb.replace('/theTop/child6[gc2]', {REPLACED6b: '6bv'}),

    // replace the value of an array item that is a nested array
    pb.replace('/theTop/array-node("child7")/node()[2]',
               [ 'REPLACED7a', 'REPLACED7b' ]),

    // replace the value of item in a nested array
    pb.replace('/theTop/child8[2]', 'REPLACED8')
).result();
}).then(function(response) {
  // (3) Emit the resulting document
  return db.documents.read(response.uri).result();
}).then(function(documents) {
  console.log(JSON.stringify(documents[0].content, null, 2));
});

The following table shows how applying the patch changes the target document.

Before Update After Update
{"theTop": {
  "child1": "c1v",
  "child2": { "gc": "gcv" },
  "child3": [ "c3v1", "c3v2" ],
  "child4": [ "c4v1", "c4v2" ],
  "child5": [ "c5v1", "c5v2" ],
  "child6": [
    {"gc1": "gc1v"},
    {"gc2":"gc2v"}
  ],
  "child7": [
    "av1",
    ["nav1", "nav2"],
    "av2"
  ],
  "child8": [
    "av1",
    ["nav1", "nav2"],
    "av2"
  ]
} }
{"theTop":{  
  "child1": "REPLACED1",
  "child2": { "REPLACE2": "gc2" },
  "child3": [ "REPLACED3a", "REPLACED3b" ],
  "child4": [ "REPLACED4a", "REPLACED4b" ],
  "child5": [ "REPLACED5", "REPLACED5" ],
  "child6": [ 
    { "gc1": "REPLACED6a" },
    { "REPLACED6b": "6bv" }
  ],
  "child7": [
    "av1",
    [ "REPLACED7a", "REPLACED7b" ],
    "av2"
  ],
  "child8": [
    "av1",
    [ "REPLACED8", "nav2" ],
    "av2"
  ]
} }

You should understand the difference between selecting a container and selecting its contents. For example, consider the two replacement operations applied to child5. The XPath expression /theTop/child6/gc1 addresses the value of property with name gc1. Therefore, the replacement operation results in the following change:

pb.replace('/theTop/child6/gc1', 'REPLACED6a')
{"gc1" : "gc1v"} ==> {"gc1" : "REPLACED6a"}

By contrast the XPath expression /theTop/child6[gc2] selects object nodes that contain a property named gc2. Therefore, the replacement operation replaces the entire object with a new value, resulting in the following change:

pb.replace('/theTop/child6[gc2]', {REPLACED6b: '6bv'})
{"gc2" : "gc2v"} ==> {"REPLACED6b": "6bv"}

Similarly, consider the replacement operation on child3. The XPath expression /theTop/array-node("child3") selects the array node named child3, so the operation replaces the entire array with a new value. For example:

pb.replace('/theTop/array-node("child3")',
              ['REPLACED3a', 'REPLACED3b'])
"child3": ["c3v1", "c3v2"] ==> "child3": ["REPLACED3a", "REPLACED3b"]

By contrast, an XPath expression such as /theTop/child5 selects the values in the array ('c5v1', 'c5v2'), so a replacement operation with this select expression replaces each of the array values with the same content. For example:

pb.replace('/theTop/child5', 'REPLACED5')

"child5": ["c5v1", "c5v2"] ==> "child5": ["REPLACED5", "REPLACED5"]

For more information on XPath over JSON, see Traversing JSON Documents Using XPath in the Application Developer's Guide.

Example: ReplaceInsert

This example demonstrates the replace-insert patch operation. The example performs the following update operations:

  • Replace or insert an array item by position (child1).
  • Replace or insert an array item by value (child2).
  • Replace or insert an object-valued array item by contained property name (child3).

The example first inserts the base document into the database, then builds and applies a patch. Finally, the modified document is retrieved from the database and displayed on the console. The example uses a Promise pattern to sequence these operations; for details, see Promise Result Handling Pattern.

To run the example, copy the following code into a file, then supply it to the node command. For example: node replace-insert.js.

const marklogic = require('marklogic');
const my = require('./my-connection.js');
const db = marklogic.createDatabaseClient(my.connInfo);

// (1) Insert the base document into the database
db.documents.write({
  uri: '/patch/replace-insert.json',
  contentType: 'application/json',
  content: {
    theTop: {
      child1: [ 'c1v1', 'c1v2' ],
      child2: [ 'c2v1', 'c2v2' ],
      child3: [ { c3a: 'c3v1' }, { c3b: 'c3v2' } ]
  } }
}).result().then(function(response) {
  // (2) Patch the document
  const pb = marklogic.patchBuilder;
  return db.documents.patch(response.documents[0].uri,
    // Replace the value of an array item by position, or 
    // insert a new one in the target position. 
    pb.replaceInsert('/theTop/child1[1]', 
              '/theTop/array-node("child1")', 'last-child',
              'REPLACED1'),
    pb.replaceInsert('/theTop/child1[3]', '/theTop/child1[2]', 'after',
              'INSERTED1'),

    // Replace an array item that has a specific value, or 
    // insert a new item with that value at the end of the array
    pb.replaceInsert('/theTop/child2[. = "c2v1"]',
              '/theTop/node("child2")', 'last-child',
              'REPLACED2'),
    pb.replaceInsert('/theTop/child2[. = "INSERTED2"]',
              '/theTop/array-node("child2")', 'last-child',
              'INSERTED2'),

    // Replace the value of an object that is an array item, or 
    // insert an equivalent object at the end of the array
    pb.replaceInsert('/theTop/child3[c3a]',
              '/theTop/node("child3")', 'last-child',
              { REPLACED3: 'c3rv'}),
    pb.replaceInsert('/theTop/child3[INSERTED3]',
              '/theTop/node("child3")', 'last-child',
              { INSERTED3: 'c3iv'})
  ).result();
}).then(function(response) {
  // (3) Emit the resulting document
  return db.documents.read(response.uri).result();
}).then(function(documents) {
  console.log(JSON.stringify(documents[0].content, null, 2));
}, function(error) { console.log(error); throw error; }
);

The following table shows how applying the patch changes the target document.

Before Update After Update
{"theTop": {
  "child1": [ "c1v1", "c1v2" ],
  "child2": [ "c2v1", "c2v2" ],
  "child3": [ 
    { "c3a": "c3v1" }, 
    { "c3b": "c3v2" } 
  ]
} }
{ "theTop": {
    "child1": [
      "REPLACED1",
      "c1v2",
      "INSERTED1"
    ],
    "child2": [
      "REPLACED2",
      "c2v2",
      "INSERTED2"
    ],
    "child3": [
      { "REPLACED3": "c3rv" },
      { "c3b": "c3v2" },
      { "INSERTED3": "c3iv" }
    ]
} }

Recall that the select path identifies the content to replace. When working with an array item, an absolute path is usually required. For example, consider the following patch operation:

pb.replaceInsert('/theTop/child1[1]', 
              '/theTop/array-node("child1")', 'last-child',
              'REPLACED1')

The goal is to replace the value of the first item in the array value of /theTop/child1 if it exists. If the array is empty, insert the new value. That is, one of these two transformations takes place:

{"theTop": {"child1": ["c1v1", "c1v2"], ... }
    ==> {"theTop": {"child1": ["REPLACED1", "c1v2"], ... }
{"theTop": {"child1": [], ... }
    ==> {"theTop": {"child1": ["REPLACED1"], ... }

The select expression, /theTop/child1[1], must target an array item value, while the context expression must target the containing array node by referencing /theTop/array-node("child1"). You cannot make the select expression relative to the context expression in this case.

Note that while you can target an entire array item value with replace-insert, you cannot target just the value of a property. For example, consider the following array in JSON:

"child3": [ 
  { "c3a": "c3v1" },
  { "c3b": "c3v2" }
]

You can use replace-insert on the entire object-valued item { "c3a": "c3v1" }, as is done in the example. However, you cannot construct an operation that targets just the value of the property in the object ("c3v1"). The replacement of the property value is fundamentally different from inserting a new object in the array. A property (as opposed to the containing object) can only be replaced by deleting it and then inserting a new value.

You cannot use a replaceInsert operation to conditionally insert or replace a property because the insertion content and the replacement content requirements differ. However, you can use separate insert and replace operations within the same patch to achieve the same effect.

For example, the following patch inserts a new property named TARGET if it does not already exists, and replaces its value if it does already exist:

db.documents.patch('/patch/replace-insert.json',
  pb.insert('/theTop[fn:empty(TARGET)]', 'last-child',
            { TARGET: 'INSERTED' }),
  pb.replace('/theTop/TARGET', 'REPLACED')
)

The following table illustrates the effect of applying the patch:

Before Update After Update
{"parent": {
  "child": "some_value"
}}
{"parent":{
  "child": "some_value",
  "TARGET": "INSERTED"
}}
{"parent": {
  "child": "some_value",
  "TARGET": "INSERTED"
}}
{"parent": {
  "child": "some_value",
  "TARGET": "REPLACED"
}}

For more details on the JSON document model and traversing JSON documents with XPath, see Working With JSON in the Application Developer's Guide.

Example: Remove

This example demonstrates how to use the patch remove (delete) operation to make the following changes to a JSON document:

  • Remove a property based on type.
  • Remove an array item by position, value, or property name.
  • Remove all items in an array.

To run the example, copy the following code into a file, then supply it to the node command. For example: node remove.js.

const marklogic = require('marklogic');
const my = require('../my-connection.js');
const db = marklogic.createDatabaseClient(my.connInfo);

// (1) Insert the base document into the database
db.documents.write({
  uri: '/patch/remove.json',
  contentType: 'application/json',
  content: {
    props: {
      anyType: [1, 2],
      objOrLiteral: 'anything',
      arrayVal: [3, 4]
    },
    arrayItems: {
      byPos: ['DELETE', 'PRESERVE'],
      byVal: ['DELETE', 'PRESERVE'],
      byPropName: [ {DELETE: 5}, {PRESERVE: 6} ],
      all: ['DELETE1', 'DELETE2']
    }
  }
}).result().then(function(response) {
  // (2) Patch the document
  const pb = marklogic.patchBuilder;
  return db.documents.patch(response.documents[0].uri,
    // Remove a property with any value type
    pb.remove('/props/node("anyType")'),

    // Remove a property with atomic or object value type
    pb.remove('/props/objOrLiteral'),

    // Remove a property with array value type
    pb.remove('/props/array-node("arrayVal")'),

    // Remove all items in an array
    pb.remove('/arrayItems/all'),

    // Remove an array item by position
    pb.remove('/arrayItems/byPos[1]'),

    // Remove an array item by value
    pb.remove('/arrayItems/byVal[. = "DELETE"]'),

    // Remove an object-valued array item by property name
    pb.remove('/arrayItems/byPropName["DELETE"]')
  ).result();
}).then(function(response) {
  // (3) Read the patched document
  return db.documents.read(response.uri).result();
}).then(function(documents) {
  console.log(JSON.stringify(documents[0].content, null, 2));
}, function(error) {  
  console.log(error); throw error;
});

The following table shows how applying the patch changes the target document.

Before Update After Update
{ "props": {
    "anyType": [1, 2],
    "objOrLiteral": "anything",
    "arrayVal": [3, 4]
  },
  "arrayItems": {
    "byPos": ["DELETE", "PRESERVE"],
    "byVal": ["DELETE", "PRESERVE"],
    "byPropName": [ 
      {"DELETE": 5}, 
      {"PRESERVE": 6} 
    ],
    "all": ["DELETE1", "DELETE2"]
  }
}
{ "props": { },
  "arrayItems": {
    "byPos": [ "PRESERVE" ],
    "byVal": [ "PRESERVE" ],
    "byPropName": [
      { "PRESERVE": 6 }
    ],
    "all": [ ]
  }
}

Note that when removing properties, you must either use a named node step to identify the target property or be aware of the value type. Consider these 3 operations from the example program:

pb.remove('/props/node("anyType")'),
pb.remove('/props/objOrLiteral'),
pb.remove('/props/array-node("arrayVal")')

The first operation uses a type agnostic XPath expression, /props/node("anyType"). This expression selects any nodes named anyType, regardless of the type of value. The second operation uses the XPath expression /props/objOrLiteral, which selects the array item values, rather than the containing array node. That is, this operation applied to the original document will delete the contents of the array, not the arrayVal property:

pb.remove('/props/arrayVal')
==> "arrayVal": [ ]

The third form, /props/array-node("arrayVal"), deletes the arrayVal property, but it will only work on properties with array type. Therefore, if you need to delete a property by name without regard to its type, use an XPath expression of the form /path/to/parent/node("propName").

For more details on the JSON document model and traversing JSON documents with XPath, see Working With JSON in the Application Developer's Guide.

Example: Patching Metadata

This example demonstrates patching document metadata. The examples performs the following patch operations:

  • Add a document to a collection.
  • Remove a document from a collection.
  • Modify a permission.
  • Add a metadata property.
  • Set the quality.
  • Add a new values metdata key-value pair.
  • Remove a values metadata key-value pair.
  • Replace the value of a values metadata key-value pair.

This example uses the metadata patching helper interfaces on patchBuilder: patchBuilderCollections, patchBuilderPermissions, patchBuilderProperties, patchBuilderQuality, and patchBuilder.metadataValues. You can also patch metadata directly, but using the helper interfaces frees you from understanding the layout details of how MarkLogic stores metadata internally.

To run the example, copy the following code into a file, then supply it to the node command. For example: node metadata.js. Note that the parameter passed to db.docments.patch must include a categories property that indicates the patch should be applied to metadata rather than content.

const marklogic = require('marklogic');
const my = require('./my-connection.js');
const db = marklogic.createDatabaseClient(my.connInfo);

// (1) Ensure the example document doesn't already exist
db.documents.remove('/patch/metadata-example.json')
  .result().then( function() {
  
// (2) Insert the base document into the database
db.documents.write({
  uri: '/patch/metadata-example.json',
  categories: ['content', 'metadata'],
  contentType: 'application/json',
  content: { key: 'value' },
  collections: [ 'initial1', 'initial2' ],
  permissions: [ {
    'role-name': 'app-user',
    capabilities: ['read']
  } ],
  properties: {
    myProp1: 'some-value',
    myProp2: 'some-other-value'
  },
  metadataValues: {
    key1: 'value1',
    key2: 2
  }
}).result().then(function(response) {
  // (3) Patch the document
  const pb = marklogic.patchBuilder;
  return db.documents.patch({
    uri: response.documents[0].uri, 
    categories: [ 'metadata' ],
    operations: [
      // Add a collection
      pb.collections.add('INSERTED'),

      // Remove a collection
      pb.collections.remove('initial1'),

      // Add a capability to a role
      pb.permissions.replace(
          {'role-name': 'app-user',
           capabilities: ['read','update']}),

      // Add a property
      pb.properties.add('myProp3', 'INSERTED'),

      // Change the quality
      pb.quality.set(2),

      // Add new values metadata
      pb.metadataValues.add('key3', 'INSERTED'),

      // Remove a values metadata key-value pair
      pb.metadataValues.remove('key2'),

      // Replace the value of a values metadata key
      pb.metadataValues.replace('key1', 'REPLACED')
    ]
  }).result();
}).then(function(response) {
  // (4) Emit the resulting metadata
  return db.documents.read({ 
    uris: [ response.uri ],
    categories: [ 'metadata']
  }).result();
}).then(function(documents) {
  console.log('collections: ' + 
              JSON.stringify(documents[0].collections, null, 2));
  console.log('permissions: ' + 
              JSON.stringify(documents[0].permissions, null, 2));
  console.log('properties: ' + 
              JSON.stringify(documents[0].properties, null, 2));
  console.log('quality: ' + 
              JSON.stringify(documents[0].quality, null, 2));
  console.log('metadataValues: ' + 
              JSON.stringify(documents[0].metadataValues, null, 2));
}, function(error) { console.log(error); throw error; });
});

The output from the example script should be similar to the following:

collections: [
  "initial2",
  "INSERTED"
]
permissions: [
  {
    "role-name": "app-user",
    "capabilities": [
      "read",
      "update"
    ]
  },
  {
    "role-name": "rest-writer",
    "capabilities": [
      "update"
    ]
  },
  {
    "role-name": "rest-reader",
    "capabilities": [
      "read"
    ]
  }
]
properties: {
  "myProp1": "some-value",
  "myProp2": "some-other-value",
  "myProp3": "INSERTED"
}
quality: 2
metadataValues: {
  "key1": "REPLACED",
  "key3": "INSERTED"
}

Note that the patch operation that adds the update capability to the app-user role replaces the entire permission, rather than attempting to insert update as a new value in the capabilities array. You must operate on each permission as a unit. You cannot modify selected components, such as role-name or capabilities.

// Add a capability to a role
pb.permissions.replace(
          {'role-name': 'app-user',
           capabilities: ['read','update']})

Also, notice that the rest-writer and rest-reader roles are assigned to the document, even though they are never explicitly specified. All documents created using the Node.js, Java, or REST Client APIs have these roles by default. For details, see Security Requirements.

Creating a Patch Without a Builder

The examples in this chapter use marklogic.PatchBuilder to construct a patch using JSON. You can also construct a raw patch without a builder and pass it to db.documents.patch as either a JavaScript object or a string.

You must use a raw patch when patching content for XML documents. For details, see Patching XML Documents.

The syntax for raw XML and JSON patches is covered in detail in Partially Updating Document Content or Metadata in the REST Application Developer's Guide.

The following call to db.documents.patch applies a raw JSON patch that insert an array element:

db.documents.patch(response.documents[0].uri,
    { patch: [ {
        insert: [ {
          context: '/theTop/child[2]', 
          position: 'after',
          content: 'three'
    } ] } ] }
);

In a raw patch, each type of update (insert, replace, replace-insert, remove) is an array of objects, with each object describing one operation of that type. The output from a call to a patch builder operation represents one such operation.

For example, the insert operation in the raw patch above is equivalent to the following patch builder call:

pb.insert('/theTop/child[2]', 'after', 'three')

The patch builder functions correspond to raw patch operations as follows:

PatchBuilder function Raw patch equivalent
insert(
  context, position, 
  content, cardinality)
"insert": [ ...,
  { "context": ...,
    "position": ...,
    "content": ...,
    "cardinality": ...
  }, ...
]
replace(select, content, cardinality)
"replace": [ ...,
  { "select": ...,
    "content": ...
    "cardinality": ...
  }, ...
]
replaceInsert(
  select, context, 
  position, content, 
  cardinality)
"replace-insert": [ ...,
  { "select": ...,
    "context": ...,
    "position": ...,
    "content": ...
    "cardinality": ...
  }, ...
]
remove(select, cardinality)
"delete": [ ...,
  { "select": ...,
    "cardinality": ...
  }, ...
]

Patching XML Documents

You must use a raw XML patch when patching content for XML documents. The patch builder only constructs JSON patch operations, and a JSON patch can only be applied to a JSON document.

You can pass a raw XML patch as a string to db.documents.patch. The syntax for raw XML patches is covered in detail in XML Patch Reference in the REST Application Developer's Guide.

The following example applies a raw XML patch that inserts a new element as a child of another. The patch is passed as a string in the second parameter of db.documents.patch.

const marklogic = require('marklogic');
const my = require('./my-connection.js');
const db = marklogic.createDatabaseClient(my.connInfo);

// (1) Insert the base document into the database
db.documents.write({
  uri: '/patch/raw-patch2.xml',
  contentType: 'application/xml',
  content: '<parent><child>data</child></parent>'
}).result().then(function(response) {
  // (2) Patch the document
  return db.documents.patch(
    response.documents[0].uri, 
    '<rapi:patch xmlns:rapi="http://marklogic.com/rest-api">' +
    '  <rapi:insert context="/parent" position="last-child">' +
    '    <new-child>INSERTED</new-child>' +
    '  </rapi:insert>' +
    '</rapi:patch>'
  ).result();
}).then(function(response) {
  // (3) Emit the resulting document
  return db.documents.read(response.uri).result();
}).then(function(documents) {
  console.log(documents[0].content);
}, function(error) { console.log(error); throw error; });

The following table shows the document transformation applied by the patch:

Before Update After Update
<parent>
  <child>data</data>
</parent>
<parent>
  <child>data</data>
  <new-child>INSERTED</new-child>
</parent>

For another Node.js example, see Example: Custom Replacement Constructors. For more details, see Partially Updating Document Content or Metadata in the REST Application Developer's Guide.

Constructing Replacement Data on MarkLogic Server

You can use builtin or custom replacement constructor functions to generate the content for a patch operation dynamically on MarkLogic Server. The builtin functions support simple arithmetic and string manipulation. For example, you can use a builtin function to increment the current value of numeric data or concatenate strings. For more complex operations, create and install a custom function.

The following topics are covered:

Overview of Replacement Constructor Functions

A replacement constructor function is a server-side function that generates content for a patch replace or replace-insert operation.

You can use replacement constructor functions when creating a patch operation using PatchBuilder.replace and PatchBuilder.replaceInsert functions, or the replace and replace-insert operations of a raw patch. The replacement constructor function call specification takes the place of replacement content supplied by your application. The replacement constructor function call is evaluated on MarkLogic Server and usually generates new content relative to the current value.

For example, you could use the builtin PatchBuilder.multiplyBy operation to increase the current value of a property by 10%. The following replace operations says For every value selected by the XPath expression /inventory/price, multiply the current value by 1.1. Notice that the multiplyBy result is passed to PatchBuilder.replace instead of new content.

pb.replace('/inventory/price', pb.multiplyBy(1.1))

The builtin replacement constuctors are available as methods of PatchBuilder. For details, see Using a Builtin Replacement Constructor.

You can also use custom replacement constructors by calling PatchBuilder.library and PatchBuilder.apply. Use PatchBuilder.library to identify a server side XQuery library module that contains your replacement constructor functions, then use PatchBuilder.apply to create a patch operation that invokes a function in that module.

For example, the following code snippet creates a patch replace operation that doubles the price of every value selected by the XPath expression /inventory/price. The custom dbl function is implemented by the XQuery library module installed in the modules database with URI /ext/marklogic/patch/apply/my-lib.xqy. The dbl function does not expect any argument values, so there is no additional content supplied to PatchBuilder.apply.

pb.library('my-lib'),
pb.apply('dbl')

For details, see Writing a Custom Replacement Constructor.

For details on using replacement generator functions in a raw patch, see Constructing Replacement Data on the Server in the REST Application Developer's Guide.

Using a Builtin Replacement Constructor

The Node.js Client API includes several builtin server-side functions you can use to dynamically generate the content for a replace or replaceInsert patch operation. For example, you can use a builtin function to increment the current value of a data item.

The builtin arithmetic functions are equivalent to the XQuery +, -, *, and div operators, and accept values castable to the same datatypes. That is, numeric, date, dateTime, duration, and Gregorian (xs:gMonth, xs:gYearMonth, etc.) values. The operand type combinations are as supported by XQuery; for details, see http://www.w3.org/TR/xquery/#mapping. All other functions expect values castable to string.

The PatchBuilder interface includes methods corresponding to each builtin function. If you use a raw patch rather than the patch builder, see Constructing Replacement Data on the Server in the REST Application Developer's Guide.

The table below lists the available builtin replacement constructor functions. In the table, $current represents the current value of the target of the replace operation; $arg and $argN represent argument values passed in by the patch. For details, see the Node.js API Reference. The Apply Operation Name column lists the name of the equivalent operation for use with patchBuilder.apply.

PatchBuilder Function Apply Operation Name Num Args Effect
add
ml.add
1 $current + $arg
subtract
ml.subtract
1 $current - $arg
multiplyBy
ml.multiply
1 $current * $arg
divideBy
ml.divide
1 $current div $arg
concatBefore
ml.concat-before
1 fn:concat($arg, $current)
concatAfter
ml.concat-after
1 fn:concat($current, $arg)
concatBetween
ml.concat-between
2 fn:concat($arg1, $current, $arg2)
substringBefore
ml.substring-before
1 fn:substring-before($current, $arg)
substringAfter
ml.substring-after
1 fn:substring-after($current, $arg)
replaceRegex
ml.replace-regex
2 or 3 fn:replace($current, $arg1,            $arg2, $arg3)

Passing Parameters to a Replacement Constructor

When using a patch builder to construct a call to a builtin or custom replacement constructor, simply pass the expected arguments to the patchBuilder method.

For example, patchBuilder.concatBetween concatenates each selected value between two strings supplied as input. Therefore, the concatBetween method takes the two input string as arguments. The following example concatenates the strings fore and aft on either side of the current value of a selected data item.

pb.replace('/some/path/expr', 
           pb.concatBetween('fore', 'aft'))

In a raw patch, you supply the input argument values in the content property of the operation. For details, see Using a Replacement Constructor Function in the REST Application Developer's Guide.

Use patchBuilder.apply and patchBuilder.datatype to explicitly specify the datatype of an input argument value. You can choose among the types supported by the XML schema; for details, see http://www.w3.org/TR/xmlschema-2/#built-in-datatypes. Omit the namespace prefix on the type name.

For example, the following call explicitly specifies xs:long as the datatype for the input value to the raw patch operation equivalent of calling patchBuilder.multiplyBy. For a list of the builtin function names usable with apply, see the table in Using a Builtin Replacement Constructor.

pb.replace('/some/path/expr', 
  p.apply('ml.add', p.datatype('long'), '9223372036854775807'))

Using a Custom Replacement Constructor

Before you can use a custom replacement constructor, the XQuery library module containing your constructor implementation must be installed in the modules database of your REST API instance. For details, see Installing or Updating a Custom Replace Library.

To bring a library of custom replacement constructor functions into scope for a patch operation, include the result of calling PatchBuilder.library in your patch. The library operation tells the API how to locate your implementation on MarkLogic Server.

For example, the following library call indicates that any custom replacement constructor functions used by the patch are in the XQuery module with modules database URI /ext/marklogic/patch/apply/my-replace-lib.xqy.

pb.patch('/some/doc.json',
  pb.library('my-replace-lib.xqy'),
  ...)

You can only use one such library per patch, but you use multiple functions from the same library.

Use PatchBuilder.apply to construct a call to a function in your library. For example, if my-replae-lib.xqy contains a function called dbl, you can use by including the result of the following call in your patch:

pb.patch('/some/doc.json',
  pb.library('my-replace-lib.xqy'),
  pb.apply('dbl')
  /* additional patch operations */)

If the function expects input arguments, include them in the apply call:

pb.apply('doSomething', 'arg1Val', 'arg2Val')

You are responsible for passing in values of the type(s) expected by the named function. No type checking is performed for you.

Writing a Custom Replacement Constructor

This section covers requirements for implementing a custom replacement constructor. For an example implementation, see Example: Custom Replacement Constructors.

You can create your own functions to generate content for the replace and replace-insert operations using XQuery. A custom replacement generator function has the following XQuery interface:

declare function module-ns:func-name(
  $current as node()?,
  $args as item()*
) as node()*

The target node of the replace (or replace-insert) operation is provided in $current. If the function is invoked as an insert operation on behalf of a replace-insert, then $current is empty.

The argument list supplied by the operation is passed through $args. You are responsible for validating the argument values. If the arguments supplied by the patch operation is a JSON array or a sequence of XML <rapi:value/> elements, then $args is the result of applying the fn:data function to each value. If an explicit datatype is specified by the patch operation, the cast is applied before invoking your function.

Your function should report errors using fn:error and RESTAPI-SRVEXERR. For details, see Error Reporting in Extensions and Transformations.

To use a patch builder to construct references to your module, you must adhere to the following namespace and installation URI convention:

  • Your module must be in the namespace http://marklogic.com/patch/apply/yourModuleName.
  • Your module must be installed in the modules database under a URI of the form /ext/marklogic/patch/apply/yourModuleName. This happens automatically if you install your module using db.config.patch.replace.write.

For example, if your library module includes the following module namespace declaration:

module namespace my-lib = "http://marklogic.com/patch/apply/my-lib";

And is installed in the modules database with the following URI:

/ext/marklogic/patch/apply/my-lib.xqy

Then the following patch successfully applies the function dbl in my-lib.xqy:

pb.patch('/some/doc.json',
  pb.library('my-lib.xqy'),
  pb.replace('/some/path/expr', pb.apply('dbl')),
  ...)

This shorthand convention does not apply to raw patches. A raw patch must explicitly specify the complete namespace and module path in the replace-library directive, even if you follow the convention naming convention.

Installing or Updating a Custom Replace Library

To install or update custom replacement constructor functions, place your function(s) into an XQuery library module and install the module and any dependent libraries in the modules database associated with your REST API instance.

Use config.patch.replace.write to install your module(s). This function ensures your module is installed using the modules database URI convention expected by PatchBuilder. You must ensure your module uses the required namespace convention; for details, see Writing a Custom Replacement Constructor.

For example, the following script installs a module into the modules database with the URI /ext/marklogic/patch/apply/my-lib.xqy. The implementation is read from a file with pathname ./my-lib.xqy. The module is executable by users with the app-user role.

const fs = require('fs');
const marklogic = require('marklogic');
const my = require('./my-connection.js');
const db = marklogic.createDatabaseClient(my.connInfo);

db.config.patch.replace.write('my-lib.xqy',
  [{'role-name': 'app-user', capabilities: ['execute']}],
  fs.createReadStream('./my-lib.xqy')
).result(function(response) {
  console.log('Installed module ' + response.path);
}, function(error) {
  console.log(JSON.stringify(error, null, 2));
});

Note that only the module name portion of the URI ('my-lib.xqy') is passed in. The remainder of the expected URI is constructed for you.

If you do not specify any permissions when writing the implementation to the modules database, the module is only executable by users with the rest-admin role.

For an end-to-end example, see Example: Custom Replacement Constructors.

If your library module requires dependent libraries, you can install them using the extlibs interface. The extlibs interface allows you to manage modules database assets at both the directory and file level. For details, see Managing Assets in the Modules Database.

Calling db.config.patch.replace.write is equivalent to calling db.config.extlibs.write and setting the path parameter to a value that conforms to the PatchBuilder convention. For example, the following call performs an installation equivalent to the above use of db.config.patch.replace.write:

db.config.extlibs.write({
  path: '/marklogic/patch/apply/my-lib.xqy',
  permissions: [
    {'role-name': 'app-user', capabilities: ['execute']}
  ],
  contentType: 'application/xquery',
  source: fs.createReadStream('./my-lib.xqy')
})

Uninstalling a Custom Replace Library

To remove a module that contains custom replacement constructor functions, use db.config.patch.replace.remove. For example, the following call removes the module my-lib.xqy that was previously installed using db.config.patch.replace.write:

db.config.patch.replace.remove('my-lib.xqy');

Example: Custom Replacement Constructors

This example walks you through installing and using an XQuery library module that contains custom replacement constructor functions.

The example installs an XQuery library module containing 2 custom replacement constructors, named dbl and min. The dbl function creates a new node whose value is double that of the original input; it accepts no additional arguments. The min function creates a new node whose value in the minimum of the current value and the values passed in as additional arguments.

For simplicity, this example skips most of the input data validation that a production implementation should include. For example, the min function accepts JSON number nodes and XML elements as input, but it does not allow for boolean, text, or date input. Nor does min perform any validation on the additional input args.

After installing the replacement content generators, a patch is applied to double the value of oranges (from 10 to 20) and select the lowest price for pears.

Use the following procedure to set up the files used by the example:

  1. Copy the following XQuery module into a file named my-lib.xqy. This file contains the implementation of the custom replacement constructors.
    xquery version "1.0-ml";
    
    module namespace my-lib = "http://marklogic.com/patch/apply/my-lib";
    
    (: Double the value of a node :)
    declare function my-lib:dbl(
      $current as node()?,
      $args as item()*
    ) as node()*
    {
      if ($current/data() castable as xs:decimal)
      then
        let $new-value := xs:decimal($current) * 2
        return
          typeswitch($current)
          case number-node()       (: JSON :)
            return number-node {$new-value}
          case element()           (: XML :)
            return element {fn:node-name($current)} {$new-value}
          default return fn:error((), "RESTAPI-SRVEXERR",
             ("400", "Bad Request", 
                fn:concat("Not an element or number node: ",
                          xdmp:path($current))
                ))
      else fn:error((), "RESTAPI-SRVEXERR",
          ("400", "Bad Request", fn:concat("Non-decimal data: ", $current)
        ))
    };
    
    (: Find the minimum value in a sequence of value composed of :)
    (: the current node and a set of input values. :)
    declare function my-lib:min(
      $current as node()?,
      $args as item()*
    ) as node()*
    {
      if ($current/data() castable as xs:decimal)
      then
        let $new-value := fn:min(($current, $args))
        return
          typeswitch($current)
          case element()           (: XML :)
            return element {fn:node-name($current)} {$new-value}
          case number-node()       (: JSON :)
            return number-node {$new-value}
          default return fn:error((), "RESTAPI-SRVEXERR",
            ("400", "Bad Request", 
               fn:concat("Not an element or number node: ",
                         xdmp:path($current))
               ))
      else fn:error((), "RESTAPI-SRVEXERR", ("400", "Bad Request",
        fn:concat("Non-decimal data: ", $current)))
    };
  2. Copy the following script into a file named install-udf.js. This script installs the above XQuery module. For demonstration purposes, the module is installed such that the role app-user has permission to execute the contained functions.
    const fs = require('fs');
    const marklogic = require('marklogic');
    const my = require('./my-connection.js');
    const db = marklogic.createDatabaseClient(my.connInfo);
    
    db.config.patch.replace.write('my-lib.xqy',
      [ {'role-name': 'app-user', capabilities: ['execute']} ],
      fs.createReadStream('./my-lib.xqy')
    ).result(function(response) {
      console.log('Installed module ' + response.path);
    }, function(error) {
      console.log(JSON.stringify(error, null, 2));
    });
  3. Copy the following script into a file named udf.js. This script inserts a base document into the database and applies a patch that uses the dbl and min replacement content constructors.
    const marklogic = require('marklogic');
    const my = require('./my-connection.js');
    const db = marklogic.createDatabaseClient(my.connInfo);
    
    // (1) Insert the base document into the database
    db.documents.write({
      uri: '/patch/udfs.json',
      contentType: 'application/json',
      content: {
        inventory: [
          {name: 'orange', price: 10},
          {name: 'apple', price: 15},
          {name: 'pear', price: 20}
      ] }
    }).result().then(function(response) {
      // (2) Patch the document
      const pb = marklogic.patchBuilder;
      return db.documents.patch(response.documents[0].uri,
        pb.library('my-lib.xqy'),
        pb.replace('/inventory[name eq "orange"]/price', pb.apply('dbl')),
        pb.replace('/inventory[name eq "pear"]/price',
                   pb.apply('min', 18, 21))
      ).result();
    }).then(function(response) {
      // (3) Emit the resulting document
      return db.documents.read(response.uri).result();
    }).then(function(documents) {
      console.log(JSON.stringify(documents[0].content, null, 2));
    }, function(error) { console.log(error); throw error; });

Use the following procedure to run the example. This procedure installs the replacement content constructor module, inserts a base document in the database, patches the document, and displays the update document contents.

  1. Install the replacement content constructor module:
    node install-udf.js
  2. Insert and patch a document, using the dbl and min functions.
    node udf.js

You should see output similar to the following:

{
  "inventory": [
    {
      "name": "orange",
      "price": 20
    },
    {
      "name": "apple",
      "price": 15
    },
    {
      "name": "pear",
      "price": 18
    }
  ]
}

The price of oranges is doubled, from 10 to 20 by the dbl function, due to the following patch operation:

pb.replace('/inventory[name eq "orange"]/price', pb.apply('dbl'))

The value of pears is lowered from 20 to 18 by the min function, due to the following patch operation:

pb.replace('/inventory[name eq "pear"]/price',
           pb.apply('min', 18, 21))

The XPath expression used in each patch operation selects a number node (price) in the target document and the replacement content constructor functions construct a new number node:

typeswitch($current)
  case element()           (: XML :)
    return element {fn:node-name($current)} {$new-value}
  case number-node()       (: JSON :)
    return number-node {$new-value}

You cannot simply return the new value. You must return a complete replacement node. To learn more about the JSON document model and JSON node constructors, see Working With JSON in the Application Developer's Guide.

The typeswitch on the node type of $current also enables the example replacement constructors to work with XML input. Run the following script to apply an equivalent to an XML document, using the previously installed dbl and min replacement content constructors. As described in Patching XML Documents, you must use a raw patch rather than a builder when working with XML documents.

const marklogic = require('marklogic');
const my = require('./my-connection.js');
const db = marklogic.createDatabaseClient(my.connInfo);

// (1) Insert the base document into the database
db.documents.write({
  uri: '/patch/udf.xml',
  contentType: 'application/xml',
  content:
    '<inventory>' +
      '<item>' +
        '<name>orange</name>' +
        '<price>10</price>' +
      '</item>' +
      '<item>' +
        '<name>apple</name>' +
        '<price>15</price>' +
      '</item>' +
      '<item>' +
        '<name>pear</name>' +
        '<price>20</price>' +
      '</item>' +
    '</inventory>'

}).result().then(function(response) {
  // (2) Patch the document
  return db.documents.patch(
    response.documents[0].uri, 
    '<rapi:patch xmlns:rapi="http://marklogic.com/rest-api">' +
      '<rapi:replace-library ' +
          'at="/ext/marklogic/patch/apply/my-lib.xqy" ' +
          'ns="http://marklogic.com/patch/apply/my-lib" />' +
      '<rapi:replace ' +
          'select="/inventory/item[name eq \'orange\']/price" ' +
          'apply="dbl" />' +
      '<rapi:replace ' +
          'select="/inventory/item[name eq \'pear\']/price" ' +
          'apply="min">' +
        '<rapi:value>18</rapi:value>' +
        '<rapi:value>21</rapi:value>' +
      '</rapi:replace>' +
    '</rapi:patch>'
  ).result();
}).then(function(response) {
  // (3) Emit the resulting document
  return db.documents.read(response.uri).result();
}).then(function(documents) {
  console.log(documents[0].content);
}, function(error) { console.log(error); throw error; });

Notice that in a raw patch, you must explicitly specify the module path and module namespace in the replace-library directive:

<rapi:replace-library at="/ext/marklogic/patch/apply/my-lib.xqy"
                      ns="http://marklogic.com/patch/apply/my-lib" />'

When you use PatchBuilder to construct a JSON patch, the call to PatchBuilder.library fills these details in for you, assuming you follow the installation path conventions described in Installing or Updating a Custom Replace Library.

Additional Operations

The db.config.patch.replace interface offers additional methods for managing library modules containing replacement content constructor functions, including the following:

  • db.config.patch.replace.read - Retrieve the implementation of a replace library. This returns a module installed using db.config.patch.replace.write.
  • db.config.patch.replace.list - Retrieve a list of all installed replace library modules.

For details, see Node.js Client API Reference.

« Previous chapter
Next chapter »