Lifecycle Hooks

Cordra validates incoming information against schemas as defined in the Type objects. Additional rules that validate and/or enrich the information in the object can be configured to be applied by Cordra at various stages of the object lifecycle. Such rules are expected to be specified in JavaScript and to be bound to the following lifecycle points:

  • before an object is schema validated (with an opportunity, during create, to set the id or properties which help determine the id);

  • during id generation;

  • before schema validation but after id generation;

  • before an object is stored, allowing for additional validation or side-effects after all other validation is done;

  • before an object is indexed, allowing the object that is indexed to differ from the one that is stored;

  • during handle record creation;

  • after an object (or payload) has been retrieved from storage, but before it is returned;

  • before an object is deleted, to forbid deletion under some circumstances;

  • after an object is deleted;

  • after an object is created or updated; and

  • before executing a user-supplied query.

Lifecycle hooks, in our parlance, are the points of entry for your own JavaScript-based rules that Cordra processes. In addition to the lifecycle hooks that are discussed in detail below, Cordra enables clients to invoke other rules in an ad hoc fashion using Type Methods.

Currently, various lifecycle hooks are enabled in Cordra for different actions: create, update, retrieve, and delete.

The following diagrams illustrate hooks that are enabled during various stages of the object lifecycle.

Create Lifecycle
Update Lifecycle
Retrieve Lifecycle
Delete Lifecycle

An update which changes the type of an object will run the beforeDelete and afterDelete hooks of the original type, only with a context where “isUpdate” is set to true, as well as the beforeSchemaValidation, beforeStorage, and afterCreateOrUpdate hooks of the new type, with “isNew” set to true (like a creation) but also with “isUpdate” set to true.

Using Hooks in JavaScript

Hooks can either be implemented in the “javascript” property of the design object, which makes a “service-level hook”, or hooks can be implemented on a per type basis as part of the JavaScript associated with each Type object. When a lifecycle hook is implemented as a service-level hook that hook will apply to all objects in the system. A hook implemented in JavaScript associated with a Type object will only apply to objects of that type. Hooks implemented for a specific type override the same hook implemented as a service-level hook. For example if you have onObjectResolution in design JavaScript and you have onObjectResolution implemented in JavaScript for type “Foo”, the service-level hook will be used for all objects except those of type “Foo” which will use the type specific implementation.

Warning

In versions of Cordra before 2.5.0, implementing service-level lifecycle hooks in design JavaScript did not mean that hook would be applied to all object types. In 2.5.0 hooks implemented in design JavaScript apply to all objects and can be overridden at the type level.

Hooks in a Type Object

Most lifecycle hooks are available for use as part of the JavaScript associated with each Type object, in the “javascript” property of the schema object content. To have have the same code that runs for multiple type you can either implement the hook as a service-level hook or you will need to have JavaScript for each of the types. See Using External Modules for methods to share code among multiple types.

The examples in this documentation are written as CommonJS modules, but Cordra also supports ECMAScript modules; see ECMAScript Modules.

Here is the shell of the hooks that are available in each Type, which will be explained below.

 1 const cordra = require('cordra');
 2 const cordraUtil = require('cordra-util'); // older name 'cordraUtil' also works
 3 const { CordraClient, Blob } = require('cordra-client');
 4 const schema = require('/cordra/schemas/Type.schema.json');
 5 const js = require('/cordra/schemas/Type');
 6
 7 exports.beforeSchemaValidation = beforeSchemaValidation;
 8 exports.beforeSchemaValidationWithId = beforeSchemaValidationWithId;
 9 exports.beforeStorage = beforeStorage;
10 exports.objectForIndexing = objectForIndexing;
11 exports.onObjectResolution = onObjectResolution;
12 exports.onPayloadResolution = onPayloadResolution;
13 exports.beforeDelete = beforeDelete;
14 exports.afterDelete = afterDelete;
15 exports.afterCreateOrUpdate = afterCreateOrUpdate;
16 exports.getAuthConfig = getAuthConfig;
17
18 function beforeSchemaValidation(object, context) {
19     /* Insert code here */
20     return object;
21 }
22
23 function beforeSchemaValidationWithId(object, context) {
24     /* Insert code here */
25     return object;
26 }
27
28 function beforeStorage(object, context) {
29     /* Insert code here */
30 }
31
32 function objectForIndexing(object, context) {
33     /* Insert code here */
34     return object;
35 }
36
37 function onObjectResolution(object, context) {
38     /* Insert code here */
39     return object;
40 }
41
42 function onPayloadResolution(object, context) {
43     /* Insert code here; use context.directIo to write payload bytes */
44 }
45
46 function beforeDelete(object, context) {
47     /* Insert code here */
48 }
49
50 function afterDelete(object, context) {
51     /* Insert code here */
52 }
53
54 function afterCreateOrUpdate(object, context) {
55     /* Insert code here */
56 }
57
58 function getAuthConfig(context) {
59     /* Insert code here */
60 }

