data-controller.js

/**
 * MOST Web Framework
 * A JavaScript Web Framework
 * http://themost.io
 *
 * Copyright (c) 2014, Kyriakos Barbounakis k.barbounakis@gmail.com, Anthi Oikonomou anthioikonomou@gmail.com
 *
 * Released under the BSD3-Clause license
 * Date: 2014-06-09
 */
/**
 * @ignore
 */
var util = require('util'),
    mvc = require('./http-mvc'),
    xml = require('most-xml'),
    string = require('string'),
    common = require('./common');
/**
 * @classdesc HttpDataController class describes a common MOST Web Framework data controller.
 * This controller is inherited by default from all data models. It offers a set of basic actions for CRUD operations against data objects
 * and allows filtering, paging, sorting and grouping data objects with options similar to [OData]{@link http://www.odata.org/}.
 <h2>Basic Features</h2>
 <h3>Data Filtering ($filter query option)</h3>
 <p>Logical Operators</p>
 <p>The following table contains the logical operators supported in the query language:</p>
  <table class="table-flat">
    <thead><tr><th>Operator</th><th>Description</th><th>Example</th></tr></thead>
    <tbody>
        <tr><td>eq</td><td>Equal</td><td>/Order/index.json?$filter=customer eq 353</td></tr>
        <tr><td>ne</td><td>Not Equal</td><td>/Order/index.json?$filter=orderStatus/alternateName ne 'OrderDelivered'</td></tr>
        <tr><td>gt</td><td>Greater than</td><td>/Order/index.json?$filter=orderedItem/price gt 1000</td></tr>
        <tr><td>ge</td><td>Greater than or equal</td><td>/Order/index.json?$filter=orderedItem/price ge 500</td></tr>
        <tr><td>lt</td><td>Lower than</td><td>/Order/index.json?$filter=orderedItem/price lt 500</td></tr>
        <tr><td>le</td><td>Lower than or equal</td><td>/Order/index.json?$filter=orderedItem/price le 1000</td></tr>
        <tr><td>and</td><td>Logical and</td><td>/Order/index.json?$filter=orderedItem/price gt 1000 and orderStatus/alternateName eq 'OrderPickup'</td></tr>
        <tr><td>or</td><td>Logical or</td><td>/Order/index.json?$filter=orderStatus/alternateName eq 'OrderPickup' or orderStatus/alternateName eq 'OrderProcessing'</td></tr>
    </tbody>
 </table>
 <p>Arithmetic Operators</p>
 <p>The following table contains the arithmetic operators supported in the query language:</p>
 <table class="table-flat">
     <thead><tr><th>Operator</th><th>Description</th><th>Example</th></tr></thead>
     <tbody>
     <tr><td>add</td><td>Addition</td><td>/Order/index.json?$filter=(orderedItem/price add 10) gt 1560</td></tr>
     <tr><td>sub</td><td>Subtraction</td><td>/Order/index.json?$filter=(orderedItem/price sub 10) gt 1540</td></tr>
     <tr><td>mul</td><td>Multiplication</td><td>/Order/index.json?$filter=(orderedItem/price mul 1.20) gt 1000</td></tr>
     <tr><td>div</td><td>Division</td><td>/Order/index.json?$filter=(orderedItem/price div 2) le 500</td></tr>
     <tr><td>mod</td><td>Modulo</td><td>/Order/index.json?$filter=(orderedItem/price mod 2) eq 0</td></tr>
     </tbody>
 </table>
 <p>Functions</p>
 <p>A set of functions are also defined for use in $filter query option:</p>
 <table class="table-flat">
     <thead><tr><th>Function</th><th>Example</th></tr></thead>
     <tbody>
        <tr><td colspan="2"><b>String Functions</b></td></tr>
        <tr><td>startswith(field,string)</td><td>/Product/index.json?$filter=startswith(name,'Apple') eq true</td></tr>
        <tr><td>endswith(field,string)</td><td>/Product/index.json?$filter=endswith(name,'Workstation') eq true</td></tr>
        <tr><td>contains(field,string)</td><td>/Product/index.json?$filter=contains(name,'MacBook') eq true</td></tr>
         <tr><td>length(field)</td><td>/Product/index.json?$filter=length(name) gt 40</td></tr>
         <tr><td>indexof(field,string)</td><td>/Product/index.json?$filter=indexof(name,'Air') gt 1</td></tr>
         <tr><td>substring(field,number)</td><td>/Product/index.json?$filter=substring(category,1) eq 'aptops'</td></tr>
         <tr><td>substring(field,number,number)</td><td>/Product/index.json?$filter=substring(category,1,2) eq 'ap'</td></tr>
         <tr><td>tolower(field)</td><td>/Product/index.json?$filter=tolower(category) eq 'laptops'</td></tr>
         <tr><td>toupper(field)</td><td>/Product/index.json?$filter=toupper(category) eq 'LAPTOPS'</td></tr>
         <tr><td>trim(field)</td><td>/Product/index.json?$filter=trim(category) eq 'Laptops'</td></tr>
 <tr><td colspan="2"><b>Date Functions</b></td></tr>
 <tr><td>day(field)</td><td>/Order/index.json?$filter=day(orderDate) eq 4</td></tr>
 <tr><td>month(field)</td><td>/Order/index.json?$filter=month(orderDate) eq 6</td></tr>
 <tr><td>year(field)</td><td>/Order/index.json?$filter=year(orderDate) ge 2014</td></tr>
 <tr><td>hour(field)</td><td>/Order/index.json?$filter=hour(orderDate) ge 12 and hour(orderDate) lt 14</td></tr>
 <tr><td>minute(field)</td><td>/Order/index.json?$filter=minute(orderDate) gt 15 and minute(orderDate) le 30</td></tr>
 <tr><td>second(field)</td><td>/Order/index.json?$filter=second(orderDate) ge 0 and second(orderDate) le 45</td></tr>
 <tr><td>date(field)</td><td>/Order/index.json?$filter=date(orderDate) eq '2015-03-20'</td></tr>
 <tr><td colspan="2"><b>Math Functions</b></td></tr>
 <tr><td>round(field)</td><td>/Product/index.json?$filter=round(price) le 389</td></tr>
 <tr><td>floor(field)</td><td>/Product/index.json?$filter=floor(price) eq 389</td></tr>
 <tr><td>ceiling(field)</td><td>/Product/index.json?$filter=ceiling(price) eq 390</td></tr>
      </tbody>
 </table>
 <h3>Attribute Selection ($select query option)</h3>
 <p>The following table contains attribute selection expressions supported in the query language:</p>
 <table class="table-flat">
     <thead><tr><th>Description</th><th>Example</th></tr></thead>
     <tbody>
     <tr><td>Select attribute</td><td>/Order/index.json?$select=id,customer,orderStatus</td></tr>
     <tr><td>Select attribute with alias</td><td>/Order/index.json?$select=id,customer/description as customerName,orderStatus/name as orderStatusName</td></tr>
     <tr><td>Select attribute with aggregation</td><td>/Order/index.json?$select=count(id) as totalCount&$filter=orderStatus/alternateName eq 'OrderProcessing'</td></tr>
     <tr><td>&nbsp;</td><td>/Product/index.json?$select=max(price) as maxPrice&$filter=category eq 'Laptops'</td></tr>
     <tr><td>&nbsp;</td><td>/Product/index.json?$select=min(price) as minPrice&$filter=category eq 'Laptops'</td></tr>
 </tbody>
 </table>
 <h3>Data Sorting ($orderby or $order query options)</h3>
 <table class="table-flat">
     <thead><tr><th>Description</th><th>Example</th></tr></thead>
     <tbody>
        <tr><td>Ascending order</td><td>/Product/index.json?$orderby=name</td></tr>
        <tr><td>Descending order</td><td>/Product/index.json?$orderby=category desc,name desc</td></tr>
     </tbody>
 </table>
  <h3>Data Paging ($top, $skip and $inlinecount query options)</h3>
 <p>The $top query option allows developers to apply paging in the result-set by giving the max number of records for each page. The default value is 25.
 The $skip query option provides a way to skip a number of records. The default value is 0.
 The $inlinecount query option includes in the result-set the total number of records of the query expression provided:
 <pre class="prettyprint"><code>
 {
     "total": 94,
     "records": [ ... ]
 }
  </code></pre>
 <p>The default value is false.</p>
  </p>
   <table class="table-flat">
      <thead><tr><th>Description</th><th>Example</th></tr></thead>
      <tbody>
      <tr><td>Limit records</td><td>/Product/index.json?$top=5</td></tr>
      <tr><td>Skip records</td><td>/Product/index.json?$top=5&$skip=5</td></tr>
      <tr><td>Paged records</td><td>/Product/index.json?$top=5&$skip=5&$inlinecount=true</td></tr>
      </tbody>
  </table>
  <h3>Data Grouping ($groupby or $group query options)</h3>
  <p>The $groupby query option allows developers to group the result-set by one or more attributes</p>
  <table class="table-flat">
  <thead><tr><th>Description</th><th>Example</th></tr></thead>
  <tbody>
  <tr><td>group</td><td>/Product/index.json?$select=count(id) as totalCount,category&$groupby=category</td></tr>
  <tr><td>group and sort</td><td>/Product/index.json?$select=count(id) as totalCount,category&$groupby=category&$orderby=count(id) desc</td></tr>
  </tbody>
  </table>
 <h3>Data Expanding ($expand)</h3>
 <p>The $expand query option forces response to include associated objects which are not marked as expandable by default.</p>
 <table class="table-flat">
     <thead><tr><th>Description</th><th>Example</th></tr></thead>
     <tbody>
     <tr><td>expand</td><td>/Order/index.json?$filter=orderStatus/alternateName eq 'OrderProcessing'&$expand=customer</td></tr>
     </tbody>
 </table>
 <p>The $expand option is optional for a <a href="https://docs.themost.io/most-data/DataField.html">DataField</a> marked as expandable.</p>
 * @class
 * @constructor
 * @augments HttpController
 * @property {DataModel} model - Gets or sets the current data model.
 * @memberOf module:most-web.controllers
 */
