Business Logic

Introduction

Kinvey Business Logic is a node.js code execution runtime that allows you to customize the behavior of requests to your backend by providing a means to implement server-side business logic. Kinvey Business Logic enables developers to focus on the unique functional business logic of a mobile application, while Kinvey handles the work of receiving, parsing, and routing requests, connecting to and querying data stores, and enabling mobile-specific functionality.

Business Logic is commonly used to enable:

  • Triggering messages such as email and push notifications on developer-defined changes in the data store.
  • Optimizing the data exchange with the app by joining multiple data collections.
  • Validating app user actions in order to enforce business rules.
  • Providing a means to host platform agnostic code and save development time on multi-platform apps.

Business logic can be invoked as part of a request to a Kinvey collection, as a custom endpoint to execute an arbitrary function, as common code created and shared among BL functions, and on a scheduled basis.

This guide provides an introduction to the core concepts of Kinvey Business Logic. For a complete reference to the APIs exposed by Kinvey Business Logic, see the Business Logic API Reference.

Core Concepts

The Elements of a Business Logic Script

A business logic script in Kinvey is a single function that is executed as part of a client request, either as a hook to a collection request, or as a custom endpoint that is either directly invoked or scheduled to execute at a specific time or on a repeating basis. All Business Logic functions are passed three arguments that can be used as part of your business logic:

ArgumentDescription
requestAn object representing the request that came into Kinvey
responseAn object representing the response that will be sent back to the client
modulesA series of API modules that can be used to manipulate or action on a request

The request and response objects allow you to manipulate what comes in and goes out of the function. For example, in the below simple Hello World example, the response body is set to the contents of the request body, plus one additional key named hello with the value of world.

functon onRequest(request, response, modules) {
  var requestBody = request.body; // this gets the JSON body of the request
  response.body = request.body;
  response.body.hello = 'world';
  response.complete(200);
}

So if the request sent to the client contained the body

{ 
  "name":"Bob",
  "department": "sales"
}

then the response received from the client would be

{
  "name":"Bob", 
  "department":"sales",
  "hello":"world"
}

The modules argument provides a variety of libraries that can be used within your business logic. These include, but are not limited to:

  • Kinvey-specific modules for accessing different elements of the Kinvey platform
  • Control-flow modules such as async.js and the Bluebird promises library, used for controlling node.js asynchrony.
  • Utility libraries for working with dates
  • Data Validation libraries
  • User engagement libraries for sending emails and push notifications

For a complete list of the APIs exposed by Kinvey Business Logic, see the Business Logic API Reference.

Best Practices

Business logic scripts are short-running scripts that are intended to augment the default functionality of Kinvey. When writing server-side business logic, it is important to take total request time into account, in order to provide a good user experience for the end-user.

While Kinvey Business Logic has a built-in timeout value that is measured in seconds, code should be written in such a way as to keep the total request time as low as possible in order to maintain a good user experience. In general, users can perceive any delay that is greater than 100 milliseconds, and most users consider anything that takes longer than one second to be slow. While it is sometimes necessary to perform tasks beyond these numbers, long-running requests should be kept to a minimum and the goal should be short, responsive API calls. To help keep requests short, you should:

  • Make use of node.js asynchrony. Multiple asynchronous requests (such as collection access, external HTTP requests, etc.) should be parallelized if possible.
  • Make use of the general collection hook flow when possible rather than custom endpoints. For example, if you need to combine the results from two collections, make a query request to the first collection, and query the second collection in a post-fetch hook.
  • Avoid looping through large recordsets using for or forEach. Instead, use the async module's each, eachSeries, and eachLimit functions along with node's process.nextTick / setImmediate.
  • Keep business logic scripts short and atomic.
  • Avoid returning large data sets to the mobile client. Mobile devices are optimized, both from a user experience perspective and a bandwidth perspective, to digest limited, relevant data sets rather than syncing entire copies of tables and/or collections.

Types of Business Logic

Business Logic is triggered in one of three ways:

  • Collection Hooks, which are triggered by a request to any Kinvey collection and allow for server-side logic to occur pre/post fetch, pre/post save, and pre/post delete.
  • Custom Endpoints, which are triggered on demand through a custom HTTP endpoint or via a function call in the client libraries.
  • Scheduled Code: A means of executing a custom endpoint on a scheduled basis.

