lib/dispatch/dimensions.js

Summary

Maintainability
F
1 wk
Test Coverage
/**
 *  @title joola/lib/dispatch/dimensions
 *  @overview Provides dimension management as part of user management.
 *  @description
 *  joola uses the concept of `dimension` to bind users together into a groups. dimensions are logical business
 *  entities and provide the developer/user with the ability to manage permissions and filters, based not only on users
 *  and roles, but also on an dimension level.
 *
 * - [list](#list)
 * - [get](#get)
 * - [add](#add)
 * - [update](#update)
 * - [delete](#delete)
 *
 *  @copyright (c) Joola Smart Solutions, Ltd. <info@joo.la>
 *  @license GPL-3.0+ <http://spdx.org/licenses/GPL-3.0+>. Some rights reserved. See LICENSE, AUTHORS
 **/

'use strict';

var
  joola = require('../joola'),
  _ = require('underscore'),
  async = require('async'),
  router = require('../webserver/routes/index'),
  Proto = require('./prototypes/dimension');

/**
 * @function list
 * @param {Function} [callback] called following execution with errors and results.
 * Lists all defined dimensions:
 *
 * The function calls on completion an optional `callback` with:
 * - `err` if occured, an error object, else null.
 * - `result` contains the list of configured dimensions.
 *
 * Configuration elements participating: `config:dimensions`.
 *
 * Events raised via `dispatch`: `dimensions:list-request`, `dimensions:list-done`
 *
 * ```js
 * joola.dispatch.dimensions.list(function(err, result) {
 *   console.log(err, result);
 * }
 * ```
 */
exports.list = {
  name: "/dimensions/list",
  description: "I list all available dimensions",
  inputs: {required: ['workspace'], optional: ['collection']},
  _proto: Proto.proto,
  _outputExample: {},
  _permission: ['dimensions:list'],
  _dispatch: {
    message: 'dimensions:list'
  },
  run: function (context, workspace, collection, callback) {
    if (typeof collection === 'function') {
      callback = collection;
      collection = null;
    }

    callback = callback || function () {
      };

    var ensureRole = function (dimensions) {
      var clean = [];
      dimensions.forEach(function (d) {
        var hasRole = false;
        if (joola.config.get('authentication:requireroles') === false)
          hasRole = ['anonymous'];
        else
          hasRole = _.intersection(context.user.roles, d.roles);
        if (hasRole.length > 0)
          clean.push(d);
      });
      return clean;
    };


    if (context.user.workspace !== workspace && context.user.permissions.indexOf('superuser') === -1) {
      var err = new Error('Forbidden');
      err.code = 403;
      return callback(err);
    }

    if (collection) {
      joola.dispatch.collections.get(context, workspace, collection, function (err, _collection) {
        if (err)
          return callback(err);

        return callback(null, ensureRole(joola.common.obj2array(_collection.dimensions)));
      });
    }
    else {
      joola.dispatch.collections.list(context, workspace, function (err, collections) {
        if (err)
          return callback(err);

        var calls = [];
        var dimensions = [];
        collections.forEach(function (collection) {
          var call = function (cb) {
            joola.dispatch.collections.get(context, workspace, collection.key, function (err, _collection) {
              if (err)
                return cb(err);

              _collection.dimensions.forEach(function (m, i) {
                _collection.dimensions[i].collection = _collection.key;
              });
              return cb(null, _collection.dimensions);
            });
          };
          calls.push(call);
        });
        async.parallel(calls, function (err, results) {
          if (err)
            return callback(err);

          results.forEach(function (result) {
            dimensions = dimensions.concat(result);
          });
          return callback(null, ensureRole(dimensions));
        });
      });
    }
  }
};

/**
 * @function get
 * @param {string} name holds the name of the dimension to get.
 * @param {Function} [callback] called following execution with errors and results.
 * Gets a specific user by username:
 * - `name` of the dimension
 *
 * The function calls on completion an optional `callback` with:
 * - `err` if occured, an error object, else null.
 * - `result` contains the requested username.
 *
 * Configuration elements participating: `config:dimensions`.
 *
 * Events raised via `dispatch`: `dimensions:get-request`, `dimensions:get-done`
 *
 * ```js
 * joola.dispatch.dimensions.get('test-dimension', function(err, result) {
 *   console.log(err, result);
 * }
 * ```
 */
exports.get = {
  name: "/dimensions/get",
  description: "I get a specific dimension by key`",
  inputs: ['workspace', 'collection', 'key'],
  _proto: Proto.proto,
  _outputExample: {},
  _permission: ['dimensions:get'],
  _dispatch: {
    message: 'dimensions:get'
  },
  run: function (context, workspace, collection, key, callback) {
    callback = callback || function () {
      };

    if (context.user.workspace !== workspace && context.user.permissions.indexOf('superuser') === -1) {
      var err = new Error('Forbidden');
      err.code = 403;
      return callback(err);
    }

    joola.dispatch.collections.get(context, workspace, collection, function (err, _collection) {
      if (err)
        return callback(err);

      var result = _.filter(_collection.dimensions, function (item) {
        return item.key === key;
      });

      if (!result || typeof result === 'undefined' || result === null || result.length === 0)
        return callback(new Error('dimension [' + key + '] does not exist.'));

      result = result[0];
      var hasRole = false;
      if (joola.config.get('authentication:requireroles') === false)
        hasRole = ['anonymous'];
      else
        hasRole = _.intersection(context.user.roles, result.roles);
      if (hasRole.length > 0)
        return callback(null, result);
      else {
        var directaccesserr = new Error('Forbidden');
        directaccesserr.code = 403;
        return callback(directaccesserr );
      }
    });
  }
};