function HttpDataController()
{
    var model_;
    var self = this;
    Object.defineProperty(this, 'model', {
        get: function() {
            if (model_)
                return model_;
            model_ = self.context.model(self.name);
            return model_;
        },
        set: function(value) {
            model_ = value;
        }, configurable:false, enumerable:false
    });
}
util.inherits(HttpDataController, mvc.HttpController);

/**
 * Handles data object creation (e.g. /user/1/new.html, /user/1/new.json etc)
 * @param {Function} callback
 */
HttpDataController.prototype.new = function (callback) {
    try {
        var self = this,
            context = self.context;
        context.handle(['GET'],function() {
            callback(null, self.result());
        }).handle(['POST', 'PUT'],function() {
            var target = self.model.convert(context.params[self.model.name] || context.params.data, true);
            self.model.save(target, function(err)
            {
                if (err) {
                    callback(common.httpError(err));
                }
                else {
                    if (context.params.attr('returnUrl'))
                        callback(null, context.params.attr('returnUrl'));
                    callback(null, self.result(target));
                }
            });
        }).unhandle(function() {
            callback(new common.HttpMethodNotAllowed());
        });
    }
    catch (e) {
        callback(common.httpError(e));
    }
};
/**
 * Handles data object edit (e.g. /user/1/edit.html, /user/1/edit.json etc)
 * @param {Function} callback
 */