In addition, common code provides a means of creating a library of reusable functions that can invoked from any collection hook or endpoint.

Collection Hooks

Collection hooks allow you to add server-side logic to Kinvey's collection request pipeline. Understanding this pipeline is essential for effective use of Collection Hooks.

Configuring Collection Hooks

All Kinvey collection requests go through a similar processing sequence.

  1. Verify request is authenticated
  2. Perform any Before processing Collection Hook Business Logic
  3. Do internal Kinvey processing (this includes ACL verification, database interaction, data integration, etc)
  4. Perform any After processing Collection Hook Business Logic
  5. Format and send response to the client

You are able to insert your own code as either a Before or After processing action on a Kinvey collection.

To configure a collection hook via the console, find your collection under the Collection Hooks list in Business Logic. Use the Add Hook button next to the collection name to add a hook.

Kinvey Business Logic

For complete request API documentation see Kinvey Requests in the Business Logic API reference.

Invoking Collection Hooks

In order to invoke a collection hook, simply access the collection for which the hook is defined, and the appropriate hook for your action will be executed. For example, if you have defined an After Fetch hook for a collection named myCollection, your code will execute every time data is retrieved (fetched) from the myCollection collection.

In order to make it easier to know whether collection hooks were executed for a specific request, Kinvey will return a X-Kinvey-Executed-Collection-Hooks header as part of the response, containing a string with a comma-separated list of hooks (i.e. "Pre-Fetch, Post-Fetch" or "Post-Save"). If this header is not included in the response from Kinvey, then no collection hooks were executed as part of your request.

Custom Endpoints

Custom Endpoints provide for the on-demand execution of server-side business logic. Custom Endpoints can be initiated via a REST request, or from the client libraries.

Configuring Custom Endpoints

To configure a custom endpoint via the console, go to the Endpoints list in Business Logic. Use the Add Endpoints button next to add an endpoint. You will be prompted to give the endpont a name:

Kinvey Business Logic

This will bring you to the editor, where you can input your business logic code.

Kinvey Business Logic

Invoking an Endpoint

Custom endpoints are accessed through the Client method customEndpoints(). This returns a re-usable instance of the AsyncCustomEndpoints class, which has a method callEndpoint(String endPoint, GenericJson input, KinveyClientCallback callback),

  • the endPoint name is the same as custom endpoint, defined in the console.
  • the input should be a GenericJson this is passed to the endpoint as the request.body. For example, if input were:
      GenericJson myInput = new GenericJson();
      myInput.put("name","Fred Jones");
      myInput.put("eyes", "blue");

and the endpoint logic were

function onRequest(request, response, modules){
    modules.logger.info(request.body.name + " has " + request.body.eyes + " eyes.");
    response.continue();
}

will cause the following to be written to the console logs:

Fred Jones has blue eyes.

The following example calls an endpoint called tagsNearMe without any input:

AsyncCustomEndpoints endpoints = getClient().customEndpoints();
try{
    GenericJson result = endpoints.callEndpoint("tagsNearMe", new GenericJson()).execute();
}catch (IOException e){
    System.out.println("Couldn't hit endpoint! -> " + e);
}

Common Code

Common code can be used to create reusable functions that can be used across your business logic scripts. To configure common code via the console, go to the Common list in Business Logic. Use the Add Common button next to add common code. You will be prompted to give the common code a name:

Kinvey Business Logic

An example of a common code function that adds two numbers is:

function add(a, b) {
  return a + b;
};

The add function can now be invoked from any collection hook or custom endpoint

function onRequest(request, response, modules) {
  // some BL code here
  var a = 1;
  var b = 2;
  var c = add(a,b);
  // more BL code here
}

Scheduled Code

Custom endpoints can be scheduled to run at specified intervals. To schedule an endpoint, click on the schedule button at the top right of the screen when in a custom endpoint.

You can select a start date and time as well as an interval (once, daily, weekly, hourly, 30-minutes, 5-minutes).

Kinvey Business Logic