/**
 * @function add
 * @param {Object} options describes the new dimension
 * @param {Function} [callback] called following execution with errors and results.
 * Adds a new data source described in `options`:
 * - `name` of the new dimension
 * - `filter` to apply on dimension members.
 *
 * The function calls on completion an optional `callback` with:
 * - `err` if occured, an error object, else null.
 * - `result` contains the newly added dimension.
 *
 * Configuration elements participating: `config:dimensions`.
 *
 * Events raised via `dispatch`: `dimensions:add-request`, `dimensions:add-done`
 *
 * ```js
 * var options = {
 *   name: 'test-dimension',
 *   filter: 'Country=France'
 * };
 *
 * joola.dispatch.dimensions.add(options, function(err, result) {
 *   console.log(err, result);
 * }
 * ```
 */
exports.add = {
  name: "/dimensions/add",
  description: "I add a new dimension",
  inputs: ['workspace', 'collection', 'dimension'],
  _outputExample: {},
  _permission: ['dimensions:add'],
  _dispatch: {
    message: 'dimensions:add'
  },
  run: function (context, workspace, collection, dimension, callback) {
    callback = callback || function () {
      };

    if (context.user.workspace !== workspace && context.user.permissions.indexOf('superuser') === -1) {
      var err = new Error('Forbidden');
      err.code = 403;
      return callback(err);
    }

    dimension.type = 'dimension';
    try {
      dimension = new Proto(dimension);
    } catch (ex) {
      return callback(ex);
    }

    exports.get.run(context, workspace, collection, dimension.key, function (err, _dimension) {
      if (!err) {
        return callback(new Error('dimension [' + dimension.key + '] already exists: ' + err));
      }

      joola.config.set('workspaces:' + workspace + ':collections:' + collection + ':' + dimension.key, dimension, function (err) {
        /* istanbul ignore if */
        if (err)
          return callback(err);

        return callback(null, dimension);
      });
    });
  }
};

/**
 * @function update
 * @param {Object} options describes the dimension to update and the new details
 * @param {Function} [callback] called following execution with errors and results.
 * Updates an existing user described in `options`:
 * - `name` name of the dimension (cannot be updated).
 * - `_filter` filter to be applied on dimension's members.
 *
 * The function calls on completion an optional `callback` with:
 * - `err` if occured, an error object, else null.
 * - `result` contains the updated dimension.
 *
 * Configuration elements participating: `config:dimensions`.
 *
 * Events raised via `dispatch`: `dimensions:update-request`, `dimensions:update-done`
 *
 * ```js
 * var options = {
 *   name: 'test-dimension',
 *   _filter: 'Country=France'
 * };
 *
 * joola.dispatch.dimensions.update(options, function(err, result) {
 *   console.log(err, result);
 * }
 * ```
 */
exports.patch = {
  name: "/dimensions/patch",
  description: "I patch an existing dimension",
  inputs: ['workspace', 'collection', 'dimension', 'payload'],
  _outputExample: {},
  _permission: ['dimensions:patch'],
  _dispatch: {
    message: 'dimensions:patch'
  },
  run: function (context, workspace, collection, dimension, payload, callback) {
    callback = callback || function () {
      };

    if (context.user.workspace !== workspace && context.user.permissions.indexOf('superuser') === -1) {
      var err = new Error('Forbidden');
      err.code = 403;
      return callback(err);
    }

    exports.get.run(context, workspace, collection, dimension, function (err, value) {
      if (err)
        return callback(err);

      var _dimension = null;
      try {
        _dimension = joola.common.extend(value, payload);
        _dimension = new Proto(_dimension);
      } catch (ex) {
        return callback(ex);
      }

      joola.config.set('workspaces:' + workspace + ':collections:' + collection + ':' + dimension, _dimension, function (err) {
        /* istanbul ignore if */
        if (err)
          return callback(err);

        return callback(err, _dimension);
      });
    });
  }
};

/**
 * @function delete
 * @param {Object} options describes the dimension to delete
 * @param {Function} [callback] called following execution with errors and results.
 * Deletes an dimension described in `options`:
 * - `name` of the dimension to delete
 *
 * The function calls on completion an optional `callback` with:
 * - `err` if occured, an error object, else null.
 * - `result` contains the deleted dimension.
 *
 * Configuration elements participating: `config:dimensions`.
 *
 * Events raised via `dispatch`: `dimensions:delete-request`, `dimensions:delete-done`
 *
 * ```js
 * var options = {
 *   name: 'test-dimension'
 * };
 *
 * joola.dispatch.dimensions.delete(options, function(err, result) {
 *   console.log(err, result);
 * }
 * ```
 */
exports.delete = {
  name: "/dimensions/delete",
  description: "I delete an existing dimension",
  inputs: ['workspace', 'collection', 'dimension'],
  _outputExample: {},
  _permission: ['dimensions:delete'],
  _dispatch: {
    message: 'dimensions:delete'
  },
  run: function (context, workspace, collection, dimension, callback) {
    callback = callback || function () {
      };

    if (context.user.workspace !== workspace && context.user.permissions.indexOf('superuser') === -1) {
      var err = new Error('Forbidden');
      err.code = 403;
      return callback(err);
    }

    exports.get.run(context, workspace, collection, dimension, function (err, value) {
      if (err)
        return callback(err);

      joola.config.clear('workspaces:' + workspace + ':collections:' + collection + ':' + dimension, function (err) {
        /* istanbul ignore if */
        if (err)
          return callback(err);

        return callback(null, {});
      });
    });
  }
};