Cordra provides three convenience JavaScript modules that can be imported for use within your JavaScript rules. These modules allow you to search and retrieve objects, verify hashes and secrets, and perform arbitrary operations against the host Cordra as arbitrary authenticated users. Additional modules allow you to retrieve schemas and associated JavaScript hooks, as discussed here. You can optionally include these modules in your JavaScript, as shown on Lines 1-5.

You can also save external JavaScript libraries in Cordra for applying complex logic as discussed here.

Lines 7-15 export references to the hooks that Cordra enables on a Type object: beforeSchemaValidation, beforeSchemaValidationWithId, beforeStorage, objectForIndexing, onObjectResolution, onPayloadResolution, beforeDelete, afterDelete and afterUpdateOrCreate,. When handling objects, Cordra will look for methods with these names and run them if found. The methods must be exported in order for Cordra to see them. None of the methods is mandatory. You only need to implement the ones you want.

Resolution of payloads will activate both onObjectResolution and onPayloadResolution. If onPayloadResolution accesses the output via context.directIo (see Direct I/O) the hook will fully control the output bytes, and the stored payload will not be directly returned to the client.

The rest of the example shell shows the boilerplate for the methods. All, except for getAuthConfig, take both an object and a context. object is the JSON representation of the Cordra object. It may be modified and returned by beforeSchemaValidation, beforeSchemaValidationWithId, objectForIndexing, and onObjectResolution.

object contains id, type, content, acl, metadata, and payloads (which has payload metadata, not the full payload data). content is the user defined JSON of the object.

object has the following format:

{
    "id": "test/abc",
    "type": "Document",
    "content": { },
    "acl": {
        "readers": [
            "test/user1",
            "test/user2"
        ],
        "writers": [
            "test/user1"
        ]
    },
    "metadata": {
        "createdOn": 1532638382843,
        "createdBy": "admin",
        "modifiedOn": 1532638383096,
        "modifiedBy": "admin",
        "txnId": 967
    }
}

context is an object with several useful properties.

Property Name

Value

isNew

Flag which is true for creations and false for modifications. Applies to beforeSchemaValidation.

objectId

The id of the object.

userId

The id of the user performing the operation.

groups

A list of the ids of groups to which the user belongs.

grantAuthenticatedAccess

Flag which is true when the user performing the operation is considered “authenticated” for the purpose of the “authenticated” ACL keyword (generally true when the user has a user object).

authContext

Arbitrary information added by the “authenticate” hook (see Authenticate Hook).

effectiveAcl

The computed ACLs for the object, either from the object itself or inherited from configuration. This is an object with “readers” and “writers” properties.

aclCreate

The creation ACL for the type being created, in beforeSchemaValidation for a creation.

newPayloads

A list of payload metadata for payloads being updated, in beforeSchemaValidation for an update operation.

payloadsToDelete

A list of payload names of payloads being deleted, in beforeSchemaValidation for an update operation.

requestContext

A user-supplied requestContext query parameter.

payload

The payload name for onPayloadResolution.

start, end

User-supplied start and end points for a partial payload resolution for onPayloadResolution.

params

The input supplied to a Type Methods call.

directIo

Can be used for more control over input/output with Type Methods or onPayloadResolution; see Direct I/O.

attributes

Request attributes supplied with a DOIP request or a method call (see Request Attributes).

isSearch

Flag set to true in onObjectResolution if the call is being made to produce search results.

isCreate

Flag set to true in onObjectResolution if the call is being made with the result of creating a Cordra object; set to true in beforeSchemaValidation, beforeStorage, afterCreateOrUpdate for creations.

isUpdate

Flag set to true in onObjectResolution if the call is being made with the result of updating a Cordra object; set to true in beforeSchemaValidation, beforeStorage, afterCreateOrUpdate for updates; set to true when beforeDelete and afterDelete are called when an update changes an object’s type (this is treated like a deletion followed by a creation).

