Callback API

The callback API allows for registration of callbacks for different phases of the content lifecycle.

Configuration

Callbacks are defined by configuration content and registered with ACE by updating the content service config.

The callback contents store JavaScript source code containing the actual callback functions. (See read pipeline below for details about these functions.)

Defining callback contents

Callbacks are referenced by name, these names are content aliases in the _config namespace.

As a developer, you do not need to care about the exact format of these contents (but you can always fetch them if you are curious), but rather import JavaScript files with "magic" file name suffixes.

  • *.composer.js will create a content composer
  • *.mapper.js will create an aspect mapper
  • *.predelete.js will create a pre delete callback
  • *.prestore.js will create a pre store callback
  • *.onconflict.js will create an on conflict callback
  • *.oncontentinit.js will create a post content initialization callback - NOTE: these callbacks are currently only executed in the Content Developer application

The names of these callbacks will be the filename with the magic suffix removed.

Example: importing a file called foobar.composer.js will create a composer called foobar, which is a callback content with the alias _config/foobar.

Registering callbacks

In the following example we assume that there is a type article.

Importing the following file will instruct the content importer to update the content service config, and add some lifecycle callbacks for the article type.

{
  "importerMetadata": {
    "alias": "_config/aceCallbacksConfig",
    "updateMode": "merge"
  },

  "aspects": {
    "aceCallbacksConfig": {
      "_type": "aceCallbacksConfig",

      "preStoreHooks": {
        "article": ["content.prestore", "article.prestore"]
      },

      "onConflictHooks": {
        "article": ["article.onconflict"]
      }
    }
  }
}

Where content.prestore, article.prestore and article.onconflict are references to callback contents.

Read pipeline

Each of these callback types are defined by content that store JavaScript code. The JavaScript code must define a function with a special name, details below.

Mappers

A mapper is used to produce an alternative representation of an aspect. It can convert the aspect's data and its type. The aspect can be removed from the content result by returning undefined.

A mapper must be a pure function, i.e. always return the same input for a given input value.

  • File name: *.mapper.js
  • Function name: map
  • Arguments: aspect (a JavaScript object corresponding to a content aspect)
  • Returns: aspect data (the data property of the aspect)
function map(aspect) {
  aspect.author = 'Foo Bar';
  return aspect;
}

Composers

A composer is used to produce an alternative representation of a content. It can convert the content's data and its type. Composers are called as the last step in a data processing pipeline. Any matching aspect mappers will be applied before the composer is called. Since composers can alter the cacheability of the input data, it can also control the cache headers, by default the cache settings are wiped when a composer has processed the content.

A composer may fetch other contents as a part of the composition.

  • File name: *.composer.js
  • Function name: composer
  • Arguments:
    • content: a JavaScript object representing the content
    • variant: the variant name as String
    • requestParameters: a JavaScript object containing all request parameters
  • Services:
    • ContentService: A service to get additional content if needed, with it's API based on the ACE UI ContentService.
  • Global object:
    • CacheControl: The input cacheControl, default or set by previous composer.
  • Returns: The transformed representation of the content.

Example of a composer that combines data from different aspects

function compose(content, variant, requestParameters) {
  var article = content.aspects.MyArticle;
  var aspect = content.aspects.MyOtherAspect;

  article.lead = '<p>' + article.title+ '</p>\n' + article.body + aspect.foo;

  return content;
}

Example of composer that fetches other contents as a part of content composition.

Note: .getContent returns a promise.

function compose(content) {
  return ContentService.getContent({alias: content.aspects.MyAspect.ref})
                       .then(function(resp) {
                         content.aspects.MyAspect = resp.content.aspects.MyAspect;
                           return content;
                       });
}

Example of composer that fetches content history as a part of content composition.

Note: .getContentHistory returns a promise.

function compose(content) {
  return ContentService.getContentHistory({alias: content.aspects.MyAspect.ref})
                       .then(function(resp) {
                         // The resp object is the content history
                         return content;
                       });
}

Example of composer that uses cacheControl

function compose(content) {
  content.cacheControl = CacheControl;
  content.cacheControl.maxAge = 42;

  return content;
}

The CacheControl has the following properties settable:

 number maxAge;
 number sMaxAge;
 boolean private;
 boolean public;
 boolean noCache;
 boolean mustRevalidate;
 boolean proxyRevalidate;

Any properties that are not set will get a default that disables caching. The same goes if the return value doesn't have a cacheControl property. The CacheControl object that is available to the composer is ignored unless it is copied into the cacheControl property of the return value.

The cache control mechanism is based on the Cache-Control header in HTTP, and is translated to the corresponding HTTP header when used in a web service. See HTTP Specification: Cache-Control for details.

Write pipeline