HttpDataController.prototype.edit = function (callback) {
    try {
        var self = this,
            context = self.context;
        context.handle(['POST', 'PUT'], function() {
            //get context param
            var target = self.model.convert(context.params[self.model.name] || context.params.data, true);
            if (target) {
                self.model.save(target, function(err)
                {
                    if (err) {
                        console.log(err);
                        console.log(err.stack);
                        callback(common.httpError(err));
                    }
                    else {
                        if (context.params.attr('returnUrl'))
                            callback(null, context.params.attr('returnUrl'));
                        callback(null, self.result(target));
                    }
                });
            }
            else {
                callback(new common.HttpBadRequest());
            }
        }).handle('DELETE', function() {
            //get context param
            var target = context.params[self.model.name] || context.params.data;
            if (target) {
                //todo::check if object exists
                self.model.remove(target, function(err)
                {
                    if (err) {
                        callback(common.httpError(err));
                    }
                    else {
                        if (context.params.attr('returnUrl'))
                            callback(null, context.params.attr('returnUrl'));
                        callback(null, self.result(null));
                    }
                });
            }
            else {
                callback(new common.HttpBadRequest());
            }
        }).handle('GET', function() {
            if (context.request.route) {
                if (context.request.route.static) {
                    callback(null, self.result());
                    return;
                }
            }
            //get context param (id)
            var filter = null, id = context.params.attr('id');
            if (id) {
                //create the equivalent open data filter
                filter = util.format('%s eq %s',self.model.primaryKey,id);
            }
            else {
                //get the requested open data filter
                filter = context.params.attr('$filter');
            }
            if (filter) {
                self.model.filter(filter, function(err, q) {
                    if (err) {
                        callback(common.httpError(err));
                        return;
                    }
                    q.take(1, function (err, result) {
                        try {
                            if (err) {
                                callback(err);
                            }
                            else {
                                if (result.length>0)
                                    callback(null, self.result(result));
                                else
                                    callback(null, self.result(null));
                            }
                        }
                        catch (e) {
                            callback(common.httpError(e));
                        }
                    });
                });
            }
            else {
                callback(new common.HttpBadRequest());
            }

        }).unhandle(function() {
            callback(new common.HttpMethodNotAllowed());
        });

    }
    catch (e) {
        callback(common.httpError(e));
    }

};