isDryRun

Set on a create or update according to the “dryRun” parameter. Could be used to prevent external side effects.

method

Set to the method name in beforeStorage or afterCreateOrUpdate when activated after an updating type method call rather than an ordinary create or update.

originalObject

The object before it was updated, in beforeSchemaValidation, beforeStorage, afterCreateOrUpdate, beforeDelete, afterDelete, and method calls.

beforeSchemaValidationResult

The object after beforeSchemaValidation and beforeSchemaValidationWithId but before other processing, notably before the removal of properties with secureProperty, in beforeStorage or afterCreateOrUpdate.

Before Storage Hook

This beforeStorage hook is run immediately before the object is inserted into storage. It has gone through all other processing and contains the generated id if an id was not included in the request. There is no return value for this function. As such you cannot edit the object here; however, you can perform additional validation and throw an exception if you want to reject the object.

Generate Object Id Hook

This hook is for generating object ids when objects are created.

The shell for this hook is as follows:

exports.generateId = generateId;
exports.isGenerateIdLoopable = true;

function generateId(object, context) {
   var id;
   /* Insert code here */
   return id;
}

The flag isGenerateIdLoopable when set to true tells Cordra that if an object with the same id already exists this method can be called repeatedly until a unique id is found. If the implementation of generateId was deterministic, which is to say it would always return the same id for a given input object, the isGenerateIdLoopable should NOT be set to true.

If null or undefined or the empty string is returned, Cordra will use its default identifier-minting logic.

Warning

In versions of Cordra before 2.4.0, generateId was guaranteed to run after schema validation. Cordra 2.4.0 introduced beforeSchemaValidationWithId, which requires generateId to run before schema validation. In order to preserve backward compatibility, generateId will currently still run after schema validation when beforeSchemaValidationWithId is unused; however, this flow should be considered deprecated and should not be relied on when writing new hook code. New generateId code should be robust to input which is not schema-valid, although throwing or returning null or undefined would suffice.

Hooks for the Design Object and Type Objects

The design object (of type CordraDesign) and Type objects (of type Schema) do not have separate Type objects. These built-in types can still have lifecycle hooks, however. Their JavaScript modules can be defined under a property “builtInTypes” of the design object, specifically “builtInTypes.CordraDesign.javascript” and “builtInTypes.Schema.javascript”. See Design Object.

Get Auth Config Hook

This getAuthConfig hook is unlike other hooks defined on a Type object in that it is not called with an instance of an object of that type.

The shell for this hook is as follows:

exports.getAuthConfig = getAuthConfig;

function getAuthConfig(context) {
    return {
        "defaultAclRead": [
            "test/group1"
        ],
        "defaultAclWrite": [
            "test/group1"
        ],
        "aclCreate": [
            "test/group1"
        ]
    };
}

Authenticate Hook

This hook is also looked for in the “javascript” property of the design object. It will be executed for every user request that requires authentication.

See External Authentication Provider for a substantial example of how this hook could be used.

The shell for this hook is as follows:

exports.authenticate = authenticate;

function authenticate(authInfo, context) {
   /* Insert code here */

   return { /* ... */ };
}

The authInfo passed into this function has the following structure:

{
    "username": "",
    "password": "",
    "token": "",
    "authHeader": "",
    "authTokenInput": {},
    "doipAuthentication": {},
    "doipClientId": "",
    "asUserId": ""
}

The hook can inspect “username” and “password”, or “token”, for typical authentication scenarios; but the hook also has access to the raw authentication data in various forms should it be needed.

Property Name

Description

username

String. The username from a Basic HTTP Authorization header, or from username/password input to /auth/token or DOIP authentication.

password

String. The password from a Basic HTTP Authorization header, or from username/password input to /auth/token or DOIP authentication.

token

String. The Bearer token from a Bearer HTTP Authorization header, or a JWT assertion made as /auth/token input, or a token from DOIP authentication.

authHeader

String. The value of the request’s HTTP Authorization header.

authTokenInput

Object. The input to the /auth/token endpoint or the 20.DOIP/Op.Auth.Token operation. See Create a new access token.

doipAuthentication

Object. If the request came in over the DOIP interface this property will contain the value of the passed-in DOIP authentication.

doipClientId

String. If the request came in over the DOIP interface this property will contain the clientId, if available.