Writing Business Logic

Business logic provides a node.js v0.10 runtime for executing your BL code. The code executes inside a container that is destroyed immediately after use. All node.js core libraries are available, but some (such as file access, console access, setting up a http or tcp server, etc.) have no practical effect due to the ephemeral nature of Business Logic. Additionally, Kinvey currently does not support the addition of custom npm modules to BL code. Otherwise, the full node.js stack is available within the context of a collection hook or endpoint.

From time to time, Kinvey will add specific whitelisted npm modules (such as async.js, moment.js, and Bluebird) to Business Logic, when it is applicable to the general Kinvey community. If there is a module that would be valuable to your app, you can submit it to support@kinvey.com for consideration. Please note that modules are only added if they have general applicability.

Understanding Asynchrony

Because it is based on node.js, Kinvey Business Logic makes heavy use of asynchronous JavaScript patterns. In general, calls that require I/O (database access, web requests, etc) are executed asynchronously while code execution continues. Upon completion of the asynchronous call, a callback function is invoked. For example, in the below block of code:

function onRequest(request, response, modules) {
  modules.request.get('http://www.somedomain.com', function(err, result) {
    modules.logger.info("Request finished");
    return response.complete();
  });

  modules.logger.info("Continuing code execution");

}

will output to the log:

Continuing Code Execution
Request Finished

This is because as soon as the call is made to invoke a request to http://www.somedomain.com, code exexution continues with the statement to log "Continuing code execution". Once the request is finished, the statement "Request finished" will be logged, and BL execution will be completed.

This asynchronous callback mechanism allows for the optimization of request time, but can also lead to many errors. For example, if we moved the return statement in the above code as follows:

function onRequest(request, response, modules) {
  modules.request.get('http://www.somedomain.com', function(err, result) {
    modules.logger.info("Request finished");
});

  modules.logger.info("Continuing code execution");
  return response.complete();
}

the callback for the request would never be invoked. Outside of asynchronous calls with callbacks, code in node.js executes synchronously to completion.

Completing Business Logic

As shown above, JavaScript code often uses callbacks that are invoked when processing completes. Since Kinvey can't know when a database or network request will complete, Kinvey doesn't know when your logic is done running. To counter this you must return either response.complete, response.continue, or response.error when your logic is finished executing.

  • return response.complete(statusCode) -- Stop processing this request further and return response to the client. The HTTP status code is set to statusCode.
  • return response.continue() -- Continue normal Kinvey request processing. This is only valid within collection hooks and is not available for custom endpoints.
  • return response.error(error) -- Stop processing this request further and return an HTTP 400 error to the client with a user-defined error message. Details on user-defined errors can be found in the Throwing Errors section of this guide.

For a list of valid HTTP status codes, see the List of HTTP status codes. Client libraries check these status codes to determine if a response contains an error. Make sure to follow the guidelines in the linked document.

If response.complete, response.continue, or response.error is not returned within the timeout period, your logic will be terminated and the client will be notified that processing timed out.

Business Logic Errors

Handling Errors

If an error occurs during execution of your Business Logic code, we return an HTTP status code and a JSON error response to the client.

The HTTP status codes returned are:

CodeDescription
400There was a runtime error in the Business Logic (probably triggered by a request) you should modify the request on the client and resubmit (or verify your code has no logic errors)
500There was an internal error within the Kinvey Business Logic system. Contact support@kinvey.com if this happens
550There was a server side error with your code, you should verify the correctness of your code

The format of the error response is:

{
    "debug": "",
    "description": "",
    "error": ""
}

Description is set to a descriptive message of the general failure, debug is set to the exact cause of the failure and error is one of:

ErrorCodeDescription
BLRuntimeError400There was a runtime error executing the JavaScript
BLSyntaxError550There was a syntax error compiling the JavaScript
BLTimeoutError500The backend processing failed to complete, please contact support@kinvey.com if you experience this error
BLViolationError550The Business Logic violated a constraint
BLInternalError550The Business Logic system experienced an internal error

Calling response.complete(400) or response.complete(550) will cause the client to see an error identical to normal Business Logic errors. Make sure to check the error in the response.