HttpDataController.prototype.schema = function (callback) {
    var self = this, context = self.context;
    context.handle('GET', function() {
        if (self.model) {
            //prepare client model
            var clone = JSON.parse(JSON.stringify(self.model));
            var m = util._extend({}, clone);
            //delete private properties
            var keys = Object.keys(m);
            for (var i = 0; i < keys.length; i++) {
                var key = keys[i];
               if (key.indexOf("_")==0)
                   delete m[key];
            }
            //delete other server properties
            delete m.view;
            delete m.source;
            delete m.fields;
            delete m.privileges;
            delete m.constraints;
            delete m.eventListeners;
            //set fields equal attributes
            m.attributes = JSON.parse(JSON.stringify(self.model.attributes));
            m.attributes.forEach(function(x) {
                var mapping = self.model.inferMapping(x.name);
                if (mapping)
                    x.mapping = JSON.parse(JSON.stringify(mapping));;
                //delete private properties
                delete x.value;
                delete x.calculation;
            });
            //prepape views and view fields
            if (m.views) {
                m.views.forEach(function(view) {
                    if (view.fields) {
                        view.fields.forEach(function(field) {
                            if (/\./.test(field.name)==false) {
                                //extend view field
                                var name = field.name;
                                var mField = m.attributes.filter(function(y) {
                                    return (y.name==name);
                                })[0];
                                if (mField) {
                                    for (var key in mField) {
                                        if (mField.hasOwnProperty(key) && !field.hasOwnProperty(key)) {
                                            field[key] = mField[key];
                                        }
                                    }
                                }
                            }
                        });
                    }
                });
            }
            callback(null, self.result(m));
        }
        else {
            callback(new common.HttpNotFoundException());
        }

    }).unhandle(function() {
        callback(new common.HttpMethodNotAllowed());
    });
}

/**
 * Handles data object display (e.g. /user/1/show.html, /user/1/show.json etc)
 * @param {Function} callback
 */
HttpDataController.prototype.show = function (callback) {
    try {
        var self = this, context = self.context;
        context.handle('GET', function() {
            if (context.request.route) {
                if (context.request.route.static) {
                    callback(null, self.result());
                    return;
                }
            }
            var filter = null, id = context.params.attr('id');
            if (id) {
                //create the equivalent open data filter
                filter = util.format('%s eq %s',self.model.primaryKey,id);
            }
            else {
                //get the requested open data filter
                filter = context.params.attr('$filter');
            }
            self.model.filter(filter, function(err, q) {
                if (err) {
                    callback(common.httpError(err));
                    return;
                }
                q.take(1, function (err, result) {
                    try {
                        if (err) {
                            callback(common.httpError(e));
                        }
                        else {
                            if (result.length>0)
                                callback(null, self.result(result));
                            else
                                callback(new common.HttpNotFoundException('Item Not Found'));
                        }
                    }
                    catch (e) {
                        callback(common.httpError(e));
                    }
                });
            });
        }).unhandle(function() {
            callback(new common.HttpMethodNotAllowed());
        });
    }
    catch (e) {
        callback(e);
    }
}
/**
 * Handles data object deletion (e.g. /user/1/remove.html, /user/1/remove.json etc)
 * @param {Function} callback
 */
HttpDataController.prototype.remove = function (callback) {
    try {
        var self = this, context = self.context;
        context.handle(['POST','DELETE'], function() {
            var target = context.params[self.model.name] || context.params.data;
            if (target) {
                self.model.remove(target, function(err)
                {
                    if (err) {
                        callback(common.httpError(err));
                    }
                    else {
                        if (context.params.attr('returnUrl'))
                            callback(null, context.params.attr('returnUrl'));
                        callback(null, self.result(target));
                    }
                });
            }
            else {
                callback(new common.HttpBadRequest());
            }
        }).unhandle(function() {
            callback(new common.HttpMethodNotAllowed());
        });
    }
    catch (e) {
        callback(common.httpError(e))
    }
}