asUserId

String. The asUserId property may be optionally set to indicate that the call should should be performed with the permissions that user has been granted; see As-User.

Many authenticate hook use cases can ignore this.

Default Cordra authentication logic only allows admin to use As-User; the authenticate hook can handle the As-User request by returning a userId which has the value of the provided asUserId, and can allow users other than admin as desired. Only set the output userId to the input asUserId when you really want to allow the authenticating user the full privileges of the asUserId. If the hook return userId does not match the input asUserId, the default As-User logic will occur; to prevent even admin use of As-User, throw an exception.

Note that if this property is set, and you are returning custom groupIds which you wish to be available using As-User, you should set userId to the asUserId, and return the groupIds for the asUserId.

Returning null or undefined causes the default cordra authentication code to be executed in that case.

To reject an authentication attempt throw an exception.

To accept an authentication return a result object. The result object should contain at least "active": true and the userId. It can optionally contain username and groupIds, where groupIds are the groups the user is a member of.

The active property on the result object should be set to true. If it is set to false it will cause an exception to be thrown.

Warning

Note that the userId “admin” will be given the full privileges of the Cordra admin user. Thus take care that the behavior of any external authentication provider used by the authenticate hook does not give users full control over their own userIds.

An example successful authentication result object is shown below:

{
    "active": true
    "userId": "test/123",
    "username": "fred",
    "groupIds": [ "test/group1" ],
    "grantAuthenticatedAccess": true,
    "bypassCordraGroupObjects": true
}

Property Name

Description

active

Boolean. Set to true if the authentication is successful.

userId

String. The userId of the authenticated user.

username

String (Optional). The username of the authenticated user.

groupIds

List of String (Optional). Custom groupIds the authenticated user is a member of.

bypassCordraGroupObjects

Boolean (Optional) Used in combination with groupIds. If set to true only the returned groupIds will be used. If missing or set to false any Cordra group objects that contain this users id will be combined with the custom groupIds.

grantAuthenticatedAccess

Boolean (Optional). If set to true this user is considered “authenticated” by ACLs that use that keyword. If missing Cordra ACLs will only consider this user “authenticated” if their exists a Cordra object that corresponds with their userId. If a hook allows authentication by a wide open set of users, such as any users with an account with some existing global service, then it would be appropriate to set this false. If your ACLs do not use the “authenticated” keyword, this has no effect.

exp

Number (Optional). Expiration in Epoch Unix Timestamp, the number of seconds since JAN 01 1970. (UTC) that the session should last for before expiration. If not supplied by default the session will not expire.

authContext

Arbitrary JSON (Optional). The value of authContext is provided to all lifecycle hooks (as part of the context) for operations making use of this authentication.

There is one additional special response which the authenticate hook can return:

{
    "anonymous": true
}

This response causes Cordra to behave as if there was no authentication attempt.

Warning

It is possible to lock out all users, including admin, by throwing an exception from the authenticate hook. Doing so would then prevent admin from signing in to edit the problem JavaScript. To recover from such a situation place the following example repoInit.json file in the data directory. Upon restart, this will delete all the JavaScript on the design object allowing admin to sign in to fix it.

{
    "design": {
        "javascript": ""
    }
}

Customize Query and Params Hook

This hook is also looked for in the “javascript” property of the design object. It will be executed for every user-supplied query to the Search API. It can be used for example to restrict the query to exclude certain objects based on the calling user. It can also be used to modify the query params supplied by the user for example to restrict the number of results per page, or to throw an exception when a user uses search parameters which are to be disallowed to that user. Query restriction can be accomplished either by altering the query or adding filterQueries (see Search for objects).

The shell for this hook is as follows:

exports.customizeQueryAndParams = customizeQueryAndParams;

function customizeQueryAndParams(queryAndParams, context) {
   /* Insert code here */
   queryAndParams.query = newQuery;
   queryAndParams.pageSize = 5;
   return queryAndParams;
}

Customize Query Hook

This hook is similar to the above-mentioned customizeQueryAndParams but only allows customization of the query string. If hooks customizeQueryAndParams and customizeQuery are both implemented, both will be executed with customizeQuery being run after.

The shell for this hook is as follows:

exports.customizeQuery = customizeQuery;

function customizeQuery(query, context) {
   /* Insert code here */
   return newQuery;
}

Create Handle Values Hook