It is possible to intercept some content lifecycle events by registering lifecycle callbacks.

Lifecycle callbacks may abort the current operation by throwing an exception, preStore may alter the data to be stored, and onConflict callbacks can be used to automatically resolve conflicts during update.

  • preStore callbacks are called before content is stored
  • preDelete callbacks are called before content is deleted
  • onConflict callbacks are invoked when an attempted store will result in a conflict
  • onContentInit callbacks are called after a content has been initialized (NOTE: these callbacks are currently only executed in the Content Developer application)

Pre Store

A preStore callback can modify the data to be stored during content update, or abort the save by throwing an exception.

  • File name: *.prestore.js
  • Function name: preStore
  • Arguments: a JavaScript object representing the content to be stored
  • Returns: The, possibly transformed, representation of the content.
  • Throws: a JavaScript object that describe the reason why the store operation was aborted

An example of a preStore callback that appends 'foo' to the article aspect's name property.

function preStore(content) {
  content.aspects.article.data.name += 'foo';
  return content;
}

An example of a preStore callback that aborts the current write operation by throwing a JavaScript object.

The exception is an object with the following properties:

  • status: The (HTTP) status code that will be sent to the user. Defaults to 500.
  • message: The reason for aborting.
  • extraInfo: A JavaScript object with additional information related to the error.
function preStore(content) {
  throw {
    status: 403,
    message: 'NO CAN DO',
    extraInfo: {
      'foo': 'bar',
      'baz':'qux'
    }
  }
}

Pre Delete

A preDelete callback will be notified when content of its type is about to be removed, and can abort the delete operation by throwing an exception.

  • File name: *.predelete.js
  • Function name: preDelete
  • Arguments: a JavaScript object representing the content that is about to be removed
  • Returns: Nothing
  • Throws: a JavaScript object that describes the reason why the remove operation was aborted
function preDelete(content) {
  throw {
    status: 403,
    message: 'NO CAN DO',
    extraInfo: {
      'foo': 'bar',
      'baz':'qux'
    }
  }
}

On Conflict

A onConflict callback will be invoked whenever a store operation conflicts with a previous update. The callback can be used to automatically resolve the conflict.

  • File name: *.onconflict.js
  • Function name: onConflict
  • Arguments:
    • content - A JavaScript object representing the content about to be stored
    • conflicting_content - A JavaScript object that represents the conflicting content
  • Returns: The data to be stored, which is considered a resolved conflict.
  • Throws: a JavaScript object that describes the reason why this store operation was aborted, or undefined to signal that this callback could not resolve the conflict.

There can be a list of onConflict callbacks for a given type, and in this case, each of them will be asked to resolve the conflict in order. If all of them return undefined, a conflict will be signalled to the user.

A onConflict callback that always resolve conflicts by storing the new version.

function onConflict(content, conflicting_content) {
  return content;
}

On Content Init

A onContentInit callback will be invoked whenever a content is created in the Content Developer application. The callback can then bootstrap and enrich the content prior to it being loaded by the content editor.

  • File name: *.oncontentinit.js
  • Function name: onContentInit
  • Arguments:
    • content - the newly created (empty) content object
    • context - the content context from which the new content was created
    • userData - the user data content of the currently logged in user
  • Returns: The (possibly) enriched content that should be loaded by the Content Developer content editor
  • Throws: a JavaScript object that describe the reason why the onContentInit operation failed

An example of a onContentInit callback that initialize the byline field of the newly created content to be the name of the current user.

function onContentInit(content, context, userData) {
  // We automatically bootstrap the article byline to include the name of the
  // current user as long as we have access to his passport aspect

  if (userData && userData.aspects && userData.aspects.acePassport) {
    var passport = userData.aspects.acePassport;

    if (passport.loginName) {
      content.aspects = content.aspects || {};
      content.aspects.article = content.aspects.article || { _type: 'article' };

      content.aspects.article.byline = "An article by " + passport.name;
    }
  }

  return content;
}

Javascript Modules

ACE supports CommonJS modules for library code. By default there are two modules available: console.js (a simple implementation of a console), and q.js (the Q promise library). Additional modules can be added by adding files to the _config/js-modules content in the node_modules directory.

Adds a module named module1.js

{
  "importerMetadata": {
    "alias": "_config/js-modules",
    "updateMode": "merge"
  },

  "aspects": {
    "aceFiles": {
      "_type": "aceFiles",
      "files": {
        "node_modules/module1.js": {
          "filePath": "node_modules/module1.js",
          "fileUri": "file:contentfiles/module1.js"
        }
      }
    }
  }
}

To use an available module in your callback just require it like this:

...
Q = require("q.js")
c = require("console.js")
m1 = require("module1.js")
...