/**
 * @param {Function(Error,DataQueryable)} callback
 * @private
 */
HttpDataController.prototype.filter = function (callback) {

    var self = this, params = self.context.params;

    if (typeof self.model !== 'object' || self.model == null) {
        callback(new Error('Model is of the wrong type or undefined.'));
        return;
    }

    var filter = params['$filter'],
        select = params['$select'],
        skip = params['$skip'] || 0,
        orderBy = params['$orderby'] || params.attr('$order'),
        groupBy = params.attr('$group') || params.attr('$groupby'),
        expand = params.attr('$expand');

    self.model.filter(filter,
        /**
         * @param {Error} err
         * @param {DataQueryable} q
         */
         function (err, q) {
            try {
                if (err) {
                    callback(err);
                }
                else {
                    //set $groupby
                    if (groupBy) {
                        var arr = groupBy.split(',');
                        var fields = [];
                        for (var i = 0; i < arr.length; i++) {
                            var item = string(arr[i]).trim().toString();
                            var field = self.model.field(item);
                            if (field) {
                                fields.push(field.name);
                            }
                            else if (/(\w+)\((.*?)\)/i.test(item)) {
                                fields.push(q.fieldOf(item));
                            }
                            else if (/\//.test(item)) {
                                fields.push(item);
                            }
                        }
                        if (fields.length>0) {
                            q.groupBy(fields);
                        }
                    }
                    //set $select
                    if (select) {
                        var arr = select.split(',');
                        var fields = [];
                        for (var i = 0; i < arr.length; i++) {
                            var item = string(arr[i]).trim().toString();
                            var field = self.model.field(item);
                            if (field) {
                                fields.push(field.name);
                            }
                            else if (/(\w+)\((.*?)\)/i.test(item) || /^(\w+)\s+as\s+(.*?)$/i.test(item)) {
                                fields.push(q.fieldOf(item));
                            }
                            else if (/\//.test(item)) {
                                //pass nested field as string
                                fields.push(item);
                            }
                        }
                        if (fields.length>0) {
                            q.select(fields);
                        }
                        else {
                            //search for data view
                            if (arr.length==1) {
                                var view = self.model.dataviews(arr[0]);
                                if (view) {
                                    q.select(view.name);
                                }
                            }
                        }
                    }
                    //set $skip
                    q.skip(skip);
                    //set $orderby
                    if (orderBy) {
                        var arr = orderBy.split(',');
                        for (var i = 0; i < arr.length; i++) {
                            var item = string(arr[i]).trim().toString(), name = null, direction = 'asc';
                            if (/ asc$/i.test(item)) {
                                name=item.substr(0,item.length-4);
                            }
                            else if (/ desc$/i.test(item)) {
                                direction = 'desc';
                                name=item.substr(0,item.length-5);
                            }
                            else if (!/\s/.test(item)) {
                                name = item;
                            }
                            if (name) {
                                var field = self.model.field(name);
                                //validate model field
                                if (field) {
                                    if (direction=='desc')
                                        q.orderByDescending(name);
                                    else
                                        q.orderBy(name);
                                }
                                //validate aggregate functions or associated field expression e.g. user/username
                                else if (/(\w+)\((.*?)\)/i.test(name) || /\//.test(name)) {
                                    if (direction=='desc')
                                        q.orderByDescending(name);
                                    else
                                        q.orderBy(name);
                                }
                                else if (/\//.test(name)) {
                                    if (direction=='desc')
                                        q.orderByDescending(name);
                                    else
                                        q.orderBy(name);
                                }

                            }
                        }
                    }
                    if (expand) {
                        if (expand.length>0) {
                            expand.split(',').map(function(x) { return x.replace(/\s/g,''); }).forEach(function(x) {
                                if (x.length)
                                    q.expand(x.replace(/\s/g,''));
                            });
                        }
                    }
                    //return
                    callback(null, q);
                }
            }
            catch (e) {
               callback(e);
            }
        });
};
/**
 *
 * @param {Function} callback
 */