This hook is for specifying the handle record that is to be returned when handles are resolved using handle client tools. This hook can be defined with other service-level hooks in Design JavaScript; it can alternatively be defined on the separate design object property design.handleMintingConfig.javascript, which can be edited by selecting Handle Records from the Admin menu on the UI.

The shell for this hook is as follows:

exports.createHandleValues = createHandleValues;

function createHandleValues(object, context) {
   const handleValues = [];
   /* Insert code here */
   return handleValues;
}

If creating handle values with JavaScript it is important to consider that all Cordra objects, even if not publicly visible, will have a handle record created. If you are storing data directly in the handle record you may wish to check if the Cordra object is publicly accessible. You can do this by inspecting the ‘context’ argument. For example:

function isPublic(context) {
    const effectiveAcl = context.effectiveAcl;
    if (effectiveAcl.writers && effectiveAcl.writers.indexOf("public") !== -1) {
        return true;
    } else if (effectiveAcl.readers && effectiveAcl.readers.indexOf("public") !== -1) {
        return true;
    } else {
        return false;
    }
}

Asynchronous Lifecycle Hooks

The return value of any hook (or type method) can be a JavaScript promise. When a hook returns a promise, Cordra will wait until the promise is fulfilled or rejected. If the promise is fulfilled, Cordra will use the value to which the promise resolved. If the promise is rejected, Cordra will treat the rejection reason like it would a thrown exception. This facility makes it possible to use asynchronous libraries to write Cordra hooks and type methods.

Throwing Errors in Schema JavaScript

Errors thrown as strings will end up in the server response, with the thrown string as the error message.

throw "An error has occurred";

If the user requests are issued via the REST API, for beforeSchemaValidation and Type methods calls, this will be returned to the user as a 400 Bad Request. For onObjectResolution and beforeDelete, this will be returned as 403 Forbidden. For search results where onObjectResolution throws an exception, the corresponding object will be omitted from the search results (this can affect search results count).

If the user requests are issued via the DOIP interface, a “bad request” or “forbidden” error will be returned.

For more control over the server response, you can also throw a custom CordraError, available via the Cordra.js Module. For example:

const cordra = require("cordra");

const responseCode = 418; // defaults to 400 or (for resolution) 403 if undefined
const response = {
    message: "Beverage Not Supported",
    requestedBeverage: "coffee",
    supportedBeverages: ["tea", "water"]
};
throw new cordra.CordraError(response, responseCode);

This will be translated into a server response with the given error message and response status code. If present, the response object will be added to the body of the server response. This can be used to send extra information about the error back to the caller.

As a convenience, if the first argument to new cordra.CordraError is a string, the response will be {"message":"that string"}. The first object can also be an instance of JavaScript Error or a Java Throwable. If there is no responseCode given, a CordraError or CordraException will be propagated using the given object’s response code, and any other Error or Throwable will be propagated with 500 Internal Server Error.

The constructor for cordra.CordraError can take an options argument as its last parameter, which will be given to the JavaScript Error constructor. This can be used to set up a cause property in the ordinary way. If the first argument to the constructor is an Error or Throwable, that will be the cause if no options argument is present.

Thrown errors other than strings and CordraError will be seen by the user as 500 Internal Server Error.

Cordra Modules

Cordra.js Module

The builtin Cordra.js module has helpful functions, listed below, that may be useful when writing JavaScript code in Type methods.

See also the Cordra Client Module which has a more general API for performing operations against the host Cordra.

Note: Lifecycle hooks are triggered when calls are made using the external APIs. Calls made to Cordra using the helpful functions in the cordra.js module do not trigger any lifecycle hooks.

Get

Use get to get an object from Cordra by the object ID:

cordra.get(objectId);

If an object with the given ID is found, it will be returned. Otherwise, null will be returned.

Payload Retrieval

Use any of the following to retrieve a payload from Cordra using the object ID and the payload name:

cordra.getPayloadAsJavaInputStream(objectId, payloadName);
cordra.getPayloadAsJavaReader(objectId, payloadName);
cordra.getPayloadAsUint8Array(objectId, payloadName);
cordra.getPayloadAsString(objectId, payloadName);
cordra.getPayloadAsJson(objectId, payloadName);
cordra.getPartialPayloadAsJavaInputStream(objectId, payloadName, start, end);
cordra.getPartialPayloadAsJavaReader(objectId, payloadName, start, end);
cordra.getPartialPayloadAsUint8Array(objectId, payloadName, start, end);
cordra.getPartialPayloadAsString(objectId, payloadName, start, end);