Validation and push triggers are built on Kinvey's Business Logic feature. You may see the above errors using the validation or push trigger features. If you're seeing a BLTimeoutError error, please contact support@kinvey.com.

Throwing Errors

At times, it may be necessary in your business logic code to throw an error back to the client. To accomplish this, your business logic code can return a User Defined Error to the client by calling response.error(err). This function either takes a String error message, or a JavaScript Error object or one of its subtypes. Errors are returned using the standard Kinvey error response format in the message body, with a response code of 400 (Bad Request). For example, the following code:

if (!myTestCondition) {
  return response.error("my custom message");
} else {
  return response.complete(200);
}

will produce a response.body of:

{
    "error": "BLRuntimeError",
    "description": "The Business Logic script has a runtime error. See debug message for details.",
    "debug": "UserDefinedRuntimeError:  my custom message"
}

If a JavaScript object such as a TypeError is passed, the response.body will reflect the JavaScript error type:

if (!myTestCondition) {
  var myError = new TypeError("Invalid Object Type");
  return response.error(myError);
} else {
  response.complete(200);
}
{
    "debug": "BLRuntimeError",
    "description": "The Business Logic script has a runtime error. See debug message for details.",
    "error": "TypeError:  Invalid Object Type"
}

Change notification

Kinvey has the ability to send email notifications to collaboratores in an app whenever a BL script changes. To enable this functionality for your app, please contact Kinvey support.

Examples

Business Logic is designed to help you perform tasks on the server that are either not possible or inefficient on a client. To use an example, just copy the code into the listed function for the collection that should trigger the logic.

Each example has a tip that tells you where in the management console to enter the code.

Business logic code (both Collection Hooks and Custom Endpoints) are managed in the Business Logic section of the console, under their respective folders. To create new hooks or endpoints, click the plus next to their respective folder names and follow the prompts.

For Collection Hooks, each of the listed events and request types (onPreSave, onPostFetch, etc) will only be triggered by a request of the same type (save, fetch, or delete) to the collection where the function is defined. Collections listed in the examples are other collections referenced in the logic don't need to be the same collection where the logic is defined.

Providing a test end-point

function onRequest(request, response, modules){
    response.body = {"someValue": "someOtherValue"};
    response.complete(201);
}

Any POST received by this endpoint just returns a 201 status with the response body of:

{
    "someValue": "someOtherValue"
}

Code is a Custom Endpoint Business Logic.

Requesting data from an API

function onRequest(request,response,modules){
    var req = modules.request;
    req.get('http://developer.mbta.com/Data/Red.json', function(error, resp, body){
        if (error){
            response.body = {error: error.message};
            response.complete(400);
            return;
        }
        response.body = JSON.parse(body);
        response.complete(resp.status);
    });
}

This requests the MBTA Red Line real-time subway feed, parses the JSON response and sends this JSON to your client.

APIs have various terms of service governing the use of the API. This includes rate limits, caching and storage policies, copyright guidelines and display requirements. Make sure to follow the API terms of service to avoid service interruptions. For example, the MBTA API ToS is documented in the MassDOT and Developer's Relationship Principles and the Developer's License Agreement documents.

Code is a Custom Endpoint Business Logic.

Posting data to an API

function onPreSave(request, response, modules){
  var endpointName = 'isCreditAvailable';  
  var uriString = 'https://baas.kinvey.com/rpc/' + 
    modules.backendContext.getAppKey() + '/custom/'+endpointName;
  var opts = {
    uri: uriString,
    method: 'post',
    headers: {
      'Authorization': request.headers.authorization
    },
    json:true,
    body: { "userid": request.body.userid, "balance": request.body.balance }
  };

  modules.request.request(opts, function( err, resp, body ) {
    if (err) {
      response.body = err;
    } else if (response.body.valid === true) {
      response.continue();
    } else {
        response.error("No balance remaining.");
       }
  });
}

This code sends a REST POST request using the request module to a custom endpoint for the purposes of validating if a particular account has credit available. Here, a request body in JSON format is provided, so json:true must be included in the request options. Authentication is passed through using the request.headers.authorization. Once the response is received, the request pipeline continues if there is balance available, and completes if there is not.