HttpDataController.prototype.index = function(callback)
{

    try {
        var self = this, context = self.context,
            top = parseInt(self.context.params.$top),
            take = top > 0 ? top : (top == -1 ? top : 25);
        var count = /^true$/ig.test(context.params.attr('$inlinecount')) || false,
            expand = context.params.attr('$expand'),
            first = /^true$/ig.test(context.params.attr('$first')) || false,
            asArray = /^true$/ig.test(context.params.attr('$array')) || false;
        common.debug(context.request.url);
        context.handle('GET', function() {
            if (context.request.route) {
                if (context.request.route.static) {
                    callback(null, self.result([]));
                    return;
                }
            }
            self.filter(function(err, q) {
                try {
                    if (err) {
                        callback(common.httpError(err));
                    }
                    else {

                        if (expand) {
                            if (expand.length>0) {
                                var arr = expand.split(',');
                                arr.forEach(function(x) {
                                    q.expand(x.replace(/\s/g,''));
                                });
                            }
                        }
                        //check $first context param
                        if (first) {
                            q.first(function(err, result) {
                                if (err) {
                                    callback(common.httpError(err));
                                }
                                else {
                                    callback(null, self.result(result));
                                }
                            });
                            return;
                        }

                        var q1 = null;
                        if (count) {
                            q1 = q.clone();
                        }
                        //pass as array option
                        q.asArray(asArray);
                        if (take<0) {
                            q.all(function(err, result)
                            {
                                if (err) {
                                    callback(common.httpError(err));
                                    return;
                                }
                                if (count) {
                                    result = { records: (result || []) };
                                    result.total = result.records.length;
                                    callback(null, self.result(result));
                                }
                                else {
                                    callback(null, self.result(result || []));
                                }
                            });
                        }
                        else {
                            q.take(take, function(err, result)
                            {
                                if (err) {
                                    callback(common.httpError(err));
                                    return;
                                }
                                if (count) {
                                    q1.count(function(err, total) {
                                        if (err) {
                                            callback(common.httpError(err));
                                        }
                                        else {
                                            result = { total: total, records: (result || []) };
                                            callback(null, self.result(result));
                                        }
                                    });
                                }
                                else {
                                    callback(null, self.result(result || []));
                                }
                            });
                        }

                    }
                }
                catch (e) {
                    callback(e);
                }
            });
        }).handle(['POST', 'PUT'], function() {
            var target;
            try {
                target = self.model.convert(context.params[self.model.name] || context.params.data, true);
            }
            catch(err) {
                common.log(err);
                var er = new common.HttpException(422, "An error occured while converting data objects.", err.message);
                er.code = 'EDATA';
                return callback(er);
            }
            if (target) {
                self.model.save(target, function(err)
                {
                    if (err) {
                        common.log(err);
                        callback(common.httpError(err));
                    }
                    else {
                        callback(null, self.result(target));
                    }
                });
            }
            else {
                return callback(new common.HttpBadRequest());
            }
        }).handle('DELETE', function() {
            //get data
            var target;
            try {
                target = self.model.convert(context.params[self.model.name] || context.params.data, true);
            }
            catch(err) {
                common.log(err);
                var er = new common.HttpException(422, "An error occured while converting data objects.", err.message);
                er.code = 'EDATA';
                return callback(er);
            }
            if (target) {
                self.model.remove(target, function(err)
                {
                    if (err) {
                        callback(common.httpError(err));
                    }
                    else {
                        callback(null, self.result(target));
                    }
                });
            }
            else {
                return callback(new common.HttpBadRequest());
            }
        }).unhandle(function() {
            return callback(new common.HttpMethodNotAllowed());
        });
    }
    catch (e) {
        callback(common.httpError(e));
    }
};
/**
 * Returns an instance of HttpResult class which contains a collection of items based on the specified association.
 * This association should be a one-to-many association or many-many association.
 * A routing for this action may be:
 <pre class="prettyprint"><code>
 { "url":"/:controller/:parent/:model/index.json", "mime":"application/json", "action":"association" }
 </code></pre>
 <p>
 or
 </p>
 <pre class="prettyprint"><code>
 { "url":"/:controller/:parent/:model/index.html", "mime":"text/html", "action":"association" }
 </code></pre>
  <pre class="prettyprint"><code>
 //get orders in JSON format
 /GET /Party/353/Order/index.json
 </code></pre>
 <p>
 This action supports common query options like $filter, $order, $top, $skip etc.
 The result will be a result-set with associated items:
 </p>
 <pre class="prettyprint"><code>
    //JSON Results:
 {
        "total": 8,
        "skip": 0,
        "records": [
            {
            "id": 37,
            "customer": 353,
            "orderDate": "2015-05-05 01:19:34.000+03:00",
            "orderedItem": {
                "id": 407,
                "additionalType": "Product",
                "category": "PC Components",
                "price": 1625.49,
                "model": "HR5845",
                "releaseDate": "2015-09-20 03:35:33.000+03:00",
                "name": "Nvidia GeForce GTX 650 Ti Boost",
                "dateCreated": "2015-11-23 14:53:04.884+02:00",
                "dateModified": "2015-11-23 14:53:04.887+02:00"
            },
            "orderNumber": "OFV804",
            "orderStatus": {
                "id": 1,
                "name": "Delivered",
                "alternateName": "OrderDelivered",
                "description": "Representing the successful delivery of an order."
            },
            "paymentDue": "2015-05-25 01:19:34.000+03:00",
            "paymentMethod": {
                "id": 6,
                "name": "Direct Debit",
                "alternateName": "DirectDebit",
                "description": "Payment by direct debit"
            },
            "additionalType": "Order",
            "dateCreated": "2015-11-23 21:00:18.264+02:00",
            "dateModified": "2015-11-23 21:00:18.266+02:00"
            }
        ...]
   ...
}
</code></pre>
 * @param {Function} callback - A callback function where the first argument will contain the Error object if an error occured, or null otherwise.
 */