CordraError

See Throwing Errors in Schema JavaScript.

CordraUtil Module

Escape for Query

Will modify a string for literal inclusion in a phrase query for calling cordra.search:

const query = '/property:"' + cordraUtil.escapeForQuery(s) + '"';

Verify Secret

Used to verify a given string against the hash stored for that property:

cordraUtil.verifySecret(obj, jsonPointer, secretToVerify);

Return true or false, depending on the results of the verification.

Verify Hashes

Verifies the hashes on a cordra object property:

cordraUtil.verifyHashes(obj);

Returns a verification report object indicating which of the object hashes verify.

Hash Json

Hashes a JSON object, JSON array or primitive:

cordraUtil.hashJson(jsonElement);
cordraUtil.hashJson(jsonElement, algorithm);

Returns a base16 encoded string of the SHA-256 hash (or other specified algorithm) of the input. The input JSON is first canonicalized before being hashed.

Sign With Key

Signs a payload (a string) with a given private key in JWK format:

const jws = cordraUtil.signWithKey(payload, jwk);

Returns a Json Web Signature in compact serialization.

Sign With Cordra Key

Signs a payload (a string) with the private key of the Cordra instance:

const jws = cordraUtil.signWithCordraKey(payload);

Returns a Json Web Signature in compact serialization.

The private key used is the same key used for administering an external handle server, and can be set by including a file “privatekey” in the Cordra data directory. See Handle Server and also Cordra Configuration for the distributed version.

Retrieve Cordra Public Key

Returns the Cordra public key in JWK format:

const jwk = cordraUtil.getCordraPublicKey();

Verify With Cordra Key

Verifies a JWS with the private key of the Cordra instance:

const isValid = cordraUtil.verifyWithCordraKey(jws);

Verify With Key

Verifies a JWS with the supplied private key in JWK format:

const isValid = cordraUtil.verifyWithKey(jws, jwk);

Extract JWT Payload

Extracts the payload of a JWT, returning the parsed JSON as a JavaScript object:

const claimsObject = cordraUtil.extractJwtPayload(jws);

Get Groups for User

Given a user id, returns a list of group ids, which are the Cordra group objects that reference the user id. Does not interact with custom groups provided by the response of the “authenticate” hook.

const groupIds = cordraUtil.getGroupsForUser(user);

Validate With Schema

Given some variable input and a JSON schema, this function will validate the input against the JSON schema and return a report:

const input = {
    name: "foo",
    description: "bar"
};

const jsonSchema = {
    type: "object",
    required: [
        "name",
        "description"
    ],
    properties: {
       name: {
           type: "string"
       },
       description: {
           type: "string"
       }
   }
};

const report = cordraUtil.validateWithSchema(input, jsonSchema);

The resulting report will contain a boolean success and possibly a list of errors, where each error will have a message and possibly a pointer indicating where in the JSON the error occurs.

The JsonSchema may use $ref to reference other schemas in Cordra. For example if you have a Cordra type named Foo you could do the following:

const input = {
    name: "foo",
    description: "bar"
};

const jsonSchema = {
    "$ref": "Foo"
};

const report = cordraUtil.validateWithSchema(input, jsonSchema);

Cordra Client Module

Using require('cordra-client') or import ... from 'cordra-client' allows access to APIs for interacting with the host Cordra, as arbitrary authenticated users. The module APIs are largely compatible with the @cnri/cordra-client JavaScript library.

Import

To import, use either:

const { CordraClient, Blob } = require('cordra-client');

or:

import CordraClient, { Blob } from 'cordra-client';

Note that the Blob import is used to interact with payloads and binary schema methods, and is otherwise not necessary.

Construction, Options, Authentication

To create a new CordraClient which will interact as the admin user:

const client = new CordraClient();

The CordraClient constructor takes a single argument which is the default Options for calls made using the client. Individual calls can specify the Options instead of using the default. The Options can specify an attempt to authenticate (such as { "username": "...", "password": "..." }), but a helper static method allows creating an Options which is automatically authenticated as a specified userId:

const client = new CordraClient(CordraClient.optionsForUserId('123/abc'));

API Calls

Once you have an instance of CordraClient, call it exactly as described in the API for the cordra-client JavaScript library. In particular, calls are generally asynchronous and use of async/await is recommended.