Code goes in the Before Save Business Logic code.

Triggering push notifications

function onPreSave(request,response,modules){
    var collectionAccess = modules.collectionAccess
    , userCollection = collectionAccess.collection('user')
    , utils = modules.utils
    , push = modules.push
    , template = '{{name}}, the {{team}} just won!'
    , pushedMessageCount = 0
    , userCount;


    // Find all users who's favoriteTeam matches the incomming request's winning team
    userCollection.find({"favoriteTeam": request.body.winningTeam}, function(err, userDocs){
        // Total number of messages to send
        userCount = userDocs.length;

        // Each message is customized
        userDocs.forEach(function(doc){
            var values = {
                name: doc.givenName,
                team: doc.favoriteTeam
            };

            // Render the message to send
            var message = utils.renderTemplate(template, values);

            // Send the push
            push.send(doc, message);

            // Keep track of how many pushes we've sent
            pushedMessageCount++;

            // reduce the number of users left to push to
            userCount--;
            if (userCount <= 0){
                // We've pushed to all users, complete the request
                response.body = {"message": "Attempted to push " + pushedMessageCount + " messages."};
                response.complete(200);
            }
        });
    });
}

This sends a push notification to all users who match the winning team provided by a POST or PUT (also known as save in Kinvey client libraries) to a collection. Instead of saving any data to the data store, this request just causes the push notification to be sent.

The body of the request must have a winningTeam property, like

{
    "winningTeam": "Boston Red Sox"
}

Users are expected to have a givenName property and a favoriteTeam property.

The Push notification will be similar to:

Joe, the Kansas City Royals just won!

Code is a Custom Endpoint Business Logic.

Triggering email notifications

function onPostSave(request,response,modules){
    var i, to, logger = modules.logger, email = modules.email;
    for (i = 0; i < request.body.to.length; i++){
        to = request.body.to[i];
        logger.info("Sending e-mail to: " + to);
        email.send('my-app-invitation@my-app.com',
                   to,
                   request.body.subject,
                   request.body.body,
                   request.body.reply_to);
    }
    response.continue();
}

This sends an email to an array of email addresses on a POST or PUT (also known as save in Kinvey client libraries) to a collection. The body of the request should be similar to:

{
    "to": ["email1@example.com", "email2@example.com"],
    "subject": "This is a test email!",
    "body": "Please attend my event @ http://url.shorten/llayhshd",
    "reply_to": "event-creator@event-host.com"
}

Code goes in the After Save Business Logic code.

Merging data in two collections

function onPostFetch(request, response, modules){
    var collectionAccess = modules.collectionAccess
      , logger = modules.logger
      , body = request.body
      , resultA = null, resultB = null, callback;

    callback = function(){
        if (resultA && resultB){
            // Got both responses
            response.body = {things: resultA, stuff: resultB};
            response.complete(200);
        } else {
            // Still waiting for the other response.
            return;
        }
    };

    collectionAccess.collection('Things')
                    .find({name: body.thing}, function (err, docs)
    {
        if (err) {
            logger.error('Query failed: '+ err);
            response.body.debug = err;
            response.complete(500);
          } else {
            resultA = docs;
            callback();
          }
    });

    collectionAccess.collection('Stuff')
                    .find({name: body.stuff}, function (err, docs)
    {
        if (err) {
            logger.error('Query failed: '+ err);
            response.body.debug = err;
            response.complete(500);
          } else {
            resultB = docs;
            callback();
          }
    });

}

Give a request with a body of {"thing": "something", "stuff": "everybody's got stuff"}, this fetches the matching things and stuff from the datastore and then returns them to the client with a status code of 200.

Code goes in the After every Fetch Business Logic.

Denying access to a user

function onPreFetch(request, response, modules){
    var collectionAccess = modules.collectionAccess
      , logger = modules.logger
      , username = request.username;  // Set by Kinvey

    collectionAccess.collection('bannedUsers')
                    .find({user: username}, function (err, docs)
    {
          if (err) {
            logger.error('Query failed: '+ err);
            response.body.debug = err;
            response.complete(500);
        } else {
            if (docs.length > 0){
                // Banned user
                response.body.reason = docs[0].reason; // Just show the first reason
                response.complete(403);
            } else {
                // Not banned, let it through
                response.continue();
            }
        }
    });
}