HttpDataController.prototype.association = function(callback) {
    try {
        var self = this, parent = self.context.params.parent, model = self.context.params.model;
        if (common.isNullOrUndefined(parent) || common.isNullOrUndefined(model)) {
            callback(new common.HttpBadRequest());
            return;
        }
        self.model.where(self.model.primaryKey).equal(parent).select([self.model.primaryKey]).first(function(err, result) {
            if (err) {
                common.log(err);
                callback(new common.HttpServerError());
                return;
            }
            if (common.isNullOrUndefined(result)) {
                callback(new common.HttpNotFoundException());
                return;
            }
            //get parent object (DataObject)
            var obj = self.model.convert(result);
            var associatedModel = self.context.model(model);
            if (common.isNullOrUndefined(associatedModel)) {
                callback(new common.HttpNotFoundException());
                return;
            }
            /**
             * Search for object junction
             */
            var field = self.model.attributes.filter(function(x) { return x.type === associatedModel.name; })[0], mapping;
            if (field) {
                /**
                 * Get association mapping fo this field
                 * @type {DataAssociationMapping}
                 */
                mapping = self.model.inferMapping(field.name);
                if (mapping) {
                    if ((mapping.parentModel===self.model.name) && (mapping.associationType==='junction')) {
                        /**
                         * @type {DataQueryable}
                         */
                        var junction = obj.property(field.name);
                        junction.model.filter(self.context.params, function(err, q) {
                            if (err) {
                                callback(err);
                            }
                            else {
                                //merge properties
                                if (q.query.$select) { junction.query.$select = q.query.$select; }
                                if (q.query.$group) { junction.query.$group = q.query.$group; }
                                if (q.query.$order) { junction.query.$order = q.query.$order; }
                                if (q.query.$prepared) { junction.query.$where = q.query.$prepared; }
                                if (q.query.$skip) { junction.query.$skip = q.query.$skip; }
                                if (q.query.$take) { junction.query.$take = q.query.$take; }
                                junction.list(function(err, result) {
                                    callback(err, self.result(result));
                                });
                            }
                        });
                        return;
                    }
                }
            }
            field = associatedModel.attributes.filter(function(x) { return x.type === self.model.name; })[0];
            if (common.isNullOrUndefined(field)) {
                callback(new common.HttpNotFoundException());
                return;
            }
            //get field mapping
            mapping = associatedModel.inferMapping(field.name);
            associatedModel.filter(self.context.params, function(err, q) {
                if (err) {
                    callback(err);
                }
                else {
                    q.where(mapping.childField).equal(parent).list(function(err, result) {
                        callback(err, self.result(result));
                    });
                }
            });
        });
    }
    catch(e) {
        common.log(e);
        callback(e, new common.HttpServerError());
    }
};

if (typeof module !== 'undefined') module.exports = HttpDataController;