Binary Blobs

For interacting with payloads and binary schema methods, the CordraClient APIs use the Blob type. This Blob supports methods blob.arrayBuffer() and blob.text() which return a Promise to an ArrayBuffer and a Promise to a string, respectively; it additionally supports blob.javaInputStream() which returns (synchronously) a Java InputStream instance. Additionally, a Blob can be constructed from a Java InputStream using:

new Blob([javaInputStream]);

Restrictions on Locking (Write) Interactions

Note that some JavaScript is run in a context where the Cordra object is locked against changes from multiple simultaneous calls. In the locked context it is not allowed to create, update, delete, or call instance methods on other Cordra objects – those interactions require another lock, which could lead to deadlock. To prevent this, CordraClient will throw an error when such a call is made inside the lock. Get, search, and static schema methods are always safe to call. From inside of static methods it is safe to perform locking (write) interactions; from the hooks afterCreateOrUpdate and afterDelete it is safe to perform locking (write) interactions. It is generally either not possible or not desirable to perform locking (write) interactions from inside instance methods or other hooks; see the figures at Lifecycle Hooks for details on which hooks are inside the lock.

Cordra Schemas and JavaScript

Schemas associated with type objects are available to the JavaScript via require('/cordra/schemas/Type.schema.json'), and JavaScript added to those type objects via require('/cordra/schemas/Type'). Here Type should be replaced by the name of the particular type to be accessed.

Using External Modules

Note: one simple way to share JavaScript code is to have one schema’s JavaScript access another schema’s JavaScript, as outlined in Cordra Schemas and JavaScript. Since the JavaScript can export properties besides the hooks and methods, any code can be shared this way.

Additionally, external JavaScript modules can be managed with a Cordra object as a payload configured to be a “referrable” source of JavaScript modules. Typically, this can be done on a single object of a type called JavaScriptDirectory. Here are the steps needed to create and populate the JavaScriptDirectory object.

  1. Create a new schema in Cordra called “JavaScriptDirectory” and using the “javascript-directory” template.

  2. Create a new JavaScriptDirectory object. Set the directory to /node_modules. This will allow you to import modules by filename, instead of directory path and filename.

  3. Add your JavaScript module files as payloads to the JavaScriptDirectory object. The payload name should match the filename and will be used when importing a module. For example, a payload named util.js could be importing using require('util');

The use of external JavaScript modules affects reindexing. It is currently necessary to ensure that objects of type “Schema” and any sources of JavaScript (like type “JavaScriptDirectory”) are indexed first. See Reindexing for information.

JavaScript Version

By default Cordra uses the GraalVM JavaScript engine. This allows JavaScript features up to ECMAScript 2022.

In case your JavaScript requires Nashorn, you can configure Cordra to continue using Nashorn by adding to config.json:

"javascript": {
    "engine": "nashorn"
}

Nashorn supports ECMAScript 5.1 but Cordra JavaScript using Nashorn does come prepopulated with a wide range of polyfills allowing features up to ECMAScript 2017. It is thus in many cases straightforward to write ECMAScript 2017 code and transpile it (using for example Babel) to ES5 for use in Cordra.

ECMAScript Modules

The examples in this documentation are written as CommonJS (CJS) modules, using require to import functionality and setting properties of exports to export functionality. Cordra also supports using ECMAScript (ESM) modules, which use import to import functionality and export to export functionality.

By default, Cordra will access JavaScript using CJS. If you have written an ESM module, you must set the "javascriptIsModule" property (a sibling of the "javascript" property) to true. This property exists on Type objects, and on the design object everywhere JavaScript can be included.

For shared code (Using External Modules) the type of a module must be correctly set on the caller side, that is, the caller must either require or import the module, as appropriate.

The built-in modules “cordra”, “cordra-client”, and “cordra-util” (also available for backward compatibility as “cordraUtil”) can be either required or imported.

Legacy JavaScript Hooks

In early versions of the Cordra 2.0 Beta software, the JavaScript hooks beforeSchemaValidation, onObjectResolution, and beforeDelete took the JSON content of the Cordra object, instead of the full Cordra object (including id, type, content, acl, metadata, and payloads properties). Additionally the JavaScript cordra.get function returned only the content instead of the full Cordra object.

If a Cordra instance with JavaScript written for those earlier versions needs to be upgraded, and it is not yet possible to adapt the JavaScript to the current API, then the following flag must be added to the Design object:

