Loading TOC...
REST Application Developer's Guide (PDF)

REST Application Developer's Guide — Chapter 9

Extending the REST API

You can extend the REST Client API in a variety of ways, including resource service extensions and evaluation of ad-hoc queries and server-side modules. This chapter covers the following topics:

Available REST API Extension Points

The REST Client API offers several ways to extend and customize the available services with user-defined code that is either pre-installed on MarkLogic Server or supplied at request time. The following extension points are available:

  • Content transformations: A user-defined transform function can be applied when documents are written to the database or read from the database; for details, see Working With Content Transformations. You can also define custom replacement content generators for the patch feature; for details, see Constructing Replacement Data on the Server.
  • Search result customization: Customization opportunities include constraint parsers for string queries, search result snippet generation, and search result customization. For details, see Customizing Search Results and the Search Developer's Guide.
  • Resource service extensions: Define your own REST endpoints, accessible through GET, PUT, POST and/or DELETE requests. Resource service extensions are covered in detail in this chapter.
  • Ad-hoc query execution: Send an arbitrary block of XQuery or JavaScript code to MarkLogic Server for evaluation. For details, see Evaluating an Ad-Hoc Query.
  • Server-side module evaluation: Evaluate user-defined XQuery or JavaScript modules pre-installed on MarkLogic Server. For details, see Evaluating a Module Installed on MarkLogic Server.

Understanding Resource Service Extensions

Use the resource service extension capability to make new REST service endpoints available through a REST Client API instance. To make a resource service extension available, follow these steps, which are discussed in detail in this chapter.

  1. Create a JavaScript or XQuery implementation of your extension. For details, see Creating a JavaScript Resource Service Extension or Creating an XQuery Resource Service Extension.
  2. Install the extension in a REST Client API instance to make it available to applications. For details, see Installing a Resource Service Extension.
  3. Access the extension service through a /resources/{extensionName} service endpoint. For details see Using a Resource Service Extension.

The /resources service also supports dynamic discovery of installed extensions. When you install an extension, you can specify extension metadata, including method parameter name and type information to make it easier to use dynamically discovered extensions.

If your extension depends on other modules or assets, you can install them in the modules database using the /ext service. For details, see Managing Dependent Libraries and Other Assets.

JavaScript modules are not available from REST extensions because resource service extensions are imported dynamically by the REST API. If your work requires the use of JavaScript library modules, the preferred method is to use Data Service endpoints rather than resource service extensions. See Creating Data Services and Developer Actions in Node.js for further information.

Creating a JavaScript Resource Service Extension

This section covers the following topics related to authoring a Resource Service Extension using server-side JavaScript:

Guidelines for Writing JavaScript Resource Service Extensions

Keep the following guidelines in mind when authoring a resource extension using JavaScript:

  • You must implement your extension using MarkLogic Server-Side JavaScript. For details, see the JavaScript Reference Guide.
  • Methods such as put and post that accept an input document receive an immutable in-memory representation. MarkLogic Server builtin functions that operate on in-database documents, such as xdmp.nodeReplace, cannot be applied directly to the input document. To modify the structure of the input document, you must create a mutable instance of the document by calling toObject on it.
  • When the client sends a single document, the input parameter passed to your implementation is a document node. When the client sends multiple documents, the input parameter is a Sequence over document nodes. You should be prepared to handle either.
  • Methods that can return multiple documents must return a Sequence over document nodes or JavaScript objects. Use Sequence.from to create a Sequence from an array.
  • Your code executes in the transaction context of the HTTP request on whose behalf it is called. You should not commit or rollback the current transaction. You can use xdmp.invokeFunction, xdmp.eval, and xdmp.invoke to perform operations in a different transaction.
  • If your extension depends on other user-defined modules, those modules must be installed in the modules database of the REST Client API instance. For details, see Managing Dependent Libraries and Other Assets.
  • Report errors back to the client application using fn.error, not through an Error object or other mechanism. For details, see Reporting Errors.

Many of these points are illustrated by Example: JavaScript Resource Service Extension.

The JavaScript Resource Extension Interface

To create a resource service extension using server-side JavaScript, implement a module based on the template below. The module exports and function signatures must be exactly as they appear in the template, with the following exceptions:

  • You only need to implement and export functions for the HTTP verbs supported by the extension.

Base your extension implementation on the following template.

function get(context, params) {
  // return zero or more document nodes
};

function post(context, params, input) {
  // return zero or more document nodes
};

function put(context, params, input) {
  // return at most one document node
};

function deleteFunction(context, params) {
  // return at most one document node
};

exports.GET = get;
exports.POST = post;
exports.PUT = put;
exports.DELETE = deleteFunction;

The following template summarizes the meaning of the function parameters passed to your implementation functions:

Parameter Description
context An object containing service request context information such as input document types and URIs, and output types accepted by the caller. For details, see Context Object Properties.
params

An object containing extension-specific parameter values supplied by the client, if any. The object contains a property for each resource parameter included in the request to the extension service.

Properties specified more than once by the caller have array value. For example: If the request is PUT /v1/resources/my-ext?rs:p=1&rs:p=2, then the value of params.p is ["1", "2"].

You can optionally define parameter name and type metadata when installing the extension. For details, see Installing a Resource Service Extension.

input The data from the request body. For a request with only part, input is a single document node. For a multipart request, input is a Sequence over document nodes.

The input parameter can be a single document node or a Sequence, depending on whether the request body contains a single part or multiple parts. You can normalize the input by converting the single document to a Sequence, if desired. For example:

function normalizeInput(item)
{
  return (item instanceof Sequence)
         ? item                     // many
         : Sequence.from([item]);   // one
};

The input document(s) are immutable. Use toObject to create a mutable copy. For example, assuming input a single JavaScript object created from passing JSON data in the request body, the following code enables you to modify the input object:

const modifiableDoc = input.toObject();
modifiableDoc.newProperty = someValue;

Your methods can accept input arguments in addition to the input documents in the request body. Arguments are passed to your methods through the params object. For an example, see the get implementation in Example: JavaScript Resource Service Extension.

Your extension can return JavaScript objects and XML, JSON, text, or binary document nodes. A JavaScript object is serialized as JSON before returning it to the client. Set context.outputTypes in the $context map to the MIME type appropriate for the returned content. Except for the JavaScript to JSON conversion, the content is returned as-is, with the MIME type you specify.

To return multiple documents, create a Sequence over your JavaScript objects or document nodes. Use xdmp.arrayValues or Sequence.from to create a Sequence from an array. For example, the following code snippet results in a multi-part response containing 2 parts, with each part containing a JSON document.