This checks for the presence of the current user in the bannedUsers collection and rejects the request with a status code of 403 if the user is banned. It also reports the reason in the response body.

Code goes in the Before every Fetch Business Logic.

Returning the values deleted

function onPreDelete(request, response, modules){
    var collectionAccess = modules.collectionAccess
      , logger = modules.logger
      , query = request.params.query;

    collectionAccess.collection('thisCollection')
                    .find(query, function (err, docs)
    {
        if (err) {
            logger.error('Query failed: '+ err);
            response.body.debug = err;
            response.complete(500);
        } else {
            modules.utils.tempObjectStore.set('recordsDeleted', docs);
            response.continue();
        }
    });
}

function onPostDelete(request, response, modules){
    response.body = modules.utils.tempObjectStore.get('recordsDeleted');
    response.complete(200);
}

This runs the query that will be deleted against the datastore and stores the results in the tempObjectStore. The onPostDelete function the extracts those and returns them to the client.

This has code for both the Before every Delete and After every Delete Business Logic code.

Returning a Kinvey Entity

When adding external data, whether generated through Business Logic or via an external API request, it is necessary to add Kinvey metadata to the object before submitting it to a Kinvey collection. The kinveyMetadata method can be used to return a skeleton object consisting of only Kinvey metadata, return a skeleton object with a user-assigned ID, or add/update Kinvey metadata on an existing JSON object. For example:

function onPreFetch(request,response,modules){
  var req = modules.request;
  var entity = modules.utils.kinveyEntity;
  req.get('http://api.openweathermap.org/data/2.5/weather?q=Boston,ma', 
      function(error, resp, body){
    if (error){
      response.body = {error: error.message};
      response.complete(400);
      return;
    }
    response.body = entity(JSON.parse(body));
    response.complete(resp.status);
  });
}

This example requests the current weather forecast for Boston, MA from OpenWeatherMap, parses the JSON response, and adds the Kinvey Metadata attributes before sending this JSON to your client.

APIs have various terms of service governing the use of the API. This includes rate limits, caching and storage policies, copyright guidelines and display requirements. Make sure to follow the API terms of service to avoid service interruptions. For example, the OpenWeatherMap ToS is documented in the How to work with the API Key section.

Code goes in the Before every Fetch Business Logic.

Manipulating Date/Time Values

To easily manipulate dates in Business Logic's JavaScript environment, use the module moment.

function onPostFetch(request, response, modules){
  var moment = modules.moment();
  var expiration = response.body[0].trialExpiration;
  if (moment.add('days', 30).isBefore(expiration)) {
    response.continue();
  } else if (moment.add('days',30).isAfter(expiration) && moment.isBefore(expiration)) {
      var body = response.body[0];
      body.warning = "Your trial will expire within the next 30 days."
      response.body[0] = body;
      response.continue(); 
  }else {
      response.error("Trial Period Expired.  Access Denied.");
  }
}

For more information on the moment module, see the reference

Code goes in the After every Fetch Business Logic.

Executing Asynchronous Functions

The async module utilizes async.js to simplify parallel and asynchronous programming within the business logic context. This module provides convenience methods for asynchronous functional methods (map, reduce, filter, each, etc) as well as some common patterns for asynchronous control flow (parallel, series, queue, etc…). For example:

function onPostFetch(request, response, modules){
  var async = modules.async;
    var body = response.body;
  var moment = modules.moment().format();
  var logger = modules.logger.error;

  var iterator = function(item, callback) {
    item.fetchTime = moment;
    callback();
  };

  var callback = function (err) {
    if (err) {
        logger(err);
    } else {
      response.body = body;
    }
    response.continue();
  };

  async.each(body, iterator, callback)
}

This request uses async.js after fetch of a collection or query of a collection to add a fetchTime timestamp to each record in the collection. By using the async.js each method, iterations through the response body are processed in parallel, improving response time and ability to update larger recordsets in BL.

Code goes in the After every Fetch Business Logic.

Got a question?