"useLegacyContentOnlyJavaScriptHooks": true

For more information on editing the Design object, see Design Object.

Cordra users upgrading from early versions of the Cordra 2.0 beta, who did not use schema JavaScript (apart from the default User schema JavaScript, which will be automatically upgraded if it has not been edited), do not in general need to take any action.

Examples of Hooks

Example: User Schema JavaScript

The default Cordra User schema comes with JavaScript that performs basic password validation.

const cordra = require("cordra");

exports.beforeSchemaValidation = beforeSchemaValidation;

function beforeSchemaValidation(object, context) {
    if (!object.content.id) object.content.id = "";
    if (!object.content.password) object.content.password = "";
    const password = object.content.password;
    if (context.isNew || password) {
        if (password.length < 8) {
            throw "Password is too short. Min length 8 characters";
        }
    }
    return object;
}

This code will run before the given object is validated and stored. If this request is a create (context.isNew is true) or contains a password, the password is checked to make sure it is long enough. If not, an error is thrown. This error will be returned to the callee and can be displayed as desired.

Example: Document Modification

In this slightly more complicated example, we will bind lifecycle hooks to the Document type pre-defined in Cordra with the following features:

  • Add a timestamp to the description of the document in a way it is stored.

  • Add a timestamp to the description when the object is resolved, but not actually store.

  • Require that the description be changed to “DELETEME” before the document can be deleted.

To demonstrate loading JavaScript from an external file, the function to create the timestamp is in a file called util.js. Create a JavaScript Directory (as described above) and upload this file as a payload named util.js.

exports.getTimestampString = getTimestampString;

function getTimestampString(isResolution) {
    const currentDate = new Date();
    if (isResolution) {
        return '\nResolved at: ' + currentDate;
    } else {
        return '\nLast saved: ' + currentDate;
    }
}

Next, edit the Document type in Cordra and put the following in the JavaScript field.

const util = require('util');

exports.beforeSchemaValidation = beforeSchemaValidation;
exports.onObjectResolution = onObjectResolution;
exports.beforeDelete = beforeDelete;

function beforeSchemaValidation(object, context) {
    if (object.content.description !== 'DELETEME') {
        object.content.description += util.getTimestampString(false);
    }
    return object;
}

function onObjectResolution(object, context) {
    object.content.description += util.getTimestampString(true);
    return object;
}

function beforeDelete(object, context) {
    if (object.content.description !== 'DELETEME') {
        throw 'Description must be DELETEME before object can be deleted.';
    }
}

Finally, create a new document in Cordra. You should see that whenever the document is updated, a new timestamp is appended to the description. If you view the document’s JSON, you should see a single resolution timestamp, which changes on every resolution. Finally, if you try to delete the document without changing the description to “DELETEME” you should see an error message.

Example: Modification of the Indexed Object

It is possible make changes to the object that is indexed such that it differs from the object that is stored. This is achieved by writing a function called objectForIndexing.

exports.objectForIndexing = objectForIndexing;

function objectForIndexing(object, context) {
    if (object.content.name == "foo") {
        object.content.otherName = "bar";
    }
    return object;
}

In this example if the incoming object has a property called name with the value foo, a new property will be added to the indexed object called otherName with the value bar. The object that is stored will not contain the new property but you will be able to search for this object via this property with the query /otherName:bar.

Example: Generating ID

Example JavaScript for generating object ids is shown below. Here we generate a random suffix for the handle in base16 and append it to a prefix. By setting isGenerateIdLoopable to true, we ask Cordra to repeatedly call this method until a unique id is generated.

const cordra = require('cordra');

exports.generateId = generateId;
exports.isGenerateIdLoopable = true;

function generateId(object, context) {
    return "test/" + randomSuffix();
}

function randomSuffix() {
    return Math.random().toString(16).substr(2);
}

Example: Creating Handle Values

Example JavaScript for creating handle values is shown below. The JavaScript puts a copy of the information from the Cordra object in the Handle record.

exports.createHandleValues = createHandleValues;

function createHandleValues(object) {
    const handleValues = [];
    const dataValue = {
        index: 500,
        type: 'CORDRA_OBJECT',
        timestamp: new Date(object.metadata.modifiedOn).toISOString(),
        data: {
            format: 'string',
            value: JSON.stringify(object.content)
        }
    };
    handleValues.push(dataValue);
    return handleValues;
};