context.outputTypes = [ 'application/json', 'application/json' ];
return Sequence.from([{one: 1}, {two:2}];

If you want to report errors back to the client application, you must use the fn.error function, with a very specific calling convention. For details, see Reporting Errors.

Reporting Errors

This section describes how to report errors back to the client application. Extensions and transforms use the same mechanism to report errors to the calling application. If you attempt to return an error in any other fashion, such as through an Error object, the client just receives a 500 Internal Error response.

To report an error from your implementation, call the fn.error function with the RESTAPI-SRVEXERR error code, and provide the HTTP response code and additional error information in the 3rd parameter to fn.error. Calling fn.error interrupts execution of your code. Control does not return.

Best practice is to use RESTAPI-SRVEXERR. If you report any other error or raise any other exception, it is reported to the calling application as a 500 Server Internal Error.

The 3rd parameter to fn:error should be a Sequence containing a sequence of the form (status-code, status-message, response-payload). You can use xdmp.arrayValues or Sequence.from to construct a sequence from a JavaScript array.

For example, if you wrap the fn.error call in a function, then you can call it as shown below:

function returnErrToClient(statusCode, statusMsg, body)
{
  fn.error(null, 'RESTAPI-SRVEXERR', 
           Sequence.from([statusCode, statusMsg, body]));
  // unreachable - control does not return from fn.error.
};
// ... use the func ...
returnErrToClient(400, 'Bad Request', 'additional info');
// unreachable

If you include an error response payload, it should either be plain text or compatible with the error format configured for the REST instance. You cannot override the default error payload format.

Setting the Response Status Code

By default, your resource service extension does not need to specify the HTTP response status code. The default behavior is as follows:

  • If your implementation function returns content, MarkLogic Server responds with a status 200.
  • If your function raises no errors but does not return content, MarkLogic Server responds with status 204.
  • If your function raises an error, the response code depends on the error; for details, see Reporting Errors.

To return a status code different from the default, set the numeric code and message in the context object. Use the outputStatus property with [code, message] ts value.

Do not use output-status to report an error to the client. Instead, use fn.error. For details, see Reporting Errors.

The following example returns a 201 Created status to the client instead of 200 OK:

function put(context, params, input)
{
    ...
    context.outputStatus = [201, 'Created'];
    ...
};

Setting Additional Response Headers

The REST API sets headers such as Content-type and Content-Length for you when constructing the response from a resource extension request. You can specify additional headers by setting context.outputHeaders.

Do not use context.outputHeaders to set the response Content-type header. Use context.outputTypes to specify the response MIME type(s) instead.

If you use context.outputHeaders, the value should be an object of the following form:

{header-name : header-value, header-name : header-value, ...}

The following example adds headers named X-MyHeader1 and X-MyHeader2 to the response by setting context.outputHeaders.

function put(context, params, input)
{
  ...
  context.outputHeaders = 
    {"X-MyHeader1" : "header1 value", "X-MyHeader2" : "header2 value"};
  ...
};

The resulting response headers are similar to the following:

HTTP/1.1 200 OK
X-MyHeader1: header1 value
X-MyHeader2: header2 value
Server: MarkLogic
Content-Type: text/plain; charset=UTF-8
Content-Length: 4
Connection: Keep-Alive
Keep-Alive: timeout=5

Context Object Properties

The context parameter to resource service extension functions is an object that can contain the following properties. Unless otherwise noted, all the property values are arrays. Not all properties are always present. For example, the inputTypes property is not meaningful for a GET request.

Key Description
inputTypes The MIME type of the input document(s). There is an array element for each item in the input parameter passed to your method implementation.
acceptTypes The MIME types accepted by the requestor.
outputHeaders Extra headers to include in the response to the client. Optional. Specify headers as a JavaScript object of the form: {header1: value1, header2: value2, ...}. For details, see Setting Additional Response Headers.
outputStatus The HTTP response status code and message to return to the client. Optional. For example, to return a 201 Created status, set outputStatus to [201, "Created"]. For details, see Setting the Response Status Code.
outputTypes The output document MIME type(s). The service must set outputTypes for all output documents.

You can create a resource extension to probe context. For example, the following extension probes the context and params parameters of PUT extension requests:

function put(context, params, input) {
  context.outputTypes = ["application/json"];
  return { context: context, params: params };
};
exports.PUT = put;

If the above example is installed as an extension named dumper, then you can probe the context as follows. There are two items in context.inputTypes because the multipart body contained one JSON part and one XML part.

curl --anyauth --user user:password -X PUT -i \
    -H "Content-type: multipart/mixed; boundary=BOUNDARY" 
    -H "Accept: application/json" \
    --data-binary @./my-multipart-body \
    http://localhost:8000/LATEST/resources/dumper?rs:myparam=foo

HTTP/1.1 200 OK
Content-type: application/json; charset=UTF-8
Server: MarkLogic
Content-Length: 176
Connection: Keep-Alive
Keep-Alive: timeout=5

{"context": {
    "acceptTypes": [],
    "inputBoundary": "BOUNDARY",
    "inputTypes": [
      "application/json",
      "application/xml"
    ],
    "outputTypes": [ "application/json" ]
  },
  "params": { "myparam": "foo" }
}

For a complete example of a resource service extension, see Example: JavaScript Resource Service Extension.

Controlling Transaction Mode

In a JavaScript extension, you cannot control the transaction mode in which your get, put, post, or delete function runs. Transaction mode is controlled by the REST API App Server.

  • GET - query for a single statement transaction, update for a multi-statement transaction; that is, if your request includes a transaction id, the function runs in update mode.
  • PUT - update
  • POST - query for a single statement transaction, update for a multi-statement transaction; that is, if your request includes a transaction id, the function runs in update mode.
  • DELETE - update

If you need to execute code in a transaction context different from the default, use one of the following options:

To learn more about transaction mode and the MarkLogic transaction model, see Understanding Transactions in MarkLogic Server in the Application Developer's Guide.

Creating an XQuery Resource Service Extension

This section covers the following topics related to authoring a Resource Service Extension using XQuery:

Guidelines for Writing XQuery Resource Service Extensions

Keep the following guidelines in mind when authoring a resource extension:

  • Methods such as put and post that accept an input document receive an in-memory representation. MarkLogic Server builtin functions that operate on in-database documents, such as xdmp:node-replace, cannot be applied directly to the input document. To modify the structure of the input document, perform the modifications during construction of a copy of the input content.
  • Your code executes in the transaction context of the HTTP request on whose behalf it is called. You should not commit or rollback the current transaction. You can use xdmp:eval and xdmp:invoke to perform operations in a different transaction.
  • If your extension depends on other user-defined modules, those modules must be installed in the modules database of the REST Client API instance. For details, see Managing Dependent Libraries and Other Assets.
  • Report errors by throwing one of the exceptions provided by the REST Client API. For details, see Reporting Errors.

The Resource Extension Interface

To create a resource service extension, implement an XQuery library module based on the template below. The module namespace, function names, and function signatures must be exactly as they appear in the template, with the following exceptions:

  • The extensionName in the module namespace is the user-defined name under which the extension is installed and by which applications access the service. Each extension should have a unique name within the REST Client API instance in which it is installed.
  • yourNSPrefix is a namespace prefix of your choosing.
  • You only need to implement functions for the HTTP verbs supported by the extension.

Base your extension implementation on the following template. You must use the function local names shown (get, put, etc.).

xquery version "1.0-ml";
module namespace yourNSPrefix =
    "http://marklogic.com/rest-api/resource/extensionName";
declare function yourNSPrefix:get(
    $context as map:map,
    $params  as map:map
) as document-node()*
declare function yourNSPrefix:put(
    $context as map:map,
    $params  as map:map,
    $input   as document-node()*
) as document-node()?
declare function yourNSPrefix:post(
    $context as map:map,
    $params  as map:map,
    $input   as document-node()*
) as document-node()*
declare function yourNSPrefix:delete(
    $context as map:map,
    $params  as map:map
) as document-node()?

The map:map type is a MarkLogic Server extended XQuery type representing a key-value map. For details, see Using the map Functions to Create Name-Value Maps in the Application Developer's Guide.

Your methods can accept input arguments. You can define the argument names and types when installing your extension; for details see Installing a Resource Service Extension. Arguments are passed to your methods through the $params map. See the get implementation in Example: XQuery Resource Service Extension.

The following template summarizes the meaning of the function parameters:

Parameter Description
$context Service request context information such as input document type and URI, and output types accepted by the caller. For details, see Context Map Keys.
$params Extension-specific parameters. The map contains a key for each parameter on a request to the extension service. You can optionally define parameters when installing the extension. For details, see Installing a Resource Service Extension.
$input The input document to which to apply the transformation.

Your extension can return XML, JSON, text, or binary documents. Set output-type in the $context map to the MIME type appropriate for the returned content. The content is returned as-is, with the MIME type you specify.

If your extension function returns data, it must be in the form document nodes. In addition to getting documents from the database or a builtin or library function, you can create documents using node constructors. The following expression demonstrate various ways of constructing XML and JSON document nodes:

(: JSON, from a serialized string representation:)
xdmp:unquote('{"from": "string"}')

(: JSON, from a node constructor :)
document { object-node {"from": "constructor"} }

(: XML from a constructor :)
document { element from { text {"constructor"} } }

(: XML from a document constructor and XML :)
document { <from>XML literal</from> },

For details, see XML and XQuery in the XQuery and XSLT Reference Guide and Working With JSON in XQuery in the Application Developer's Guide.

Reporting Errors

Extensions and transforms use the same mechanism to report errors to the calling application: Use fn:error to report a RESTAPI-SRVEXERR error, and provide additional information in the $data parameter of fn:error. You can control the response status code, status message, and provide an additional error reporting response payload. For example:

fn:error((),"RESTAPI-SRVEXERR", 
  (415, "Unsupported Input Type", 
   "Only application/xml is supported"))

The 3rd parameter to fn:error should be a sequence of the form ("status-code", "status-message", "response-payload"). That is, when using fn:error to raise RESTAPI-SRVEXERR, the $data parameter to fn:error is a sequence with the following members, all optional:

  • HTTP status code. Default: 400.
  • HTTP status message. Default: Bad Request.
  • Response payload. Plain text or data compatible with the error message format configured for the REST API instance.

    Best practice is to use RESTAPI-SRVEXERR. If you report any other error or raise any other exception, it is reported to the calling application as a 500 Server Internal Error.

For example, this resource extension function raises RESTAPI-SRVEXERR if the input content type is not as expected:

declare function example:put(
    $context as map:map,
    $params  as map:map,
    $input   as document-node()
) as document-node()
{
    (: get 'input-types' to use in content negotiation :)
    let $input-types := map:get($context,"input-types")
    let $negotiate :=
        if ($input-types = "application/xml")
        then () (: process, insert/update :)
        else fn:error((),"RESTAPI-SRVEXERR",
          ("415","Raven","nevermore"))
    return document { "Done"}  (: may return a document node :)
};

If a PUT request is made to the extension with an unexpected content type, the fn:error call causes the request to fail with a status 415 and to include the additional error description in the response body:

HTTP/1.1 415 Raven
Content-type: application/json
Server: MarkLogic
Set-Cookie: SessionID=714070bdf4076536; path=/
Content-Length: 62
Connection: close
{
  "message": "js-example: response with invalid 400 status",
  "statusCode": 415,
  "body": {
    "errorResponse": {
      "statusCode": 415,
      "status": "Raven",
      "messageCode": "RESTAPI-SRVEXERR",
      "message": "nevermore"
    }
  }
}

Setting the Response Status Code

By default, your resource service extension does not need to specify the HTTP response status code. The default behavior is as follows:

  • If your implementation function returns content, MarkLogic Server responds with a status 200.
  • If your function raises no errors but does not return content, MarkLogic Server responds with status 204.
  • If your function raises an error, the response code depends on the error; for details, see Reporting Errors.

To return a different status code other than the default, set the numeric code and message in $context. Use the key output-status with a (code, message) pair as its value.

Do not use output-status to report an error to the client. Instead, use fn:error. For details, see Reporting Errors.

The following example returns a 201 Created status to the client instead of 200 OK:

declare function example:put(
    $context as map:map,
    $params  as map:map,
    $input   as document-node()*
) as document-node()?
{
    ...
    map:put($context, "output-status", (201, "Created"));
};

Setting Additional Response Headers

The REST API sets headers such as Content-type and Content-Length for you when constructing the response from a resource extension request. You can specify additional headers by setting output-headers in the $context map.

Do not use output-headers to set Content-type. Use output-type to specify MIME type(s) instead.

The following example adds headers named X-My-Header1 and X-My-Header2 to the response by setting the output-headers to a sequence of the form (header-name, header-value, header-name, header-value, ...)

declare function example:put(
    $context as map:map,
    $params  as map:map,
    $input   as document-node()*
) as document-node()?
{
    ...
    map:put($context, "output-headers", 
      ("X-MyHeader1", "header1 value", "X-MyHeader2", "header2 value")) 2"));
};

You can also set output-headers using a map with the header names as keys. The following code snippet has the same effect on the response headers as the previous example.

let $headers := map:map()
...
(map:put($headers, "X-MyHeader1", "header1 value"),
 map:put($headers, "X-MyHeader2", "header2 value"),
 map:put($context, "output-headers", $headers))

In both cases, the resulting response headers are similar to the following:

HTTP/1.1 200 OK
X-MyHeader1: header1 value
X-MyHeader2: header2 value
Server: MarkLogic
Content-Type: text/plain; charset=UTF-8
Content-Length: 4
Connection: Keep-Alive
Keep-Alive: timeout=5

Context Map Keys

The $context parameter to resource service extension functions can contain the following keys. Not all keys are always present. For example, there is no input-type on a GET request.

Key Description
input-types The MIME type of the input document(s). If there are multiple input documents, the value is a sequence of strings, one for the MIME type of each input document.
accept-types For a GET, the MIME types accepted by the requestor. If there are multiple MIME types, the value is a sequence of strings.
output-headers Response headers to include extra headers in the response to the client. Optional. Specify headers as a sequence of alternating header and value pairs: (header1, value1, header2, value2, ...), or as a map. For details, see Setting Additional Response Headers.
output-status The HTTP response status code and message to return to the client. Optional. If unset, 200 OK is returned, unless an error is raised. For example, to return a 201 Created status, set output-status to (201, "Created"). For details, see Setting the Response Status Code.
output-type The output document MIME type. The service must set output-type for all output documents.

For a complete example of a resource service extension, see Example: XQuery Resource Service Extension.

Controlling Transaction Mode

This section discusses the default transaction mode under which the implementation of a resource service extension is evaluated, and how you can override the default behavior. This discussion assumes you are familiar with the MarkLogic Server transaction mode concept; for details, see Transaction Mode in Application Developer's Guide.

Choosing the appropriate transaction mode can improve the resource utilization of your implementation. For example, the function implementing your POST method runs in update transaction mode by default. This means it acquires exclusive locks on documents, as described in Update Transactions: Readers/Writers Locks in Application Developer's Guide. If you know that your POST implementation is actually a read-only operation, you can override the default transaction mode so that the function is evaluated in query transaction mode and does not acquire exclusive locks.

The XQuery functions in a resource service extension implementation are evaluated using the following default transaction modes:

  • get - query for a single statement transaction, update for a multi-statement transaction; that is, if your request includes a transaction id, the function runs in update mode.
  • put - update
  • post - query for a single statement transaction, update for a multi-statement transaction; that is, if your request includes a transaction id, the function runs in update mode.
  • delete - update

To override the default behavior, include a transaction mode annotation in the declaration of your function:

declare %rapi:transaction-mode("the-mode") function yourNS:method(...)

The value for %rapi:transaction-mode can be either update or query.

For example, to specify query transaction mode for your put function, use the following declaration:

declare %rapi:transaction-mode("query") function example:put(
    $context as map:map,
    $params  as map:map,
    $input   as document-node()*
) as document-node()?
{...};

Installing a Resource Service Extension

You install a resource service extension module using the REST Client API. Any dependent user-defined modules must be installed separately before installing your extension; for details, see Managing Dependent Libraries and Other Assets.

To install or update an extension, send a PUT request to /config/resources with a URL of the following form, with the extension code in the request body:

http://host:port/version/config/resources/extensionName

Where extensionName is the name used when administering and accessing the extension.

For an XQuery extension, extensionName must match the name in the extension module namespace declaration. For example:

xquery version "1.0-ml";
module namespace yourNSPrefix = 
  "http://marklogic.com/rest-api/resource/extensionName";
...

Set the Content-type of the request body to application/xquery or application/vnd.marklogic-javascript.

MarkLogic Server parses the extension implementation to determine which HTTP methods it supports. Only functions in the implementation that conform to the expected interface become available for use. For interface details, see The Resource Extension Interface.

If the extension service expects parameters, you can optionally declare the parameters using request parameters when installing the extension. This information is metadata that can be returned by a GET request to /config/resources. It is not used to check parameters on requests to the extension.

To include parameter metadata, declare each supported method and parameter using request parameters of the following form:

method=methodNamemethodName:paramName=paramTypeAndCardinality

Where methodName is one of the extension functions: get, put, post, or delete; paramName is the name of the parameter; and paramTypeAndCardinality reflects the type and cardinality the parameter would have in an XQuery function declaration.

For example, xs:string? means a string type parameter which may appear 0 or 1 time. The following URL illustrates an extension named example with an optional string parameter named reviewer to the put method and a required string parameter genre to the get method:

'http://host:port/version/config/resources/example?method=put&put:reviewer=string\?&method=get&get:genre=xs:string'

You can also specify extension metadata for provider, version, title, and description as request parameters during installation. For example:

http://host:port/version/config/resources/example?method=get&get:genre=string&provider=MarkLogic%20Corporation

For a complete example, see one of the following topics:

Using a Resource Service Extension

To access extension services, send a request of the appropriate type (GET, PUT, POST, or DELETE) to a URL of the following form:

http://host:port/version/resources/extensionName

Where extensionName is the name under which the extension is installed.

If the service expects parameters, supply the parameter names and values as request parameters, prefixing each parameter name with rs:. For example, if the example service for GET expects a parameter named genre, the request URL would be similar to the following:

http://localhost:8000/LATEST/resources/example?rs:genre=literature

The names, types, and cardinality of method parameters are resource specific. You can send a GET request to /config/resources to dynamically discover resources extension interfaces. For details, see Discovering Resource Service Extensions.

Example: JavaScript Resource Service Extension

This section demonstrates the implementation, installation and invocation of a resource service extension implemented in Server-Side JavaScript.

JavaScript Extension Implementation

This example extension implements all the HTTP methods supported by the resource extension interface. It demonstrates the use of input arguments, setting response content type, response status codes, additional response headers, and reporting errors to the calling application. The implementation follows the template described in Creating a JavaScript Resource Service Extension.

The methods implemented by this extension do the following:

Method Description
GET Accepts one or more caller-defined parameters. Returns a multipart response, with a part for each user-defined format. Each part contains a JSON document of the following form: { "name": param-name, "value": param-value }
PUT This method mimics some of the functionality of a multi-document write using POST:/v1/documents. For each input JSON document, adds a property to document and inserts it into the database. For each input XML document, inserts the document into the database unchanged. For any other document type, no action is taken. The document URIs are derived from a basename parameter supplied by the caller. The response contains a JSON document that reports the URIs of the inserted documents.
POST Logs a message.
DELETE Logs a message.

Helper functions are used to abstract away operations such as reporting errors to the client and normalizing the type of the input document set to Sequence. The later is demonstrated as a convenience since the PUT method might receive either a single document node or a Sequence, depending on whether the request body has one or multiple parts.

The following code implements the contract described above. See the comments in the code for details. For usage, see Using the Example Extension.

// GET
//
// This function returns a document node corresponding to each
// user-defined parameter in order to demonstrate the following
// aspects of implementing REST extensions:
// - Returning multiple documents
// - Overriding the default response code
// - Setting additional response headers
//
function get(context, params) {
  const results = [];
  context.outputTypes = [];
  for (const pname in params) {
    if (params.hasOwnProperty(pname)) {
      results.push({name: pname, value: params[pname]});
      context.outputTypes.push('application/json');
    }
  }

  // Return a successful response status other than the default
  // using an array of the form [statusCode, statusMessage].
  // Do NOT use this to return an error response.
  context.outputStatus = [201, 'Yay'];

  // Set additional response headers using an object
  context.outputHeaders = 
    {'X-My-Header1' : 42, 'X-My-Header2': 'h2val' };

  // Return a Sequence to return multiple documents
  return Sequence.from(results);
};

// PUT
//
// The client should pass in one or more documents, and for each
// document supplied, a value for the 'basename' request parameter.
// The function inserts the input documents into the database only 
// if the input type is JSON or XML. Input JSON documents have a
// property added to them prior to insertion.
//
// Take note of the following aspects of this function:
// - The 'input' param might be a document node or a Sequence
//   over document nodes. You can normalize the values so your
//   code can always assume a Sequence.
// - The value of a caller-supplied parameter (basename, in this case)
//   might be a single value or an array.
// - context.inputTypes is always an array
// - How to return an error report to the client
//
function put(context, params, input) {
  // normalize the inputs so we don't care whether we have 1 or many
  const docs = normalizeInput(input);
  const basenames = params.basename instanceof Array 
                  ? params.basename: [ params.basename ];

  // Validate inputs.
  if (docs.count > basenames.length) {
    returnErrToClient(400, 'Bad Request',
       'Insufficient number of uri basenames. Expected ' +
       docs.count + ' got ' + basenames.length + '.');
    // unreachable - control does not return from fn.error
  }

  // Do something with the input documents
  let i = 0;
  const uris = [];
  for (const doc of docs) {
    uris.push( doSomething(
        doc, context.inputTypes[i], basenames[i++]
    ));
  }

  // Set the response body MIME type and return the response data.
  context.outputTypes = ['application/json'];
  return { written: uris };
};

function post(context, params, input) {
  xdmp.log('POST invoked');
  return null;
};

function deleteFunction(context, params) {
  xdmp.log('POST invoked');
  return null;
};

// PUT helper func that demonstrates working with input documents.
//
// It inserts a (nonsense) property into the incoming document if
// it is a JSON document and simply inserts the document unchanged
// if it is an XML document. Other doc types are skipped.
//
// Input documents are imutable, so you must call toObject()
// to create a mutable copy if you want to make a change.
//
// The property added to the JSON input is set to the current time
// just so that you can easily observe it changing on each invocation.
//
function doSomething(doc, docType, basename)
{
  let uri = '/extensions/' + basename;
  if (docType == 'application/json') {
    // create a mutable version of the doc so we can modify it
    const mutableDoc = doc.toObject();
    uri += '.json';

    // add a JSON property to the input content
    mutableDoc.written = fn.currentTime();
    xdmp.documentInsert(uri, mutableDoc);
    return uri;
  } else if (docType == 'application/xml') {
    // pass thru an XML doc unchanged
    uri += '.xml';
    xdmp.documentInsert(uri, doc);
    return uri;
  } else {
    return '(skipped)';
  }
};

// Helper function that demonstrates how to normalize inputs
// that may or may not be multi-valued, such as the 'input'
// param to your methods.
//
// In cases where you might receive either a single value
// or a Sequence, depending on the request context,
// you can normalize the data type by creating a Sequence
// from the single value.
function normalizeInput(item)
{
  return (item instanceof Sequence)
         ? item                        // many
         : Sequence.from([item]);      // one
};

// Helper function that demonstrates how to return an error response
// to the client.

// You MUST use fn.error in exactly this way to return an error to the
// client. Raising exceptions or calling fn.error in another manner
// returns a 500 (Internal Server Error) response to the client.
function returnErrToClient(statusCode, statusMsg, body)
{
  fn.error(null, 'RESTAPI-SRVEXERR', 
           Sequence.from([statusCode, statusMsg, body]));
  // unreachable - control does not return from fn.error.
};


// Include an export for each method supported by your extension.
exports.GET = get;
exports.POST = post;
exports.PUT = put;
exports.DELETE = deleteFunction;

Installing the Example Extension

This section installs the example resource extension under the name js-example.

Follow these steps to install the example extension in the modules database associated with your REST API instance:

  1. If you do not already have a REST API instance, create one. This example uses the pre-configured instance on port 8000, and assumes the host is localhost.
  2. Copy the code from JavaScript Extension Implementation into a file. You can use any file name. This example assumes example.sjs.
  3. Install the example using a command similar to the following.
    # Windows users, see Modifying the Example Commands for Windows 
    $ curl --anyauth --user user:password -X PUT -i \
        -H "Content-type: application/vnd.marklogic-javascript" \
        --data-binary @./example.sjs \
        http://localhost:8000/LATEST/config/resources/js-example

For usage examples, see Using the Example Extension.

Optionally, you can include metadata during installation so that a GET request to /config/resources returns more detail. For example:

# Windows users, see Modifying the Example Commands for Windows 
$ curl --anyauth --user user:password -X PUT \
    -H "Content-type: application/vnd.marklogic-javascript" \
    -d@"./example.sjs" \
    "http://localhost:8000/LATEST/config/resources/js-example?version=1.0&provider=marklogic&description=A simple resource example&method=get&method=post&method=put&method=delete&get:arg1=[string]"

The table below breaks down the metadata request parameters so it is easier to see what metadata is included.

Request Parameter Description
version=1.0 The extension version number.
provider=marklogic The extension provider.
description=A simple resource example A brief description of the extension.
method=get The extensions supports GET requests.
method=post The extension supports POST requests.
method=put The extension supports PUT requests
method=delete The extension supports DELETE requests
get:arg1=[string] The GET method accepts an argument named arg1 which can occur 1 or more times.

Using the Example Extension

This section explores the behavior of the example extension and demonstrates how to exercise some of the methods.

The GET method of the example extension returns a JSON document for each parameter value passed in. Each document contains the name and value of a parameter. The GET method also demonstrates overriding the default response status and adding response headers.

function get(context, params) {
  ...
  for (const pname in params) {
    if (params.hasOwnProperty(pname)) {
      results.push({name: pname, value: params[pname]});
      context.outputTypes.push('application/json');
    }
  }
  context.outputStatus = [201, 'Yay'];
  context.outputHeaders =
    {'X-My-Header1' : 42, 'X-My-Header2': 'h2val' };
  return Sequence.from(results);
};

The following command exercises the GET method. The response contains 2 parts, one for each unique input parameter name. Notice that the value of the parameter a is an array, reflecting its inclusion twice in the request URL.

# Windows users, see Modifying the Example Commands for Windows 
$ curl --anyauth --user user:password -X GET \
  -H "Accept: multipart/mixed; boundary=BOUNDARY" \
  'http://localhost:8000/LATEST/resources/js-example?rs:a=1&rs:b=2&rs:a=3'HTTP/1.1 201 Yay
Content-type: multipart/mixed; boundary=BOUNDARY
X-My-Header1: 42
X-My-Header2: h2val
Server: MarkLogic
Content-Length: 207
Connection: Keep-Alive
Keep-Alive: timeout=5

--BOUNDARY
Content-Type: application/json
Content-Length: 25

{"name":"b", "value":"2"}
--BOUNDARY
Content-Type: application/json
Content-Length: 32

{"name":"a", "value":["1", "3"]}
--BOUNDARY--

The PUT method expects one or more input documents, and a basename request parameter value for each one. The basename is used to construct the URI for JSON and XML input; other input document types are ignored. A written property is added to each JSON input by the doSomething helper function, as shown below:

if (docType == 'application/json') {
  const mutableDoc = doc.toObject();
  mutableDoc.written = fn.currentTime();
  xdmp.documentInsert(uri, mutableDoc);
  ...
}

The following example invokes the PUT method, passing in a single JSON document as input. The response tells you the URI under which the document is inserted.

$ curl --anyauth --user user:password -X PUT -d '{"key":"value"}' \
  -H "Content-type: application/json" \
  'http://localhost:8000/LATEST/resources/js-example?rs:basename=one'

{"written": ["/extensions/one.json"]}

If you retrieve the inserted document, you can see that the PUT method added a JSON property named written to the input document:

$ curl --anyauth --user user:password -X GET \
  -H "Accept: application/json" \
  http://localhost:8000/LATEST/documents?uri=/extensions/one.json
{"key":"value", "written":"11:21:01-08:00"}

You can use a multi-part body to pass more than one document to the PUT method. JSON input is modified as shown above. XML documents are inserted unmodified. All other inputs are ignored. The following example command inserts two documents into the database if the multi-part body contains one JSON part and one XML part:

$ curl --anyauth --user user:password -X PUT -i \
  -H "Content-type: multipart/mixed; boundary=BOUNDARY" \
  -H "Accept: application/json" \
  --data-binary @./multipart-body   'http://localhost:8000/LATEST/resources/js-example?rs:basename=a&rs:basename=b'

HTTP/1.1 200 OK
Content-type: application/json; charset=UTF-8
Server: MarkLogic
Content-Length: 55
Connection: Keep-Alive
Keep-Alive: timeout=5

{"written":["/extensions/a.json", "/extensions/b.xml"]}

If you do not include a basename parameter value corresponding to each input document, the PUT implementation returns an error response. Extensions should always return errors to the client using RESTAPI-SRVEXERR and the pattern described in Reporting Errors. In the example extension, errors are reported using reportErrToClient as follows:

fn.error(null, 'RESTAPI-SRVEXERR',
         Sequence.from([statusCode, statusMsg, body]));
// unreachable - control does not return from fn.error.

For example, if we repeat the previous PUT request, but remove the second occurrence of basename, a 400 error is returned:

$ curl --anyauth --user user:password -X PUT -i \
  -H "Content-type: multipart/mixed; boundary=BOUNDARY" \
  -H "Accept: application/json" \
  --data-binary @./multipart-body   'http://localhost:8000/LATEST/resources/js-example?rs:basename=a'

HTTP/1.1 400 Bad Request
Content-type: application/json; charset=UTF-8
Server: MarkLogic
Content-Length: 162
Connection: Keep-Alive
Keep-Alive: timeout=5
{
  "errorResponse": {
    "statusCode": 400,
    "status": "Bad Request",
    "messageCode": "RESTAPI-SRVEXERR",
    "message": "Insufficient number of uri basenames. Expected 2 got 1."
  }
}

The POST and DELETE methods simply log a message when invoked. If you call them, check the MarkLogic Server error log for output.

Example: XQuery Resource Service Extension

This section demonstrates the implementation, installation and invocation of a resource service extension implemented in XQuery.

XQuery Extension Implementation

This example extension implements all the HTTP methods supported by the resource extension interface. It demonstrates the use of input arguments, as well as setting content type and response status codes. The implementation follows the template described in Creating an XQuery Resource Service Extension.

The methods implemented by this extension do the following:

Method Description
GET Accepts one or more string values through an arg1 parameter. Returns an XML document that contains the input argument values using the template:
<args>
  <arg1>value</arg1>
<args>
POST Logs a message.
PUT Accepts XML as input and returns Done.
DELETE Logs a message.

The following code implements the contract described above.

xquery version "1.0-ml";

(: Namespace pattern must be:  
 : "http://marklogic.com/rest-api/resource/{$rname}" 
 : and prefix must match resource name :)
module namespace example =
  "http://marklogic.com/rest-api/resource/example";

declare default function namespace
  "http://www.w3.org/2005/xpath-functions";
declare option xdmp:mapping "false";

(: Conventions: 
 : Module prefix must match resource name, 
 : and function signatures must conform to examples below.
 : The $context map carries state between the extension
 : framework and the extension.
 : The $params map contains parameters set by the caller,
 : for access by the extension.
 :)

(: Function responding to GET method - must use local name 'get':)
declare function example:get(
    $context as map:map,
    $params  as map:map
) as document-node()*
{
    (: set 'output-type', used to generate content-type header :)
    let $output-type :=
        map:put($context,"output-type","application/xml") 
    let $arg1 := map:get($params,"arg1")
    let $content := 
        <args>
            {for $arg in $arg1
             return <arg1>{$arg1}</arg1>
            }
        </args>
    return document { $content } 
    (: must return document node(s) :)
};

(: Function responding to PUT method - must use local name 'put'. :)
declare function example:put(
    $context as map:map,
    $params  as map:map,
    $input   as document-node()*
) as document-node()?
{
    (: get 'input-types' to use in content negotiation :)
    let $input-types := map:get($context,"input-types")
    let $negotiate := 
        if ($input-types = "application/xml")
        then () (: process, insert/update :) 
        else error((),"ACK",
          "Invalid type, accepts application/xml only") 
    return document { "Done"}  (: may return a document node :)
};

(: Function responding to POST method - must use local name 'post'. :)
declare function example:post(
    $context as map:map,
    $params  as map:map,
    $input   as document-node()*
) as document-node()*
{
    xdmp:log("post!")
};

(: Func responding to DELETE method - must use local name 'delete'. :)
declare function example:delete(
    $context as map:map,
    $params  as map:map
) as document-node()?
{
    xdmp:log("delete!")
};

Installing the Example Extension

Follow these steps to install the example extension in the modules database associated with your REST API instance:

  1. If you do not already have a REST API instance, create one. This example assumes an instance is running on localhost:8000.
  2. Copy the code from XQuery Extension Implementation into a file. You can use any file name. This example assumes example.xqy.
  3. Install the example using a command similar to the following. The extension metadata such as title, version, provider, description, and method interface details are all optional.
    # Windows users, see Modifying the Example Commands for Windows 
    $ curl --anyauth --user user:password -X PUT \
        -H "Content-type: application/xquery" -d@"./example.xqy" \
        "http://localhost:8000/LATEST/config/resources/example"

Optionally, you can include metadata during installation so that a GET request to /config/resources returns more detail. For example:

# Windows users, see Modifying the Example Commands for Windows 
$ curl --anyauth --user user:password -X PUT \
    -H "Content-type: application/xquery" -d@"./example.xqy" \
    "http://localhost:8000/LATEST/config/resources/example?version=1.0&provider=marklogic&description=A simple resource example&method=get&method=post&method=put&method=delete&get:arg1=xs:string*"

The table below breaks down the request parameters so it is easier to see what metadata is included.

Request Parameter Description
version=1.0 The extension version number.
provider=marklogic The extension provider.
description=A simple resource example A brief description of the extension.
method=get The extensions supports GET requests.
method=post The extension supports POST requests.
method=put The extension supports PUT requests
method=delete The extension supports DELETE requests
get:arg1=xs:string* The GET method accepts an argument named arg1 which can occur 1 or more times.

Using the Example Extension

The following example command invokes the GET method of the example extension, passing the string super fantastic as arguments to the GET implementation:

# Windows users, see Modifying the Example Commands for Windows 
$ curl --anyauth --user user:password -X GET \
  -H "Accept: application/xml" \
  "http://localhost:8000/LATEST/resources/example?rs:arg1=super fantastic"
<?xml version="1.0" encoding="UTF-8"?>
<args>
  <arg1>super fantastic</arg1>
</args>

The following example invokes the PUT method:

$ curl --anyauth --user user:password -X PUT -d "<input/>" \
    -H "Content-type: application/xml" \
    http://localhost:8000/LATEST/resources/example

Done

The POST and DELETE methods simply log a message when invoked. If you call them, check the MarkLogic Server error log for output.

Controlling Access to a Resource Service Extension

Only users with the rest-extension-user role (or equivalent privileges) can execute modules in the modules database, including installed REST resource service extensions.

The pre-defined rest-reader, rest-writer, and rest-admin roles inherit rest-extension-user. You do not need additional configuration to grant access to your extension to users with these roles.

However, if instead of using the pre-defined roles, you create a custom role with the equivalent privileges, then you must explicitly inherit the rest-extension-user role in your custom role. Users with the custom role cannot execute resource service extensions unless you do so.

For details, see Controlling Access to Documents and Other Artifacts.

Discovering Resource Service Extensions

To discover the name, interface, and other metadata about installed resource extensions, send a GET request to the /config/resources service of the form:

http://host:port/version/config/resources

MarkLogic Server returns a summary of the installed extensions in XML or JSON. The default summary content type is XML. Use the Accept header or format request parameter to select the output content type. For details, see Controlling Input and Output Content Type.

The amount of information available about a given extension depends on the amount of metadata provided during installation of the extension. The name and methods are always available. Details such as provider, version, and method parameter information are optional.

By default, this request rebuilds the extension metadata each time it is called to ensure the metadata is up to date. If you find this refresh makes discovery take too long, you can disable the refresh by setting the refresh request parameter to false. Disabling the refresh can result in this request returning inaccurate information, but it does not affect the freshness or availability of the implementation of any extensions.

The following example requests an XML report. One extension is installed, called example. The extension implements only a get method, which accepts a string parameter named genre. The extension provider is Acme Widgets.

# Windows users, see Modifying the Example Commands for Windows 
$ curl --anyauth --user user:password -H "Accept: application/xml" \
    -X GET http://localhost:8000/LATEST/config/resources
...
<rapi:resources xmlns:rapi="http://marklogic.com/rest-api">
  <rapi:resource>
    <rapi:name>example</rapi:name>
    <rapi:title/>
    <rapi:version/>
    <rapi:provider-name>Acme Widgets</rapi:provider-name>
    <rapi:description/>
    <rapi:methods>
      <rapi:method>
        <rapi:method-name>get</rapi:method-name>
        <rapi:parameter>
          <rapi:parameter-name>genre</rapi:parameter-name>
          <rapi:parameter-type>string</rapi:parameter-type>
        </rapi:parameter>
      </rapi:method>
    </rapi:methods>
    <rapi:resource-source>/v1/resources/example</rapi:resource-source>
  </rapi:resource>
</rapi:resources>

Retrieving the Implementation of a Resource Service Extension

To retrieve the XQuery code that implements an extension, send a GET request to the /config/resources/{name} service of the form:

http://host:port/version/config/resources/extensionName

Where extensionName is the name under which the extension is installed.

Set the request Accept header to application/xquery.

MarkLogic Server returns the XQuery library module that implements the extension in the response body. For example:

# Windows users, see Modifying the Example Commands for Windows 
$ curl --anyauth --user user:password -H "Accept: application/xquery" \
    -X GET http://localhost:8000/LATEST/config/resources/example
...
xquery version "1.0-ml";
module namespace example="http://marklogic.com/rest-api/resource/example";
declare function example:get( ...

Managing Dependent Libraries and Other Assets

Use the /ext service to install, update, retrieve, and delete assets required by your REST application, including additional XQuery library modules required by resource extensions and transforms, and partial update replace library modules.

You can only use the /ext service with REST API instances created with MarkLogic Server 7 or later. If you REST API instance was created with an earlier version, refer to the documentation for that version.

The following topics are covered:

Maintenance of Dependent Libraries and Other Assets

When you install or update a dependent library module or other asset as described in this section, the asset is replicated across your cluster automatically. There can be a delay of up to one minute between updating and availability.

MarkLogic Server does not automatically remove dependent assets when you delete the related extension or transform.

Since dependent assets are installed in the modules database, they are removed when you remove the REST API instance if you include the modules database in the instance teardown. For details, see Removing an Instance.

If you installed assets in a REST API instance using MarkLogic 6, they cannot be managed using the /ext service unless you re-install them using /ext. Reinstalling the assets may require additional changes because the asset URIs will change. If you choose not to migrate such assets, continue to maintain them according to the documentation for MarkLogic 6.

Installing or Updating an Asset

Use the /ext service to install any assets required by resource service extensions or transformations, and to install partial update replace library modules. Using this service installs an asset into the modules database associated with the REST API instance. If the REST API instance is part of a cluster, the asset is automatically propagated throughout the cluster.

To install or update an asset, send a PUT request to /ext/{directories}/{asset} with a URL of the following form:

http://host:port/version/ext/directories/asset?perm:role=capability

Where directories is one or more database directories and asset is the asset document name. Use the perm request parameter to specify additional permissions on the asset, beyond the permissions granted to rest-admin privileges that are always present. The asset is installed into the modules database with the URI /ext/directories/asset.

The following example installs the XQuery library module my-lib.xqy into the modules database with the URI /ext/my/domain/my-lib.xqy. Execute privileges are granted to the role my-users.

# Windows users, see Modifying the Example Commands for Windows 
$ curl --anyauth --user user:password -X PUT -i -d @./my-lib.xqy \
  -H "Content-type: application/xquery" \
  "http://localhost:8000/LATEST/ext/my/domain/my-lib.xqy?perm:my-users=execute"

If you're installing an asset whose content is sensitive to line breaks, such as a JavaScript module that contains '//' style comments, use --data-binary rather than -d to pass the content to curl. For details, see Introduction to the curl Tool.

You can insert assets into the modules database as JSON, XML, text, or binary documents. MarkLogic Server determines the document format by examining the format request parameter, the MIME type in the Content-type HTTP header, or the document URI extension, in that order. For example, if the format parameter is present, it determines the document type. If there is no format parameter and no Content-type header, the URI extension (.xml, .jpg, etc.) determines the document type, based on the MIME type mappings defined for your installation; see the Mimetypes section of the Admin Interface.

Referencing an Asset

To use a dependent library installed with /ext in your extension or transform module, use the same URI under which you installed the dependent library. For example, if a dependent library is installed with the URI /ext/my/domain/my-lib.xqy, then the extension module using this library should include an import of the following form:

import module namespace dep="mylib" at "/ext/my/domain/lib/my-lib.xqy";

Removing an Asset

You can remove either a single asset or all the assets in a directory in the modules database associated with your REST API instance.

To remove a single asset, send a DELETE request with a URL of the following form:

http://host:port/version/ext/directories/asset

Where directories is one or more database directories and asset is the asset document name. For example, the following command removes an XQuery library module with the URI /ext/my/domain/my-lib.xqy:

# Windows users, see Modifying the Example Commands for Windows 
$ curl --anyauth --user user:password -X DELETE -i\
  "http://localhost:8000/LATEST/ext/my/domain/my-lib.xqy

To remove all assets in a modules database directory, send a DELETE request with a URL of the following form:

http://host:port/version/ext/directories/

The URL must end with a terminating path separator (/) so that MarkLogic Server recognizes it as a directory.

Where directories is one or more database directories. For example, the following command removes all assets in the directory /ext/my/domain/:

$ curl --anyauth --user user:password -X DELETE -i\
  "http://localhost:8000/LATEST/ext/my/domain/

Retrieving an Asset List

You can retrieve a list of all assets in the modules database by sending a GET request with a URL of the following form:

http://host:port/version/ext/

You can retrieve a list of assets in a particular directory by sending a GET request with a URL of the following form, where directories is one or more database directory path steps:

http://host:port/version/ext/directories

You can request results as either XML or JSON, using either the HTTP Accept header or the format request parameter.

The following example retrieves a list of all assets installed under the database directory /ext/my/domain/, as XML:

# Windows users, see Modifying the Example Commands for Windows 
$ curl --anyauth --user user:password -X GET -i\
  -H "Accept: application/xml" \
  http://localhost:8000/LATEST/ext/my/domain/
...
HTTP/1.1 200 OK
Server: MarkLogic
Content-Type: text/xml; charset=UTF-8
Content-Length: 125
Connection: Keep-Alive
Keep-Alive: timeout=5

<rapi:assets xmlns:rapi="http://marklogic.com/rest-api">
  <rapi:asset>/ext/patch/replace/my-lib.xqy</rapi:asset>
</rapi:assets>

The following example makes the same request, but returns JSON results.

# Windows users, see Modifying the Example Commands for Windows 
$ curl --anyauth --user user:password -X GET -i\
  -H "Accept: application/json" \
  http://localhost:8000/LATEST/ext/my/domain/
...
HTTP/1.1 200 OK
Server: MarkLogic
Content-Type: text/plain; charset=UTF-8
Content-Length: 54
Connection: Keep-Alive
Keep-Alive: timeout=5

{"assets":[
  {"asset":"/ext/my/domain/my-lib.xqy"}
]}

Retrieving an Asset

To retrieve the contents of an asset document, send a GET request with a URL of the following form:

http://host:port/version/ext/directories/asset

Where directories is one or more database directories and asset is the asset document name. For example, the following command retrieves an XQuery library module with the URI /ext/my/domain/my-lib.xqy:

# Windows users, see Modifying the Example Commands for Windows 
$ curl --anyauth --user user:password -X GET -i \
  -Accept: "application/xquery" \
  "http://localhost:8000/LATEST/ext/my/domain/my-lib.xqy
...
HTTP/1.1 200 Document Retrieved
Content-type: application/xquery
Server: MarkLogic
Content-Length: 633
Connection: Keep-Alive
Keep-Alive: timeout=5
...asset contents...

The response data content type is determined by the MIME type in the Accept header. No content conversions are applied. For example, if you retrieve a binary document containing PDF and set the Accept header to application/xml, the response Content-type will be application/xml, even though the data in the body is binary.

Evaluating an Ad-Hoc Query

The /eval service enables you to send blocks of XQuery and server-side JavaScript code to MarkLogic Server for evaluation. This is equivalent to calling the builtin server function xdmp:eval (XQuery) or xdmp.eval (JavaScript). The following topics are covered:

Required Privileges

Using the /eval service requires special privileges not required for other REST Client API services. You must have the following privileges or the equivalent:

  • http://marklogic.com/xdmp/privileges/xdmp-eval
  • http://marklogic.com/xdmp/privileges/xdmp-eval-in
  • http://marklogic.com/xdmp/privileges/xdbc-eval
  • http://marklogic.com/xdmp/privileges/xdbc-eval-in

Evaluating an Ad-Hoc JavaScript Query

To send JavaScript code to MarkLogic Server for evaluation, use POST /v1/eval. Send a POST request to the /eval service with a URL of the following form:

http://host:port/version/eval

The POST body MIME type must be x-www-form-urlencoded, with the ad-hoc code as the value of the javascript form parameter and any external variables in the vars form parameter. For details on passing in variable values, see Specifying External Variable Values.

Your query must be expressed using the MarkLogic server-side JavaScript dialect. For details, see the JavaScript Reference Guide.

For example, suppose you want to evaluate the following JavaScript code, where word1 and word2 are external variable values supplied by the request:

word1 + " " + word2

The following request causes the code to be evaluated by MarkLogic Server. Use the curl --data-urlencode option to force URL-encoding of the form parameter values in the POST body.

# Windows users, see Modifying the Example Commands for Windows 
$ curl --anyauth --user user:password -X POST -i \
    -H "Content-type: application/x-www-form-urlencoded" \
    -H "Accept: multipart/mixed" \
    --data-urlencode javascript='word1 + " " + word2' \
    --data-urlencode vars='{"word1":"hello","word2":"world"}' \
    http://localhost:8000/LATEST/eval

The response is always multipart/mixed, with one part for each value returned by the evaluated code. The part headers can contain additional type information about the returned data in the X-Primitive header. For details, see Interpreting the Results of an Ad-Hoc Query.

For example, the above request returns results similar to the following:

HTTP/1.1 200 OK
Server: MarkLogic 8.0-20141122
Set-Cookie: TxnMode=auto; path=/
Set-Cookie: TxnID=null; path=/
Content-Type: multipart/mixed; boundary=198d5dc521a0e8cf
Content-Length: 113
Connection: Keep-Alive
Keep-Alive: timeout=5


--198d5dc521a0e8cf
Content-Type: text/plain
X-Primitive: untypedAtomic

hello world
--198d5dc521a0e8cf--

You can return documents, objects, and arrays as well as atomic values. To return multiple items, you must return either a JavaScript Sequence or an XQuery sequence. You can construct a Sequence from an array-like or a generator, and many builtin functions that can return multiple values return a Sequence. To construct a sequence, apply xdmp.arrayValues or Sequence.from to a JavaScript array.

For example, to extend the previous example to return the two input values as well as the concatenated string, accumulate the results in an array and then apply Sequence.from to the array:

Sequence.from([word1, word2, word1 + " " + word2])

The following command executes the above code. The response contains 3 parts: one for the string value hello, one for the string value world, and one for the concatenation result, hello world.

$ curl --anyauth --user user:password -X POST -i \
    -H "Content-type: application/x-www-form-urlencoded" \
    -H "Accept: multipart/mixed" \
    --data-urlencode javascript='Sequence.from([word1, word2, word1 + " " + word2])' \
    --data-urlencode vars='{"word1":"hello","word2":"world"}' \
    http://localhost:8000/LATEST/eval
HTTP/1.1 200 OK
Server: MarkLogic 8.0-20141122
Set-Cookie: TxnMode=query; path=/
Set-Cookie: TxnID=null; path=/
Content-Type: multipart/mixed; boundary=3d0b32f66d6d0f33
Content-Length: 279
Connection: Keep-Alive
Keep-Alive: timeout=5


--3d0b32f66d6d0f33
Content-Type: text/plain
X-Primitive: untypedAtomic

hello
--3d0b32f66d6d0f33
Content-Type: text/plain
X-Primitive: untypedAtomic

world
--3d0b32f66d6d0f33
Content-Type: text/plain
X-Primitive: untypedAtomic

hello world
--3d0b32f66d6d0f33--

By default, the ad-hoc code is evaluated in the context of the database associated with the REST API instance that receives the request. To evaluate the code against another database, specify the database name or id through the database URL parameter. For example, the following request uses the example database:

$ curl --anyauth --user user:password -X POST -i \
    -H "Content-type: application/x-www-form-urlencoded" \
    -H "Accept: multipart/mixed" \
    --data-urlencode javascript='word1 + " " + word2' \
    --data-urlencode vars='{"word1":"hello","word2":"world"}' \
    http://localhost:8000/LATEST/eval?database=example

Evaluating an Ad-Hoc XQuery Query

To send XQuery code to MarkLogic Server for evaluation, use POST /v1/eval. Send a POST request to the /eval service with a URL of the following form:

http://host:port/version/eval

The POST body MIME type must be x-www-form-urlencoded, with the ad-hoc code as the value of the xquery form parameter and any external variables in the vars form parameter. For details on passing in variable values, see Specifying External Variable Values.

For example, suppose you want to evaluate the following XQuery code, where word1 and word2 are external variable values supplied by the request:

xquery version "1.0-ml";
declare variable $word1 as xs:string external;
declare variable $word2 as xs:string external;
fn:concat($word1, " ", $word2)

The following request body supplies the query code and the parameter values. (The values should be urlencoded. They're shown unencoded here for readability.)

xquery=
xquery version "1.0-ml";
declare variable $word1 as xs:string external;
declare variable $word2 as xs:string external;
fn:concat($word1, " ", $word2)

&vars={"word1":"hello","word2":"world"}

The following request causes the code to be evaluated by MarkLogic Server, assuming file concat.xqy contains the XQuery shown above. Use the curl --data-urlencode option to force URL-encoding of the form parameter values in the POST body.

# Windows users, see Modifying the Example Commands for Windows 
$ curl --anyauth --user user:password -X POST -i \
    -H "Content-type: application/x-www-form-urlencoded" \
    -H "Accept: multipart/mixed" \
    --data-urlencode xquery@./concat.xqy \
    --data-urlencode vars='{"word1":"hello","word2":"world"}' \
    http://localhost:8000/LATEST/eval

The response is always multipart/mixed, with one part for each value returned by the evaluated code. The part headers can contain additional type information about the returned data in the X-Primitive header. For details, see Interpreting the Results of an Ad-Hoc Query.

For example, the above request returns results similar to the following:

HTTP/1.1 200 OK
Server: MarkLogic 8.0-20141122
Set-Cookie: TxnMode=auto; path=/
Set-Cookie: TxnID=null; path=/
Content-Type: multipart/mixed; boundary=7e235c5751341cff
Content-Length: 106
Connection: Keep-Alive
Keep-Alive: timeout=5


--7e235c5751341cff
Content-Type: text/plain
X-Primitive: string

hello world
--7e235c5751341cff--

You can return documents and other complex values, as well as atomic values. To return multiple items, return a sequence. The multipart response contains a part for each sequence item.

The following query extends the previous example to return the two input values as well as the concatenated string:

xquery version "1.0-ml";
declare variable $word1 as xs:string external;
declare variable $word2 as xs:string external;
($word1, $word2, fn:concat($word1, " ", $word2))

The following command evaluates the query, assuming the query is in a file named concat2.xqy. The response contains 3 parts, one for the value of word1, one for the value of word2, and one for the concatenation result, hello world.

$ curl --anyauth --user user:password -X POST -i \
    -H "Content-type: application/x-www-form-urlencoded" \
    -H "Accept: multipart/mixed" \
    --data-urlencode xquery@./concat2.xqy \
    --data-urlencode vars='{"word1":"hello","word2":"world"}' \
    http://localhost:8000/LATEST/eval

HTTP/1.1 200 OK
Server: MarkLogic 8.0-20141122
Set-Cookie: TxnMode=query; path=/
Set-Cookie: TxnID=null; path=/
Content-Type: multipart/mixed; boundary=3d0b32f66d6d0f33
Content-Length: 279
Connection: Keep-Alive
Keep-Alive: timeout=5


--3d0b32f66d6d0f33
Content-Type: text/plain
X-Primitive: string

hello
--3d0b32f66d6d0f33
Content-Type: text/plain
X-Primitive: string

world
--3d0b32f66d6d0f33
Content-Type: text/plain
X-Primitive: string

hello world
--3d0b32f66d6d0f33--

By default, the ad-hoc query is evaluated in the context of the database associated with the REST API instance that receives the request. To evaluate the query against another database, specify the database name or id through the database URL parameter. For example, the following request evaluates the query against the example database:

$ curl --anyauth --user user:password -X POST -i \
    -H "Content-type: application/x-www-form-urlencoded" \
    -H "Accept: multipart/mixed" \
    --data-urlencode xquery@./concat.xqy \
    --data-urlencode vars='{"word1":"hello","word2":"world"}' \
    http://localhost:8000/LATEST/eval?database=example

Specifying External Variable Values

You can pass values to an ad-hoc query (or invoked module) at runtime using external variables. Specify the variable values in the POST body using the vars form parameter. The vars parameter has the following form:

vars={name:value, name:value, ...}

For example, the following vars parameter supplies values for 2 external variables, named word1, and word2:

vars={word1:"hello",word2:"world"}

If you're evaluating or invoking XQuery code, you must declare the variables explicitly in the ad-hoc query or module. For example, the following prolog declares two external variables whose values can be supplied through the above vars parameter.

xquery version "1.0-ml";
declare variable $word1 as xs:string external;
declare variable $word2 as xs:string external;
...

If you're evaluating or invoking XQuery code that depends on variables in a namespace, use Clark notation on the variable name. That is, specify the name using notation of the form {namespaceURI}name. For example, given the following query saved to the file nsquery.xqy:

xquery version "1.0-ml";
declare namespace my = "http://example.com";
declare variable $my:who as xs:string external;
fn:concat("hello", " ", $my:who)

You can specify the value of $my:who as shown in the command below. Note that you must quote the namespace qualified variable name.

$ curl --anyauth --user user:password -i -X POST \
    -H "Content-type: application/x-www-form-urlencoded" \
    --data-urlencode xquery@./nsquery.xqy 
    --data-urlencode vars='{"{http://example.com}who":"world"}' \
    http://localhost:8000/LATEST/eval
HTTP/1.1 200 OK
Server: MarkLogic 8.0-20141122
Set-Cookie: TxnMode=auto; path=/
Set-Cookie: TxnID=null; path=/
Content-Type: multipart/mixed; boundary=ea9250df22f6543f
Content-Length: 106
Connection: Keep-Alive
Keep-Alive: timeout=5


--ea9250df22f6543f
Content-Type: text/plain
X-Primitive: string

hello world
--ea9250df22f6543f--

Interpreting the Results of an Ad-Hoc Query

When you make a POST request to the /eval or /invoke service, the result is always a multipart/mixed response, with a part for each value returned by the query. Depending on the type of value, the part header can contain information that helps your application interpret the value.

For example, an atomic value (anyAtomicType, a type derived from anyAtomicType, or an equivalent JavaScript type) has an X-Primitive part header that can specify an explicit type such as integer, string, or date. The reported type may be more general than the actual type. Types derived from anyAtomicType include anyURI, boolean, dateTime, double, and string. For details, see http://www.w3.org/TR/xpath-functions/#datatypes.

Returning a JavaScript null value or an XQuery empty sequences results in an empty response.

The table below lists the Content-type and X-Primitive part header values for a variety of types. The response for other output is undefined, including the output from constructors such as attribute(), comment(), processing-instruction(), boolean-node(), cts:query(), or a JavaScript object.

Value Type Content-type Header Value X-Primitive Header Value
document-node[object-node()]
application/json
undefined
object-node()
application/json
undefined
document-node[array-node()]
application/json
undefined
array-node()
application/json
undefined
map:map
application/json
undefined
json:array
application/json
undefined
document-node[element()]
text/xml
undefined
element()
text/xml
undefined
document-node[binary()]
application/x-unknown-content-type
undefined
binary()
application/x-unknown-content-type
undefined
document-node[text()]
text/plain
text
text()
text/plain
text
any atomic value
text/plain
anyAtomicType or a derived type
JavaScript string
text/plain
string
JavaScript number
text/plain
decimal, a derived type such as integer, or string (for infinity)
JavaScript boolean
text/plain
Boolean

Managing Session State

When you use POST /v1/invoke or POST /v1/eval MarkLogic returns a cookie. Normally, your client application code should ignore the cookie. However, if your server-side code sets a session field, then your client application must send the same cookie to the same host for any subsequent eval or invoke requests that access the session field.

You should also include any load balancer cookies if you are using a load balancer.

If you make an eval or invoke request that requires access to an existing session field in the context of a multi-statement transaction, include both the transaction cookie and the session cookie in the request.

Evaluating a Module Installed on MarkLogic Server

A JavaScript MJS module can be invoked through the /v1/invoke endpoint. This is the preferred method.

POST /v1/invoke enables you to evaluate an XQuery or server-side JavaScript module installed on MarkLogic Server. This is equivalent to calling the builtin server function xdmp:invoke (XQuery) or xdmp.invoke (JavaScript).

Using the /invoke service requires special privileges not required for other REST Client API services. You must have the following privileges or their equivalent:

  • http://marklogic.com/xdmp/privileges/xdmp-invoke
  • http://marklogic.com/xdmp/privileges/xdmp-invoke-in
  • http://marklogic.com/xdmp/privileges/xdbc-invoke
  • http://marklogic.com/xdmp/privileges/xdbc-invoke-in

To evaluate an XQuery or JavaScript module installed on MarkLogic Server, make a POST request to the /invoke service with a URL of the following form:

http://host:port/version/invoke

The POST body MIME type must be x-www-form-urlencoded, with the module path as the value of the module form parameter and any external variables in the vars form parameter. For details on passing in variable values, see Specifying External Variable Values.

For example, assuming you previously used a PUT /LATEST/ext/invoke/example.sjs request to install a JavaScript module on MarkLogic Server, then the following command invokes the module:

$ curl --anyauth --user user:password -i -X POST \
    -H "Content-type: application/x-www-form-urlencoded" \
    --data-urlencode module=/ext/invoke/example.sjs \
    http://localhost:8000/LATEST/invoke

The module parameter must be the path to a module installed in the modules database associated with the REST API instance. You can use the /ext service to install your module, as described in Managing Dependent Libraries and Other Assets. The module path is resolved using the rules described in Rules for Resolving Import, Invoke, and Spawn Paths in the Application Developer's Guide. If you install your module using PUT /v1/ext/{name}, the module path always begins with /ext/.

If the invoked module expects input variable values, specify them using the vars form parameter, as described in Specifying External Variable Values.

The response is always multipart/mixed, with one part for each value returned by the invoked module. The part headers can contain additional type information about the returned data in the X-Primitive header. For details, see Interpreting the Results of an Ad-Hoc Query.

If your server-side code sets a session field that subsequent eval or invoke requests depend upon, your subsequent calls must pass along the session cookie returned by the first such request. For details, see Managing Session State.

The following example first installs a module in the modules database associated with the REST API instance using the /ext service, then invokes module through the /invoke service.

  1. Save the following code to file. This is the code to be invoked. The rest of this example demonstrates using the JavaScript module, saved to a file named module.sjs.
    1. For JavaScript, copy the following code into a file named module.sjs.
      Sequence.from([word1, word2, word1 + " " + word2])
    2. For XQuery, copy the following code into a file named module.xqy.
      xquery version "1.0-ml";
      declare variable $word1 as xs:string external;
      declare variable $word2 as xs:string external;
      
      ($word1, $word2, fn:concat($word1, " ", $word2))
  2. Install the module in the modules database of the REST API instance. Modify the username and password in the example commands to fit your environment.
    1. To install the JavaScript module, use the following command:
      curl --anyauth --user user:password -X PUT -i \
          --data-binary @./module.sjs \
          -i -H "Content-type: application/vnd.marklogic-javascript" 
          http://localhost:8000/LATEST/ext/invoke/example.sjs
    2. To install the XQuery module, use the following command:
      curl --anyauth --user user:password -X PUT -i -d @./module.xqy \
          -H "Content-type: application/xquery" 
          http://localhost:8000/LATEST/ext/invoke/example.xqy
  3. Invoke the module. Modify the username and password in the example commands to fit your environment.
    1. To invoke the JavaScript module, use the following command:
      curl --anyauth --user user:password -i -X POST \
          -H "Content-type: application/x-www-form-urlencoded" \
          --data-urlencode module=/ext/invoke/example.sjs \
          --data-urlencode vars='{"word1":"hello","word2":"world"}' \
          http://localhost:8000/LATEST/invoke
    2. To invoke the XQuery module, use the following command:
      curl --anyauth --user user:password -i -X POST \
          -H "Content-type: application/x-www-form-urlencoded" \
          --data-urlencode module=/ext/invoke/example.xqy \
          --data-urlencode vars='{"word1":"hello","word2":"world"}' \
          http://localhost:8000/LATEST/invoke

The result of running this example is similar to the following. There are 3 parts in the response body, corresponding to the 3 values returned by the invoked module.

HTTP/1.1 200 OK
Server: MarkLogic 8.0-20141122
Set-Cookie: TxnMode=query; path=/
Set-Cookie: TxnID=null; path=/
Content-Type: multipart/mixed; boundary=09ff9ca463ea6272
Content-Length: 279
Connection: Keep-Alive
Keep-Alive: timeout=5


--09ff9ca463ea6272
Content-Type: text/plain
X-Primitive: string

hello
--09ff9ca463ea6272
Content-Type: text/plain
X-Primitive: string

world
--09ff9ca463ea6272
Content-Type: text/plain
X-Primitive: string

hello world
--09ff9ca463ea6272--

« Previous chapter
Next chapter »