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.
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.
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.
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. |
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.
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.
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.
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"
]
};
}
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 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 |
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 |
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": ""
}
}
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;
}
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;
}
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;
}
}
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.
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.
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.
Use the search function to find objects in the Cordra index:
cordra.search(query, params)
This will return an array (in JSON sense) of Cordra objects matching the query.
params
is an optional object with optional fields pageNum
, pageSize
,
sortFields
, facets
, filterQueries
, includeScore
, and includeVersions
.
The default behavior is to get all results for a query, which corresponds to a pageNum
of 0
and pageSize
of -1
.
Caution should be used when requesting all results when the query might match a very large
number of objects.
The legacy form cordra.search(query, pageNum, pageSize, sortFields)
still functions as well.
Note: Former versions of Cordra would return all results with pageSize=0. To restore this former behavior, you can add
"useLegacySearchPageSizeZeroReturnsAll":true
to the Cordra Design object. By default a search with pageSize=0
returns the number of matched objects but no object content.
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.
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);
Will modify a string for literal inclusion in a phrase query for calling cordra.search
:
const query = '/property:"' + cordraUtil.escapeForQuery(s) + '"';
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.
Verifies the hashes on a cordra object property:
cordraUtil.verifyHashes(obj);
Returns a verification report object indicating which of the object hashes verify.
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.
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.
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.
Returns the Cordra public key in JWK format:
const jwk = cordraUtil.getCordraPublicKey();
Verifies a JWS with the private key of the Cordra instance:
const isValid = cordraUtil.verifyWithCordraKey(jws);
Verifies a JWS with the supplied private key in JWK format:
const isValid = cordraUtil.verifyWithKey(jws, jwk);
Extracts the payload of a JWT, returning the parsed JSON as a JavaScript object:
const claimsObject = cordraUtil.extractJwtPayload(jws);
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);
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);
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.
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.
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'));
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.
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]);
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.
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.
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.
Create a new schema in Cordra called “JavaScriptDirectory” and using the “javascript-directory” template.
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.
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.
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.
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.
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.
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.
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.
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 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 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;
};