CategoryJavaScript

My take on a Vending Machine Simulation in JavaScript

How to convert indented text to a collapsible list

In my article about How to setup a Multi Page Application in AngularJS, I wanted to show a directory structure. I thought I would wrap it into a PRE element, to easily glance at the containment hierarchy. I started by jotting down only folders and files that I needed to talk about, hoping to simplify my readers task to go through them. But soon I realized that that structure was exactly what I was trying to describe in the first place and it didn’t make much sense to intentionally leave out other meaningful folders and files, just because it would be a pain to read. So I understood I had to find a way to make folders toggle on click. That would allow me to show only a portion by default, and allow the reader to freely move around in the hierarchy.

I looked in the Internet for a ready made solution with these features:

  1. Input: indented text.
  2. Output: collapsible list.
  3. Option to open some folders by default.
  4. Easily injectable into a WordPress post.

I couldn’t find anything that suited my needs, so I embarked into building something myself.

First I found an ingenuous article about making a Pure CSS collapsible tree menu. But if a way to expand collapse unordered lists is just a couple of lines in jQuery, and I still needed some code for converting the indented text to an HTML list, better to stay in the JS realm for all the solution and not mess with CSS. However, I really enjoyed how that guy took advantage of checkboxes to keep track of the collapsing state of folders.

After a couple of days, I shared my Indentation to Toggling List project on GitHub.

 

How to setup a Multi Page Application in AngularJS

Given that

  1. AngularJS enforces a Single Page Application structure;
  2. a web application can easily have two or more layouts;

I understood that I needed to setup a Multi Page Application in AngularJS to be able to have multiple layouts with standard routing, i.e. without installing, understanding, and learning special solutions like ui-router. How did I do it? In a few words,

  1. independent SPAs share the same routes
  2. routes reference the SPA they belong to
  3. when a user crosses one’s SPA boundaries, the other SPA is navigated to

Application Structure

(click on bold text to expand / collapse)
client/
  apps/
    auth/
      components/
        login/
          login.html
          login.js
        register/
        reset-password/
      index/
        index.css
      index.html
      shared/
        config.js
        modules/
        services/
      start.js
    core/
      components/
        bears/
          bears.html
          bears.js
        home/
          home.html
        users/
          users.html
          users.js
      index/
        index.css
      index.html
      shared/
        config.js
        modules/
        services/
          bears.service.js
          users.service.js
      start.js
  bower_components/
  docs/
  shared/
    config.js
    modules/
      my-empty-constroller.js
      my-route.js
    services/
      authentication.service.js
      crud.service.js
      flash.service.js
      storage.service.js
    my-project.js
    routes.js

This MPA allows to host its SPAs into their own folder in ´client/apps/´ and functionalities shared among them (like the authentication service) into ´client/shared/´.

Each of those SPAs allows to host its components into their own folders in ´client/apps/<app>/components/´ and functionalities shared among them (like the user service) into ´client/apps/<app>/shared/´.

Thus the organization of specific and shared stuff between the MPA and its SPAs is analogous to that between each SPA and its components.

client/shared/routes.js

Routes belong to the MPA, i.e. they are shared among all the SPAs. This allows me to look at one file and know exactly which URL translates to which component of which app.

define([
    '/shared/modules/my-route.js'  // loads routeProvider (no $ prefix)
], function() {
    'use strict';

    MyProject.Config({
        anonymousRoutes: ['/login', '/register', '/reset-password']
    });

    angular
        .module(MyProject.AppName())
        .config(routes);

    routes.$inject = ['$routeProvider', 'routeProvider'];

    function routes($routeProvider, routeProvider) {

        var route = routeProvider.route;

        /* beautify preserve:start */

        $routeProvider

            .when('/login',                 route.forComponent('auth: login as vm'))
            .when('/register',              route.forComponent('auth: register as vm'))
            .when('/reset-password',        route.forComponent('auth: reset-password as vm'))

            // Case 1: This works because AngularJS allows a view without a controller (but it does not allow a route without a view).
            .when('/',                      route.forComponent({
                                                app: 'core',
                                                filepath: 'home',
                                                controller: '',
                                                title: ''
                                            }))

            // Case 2: This works like Case 1, but it requires a controller at /apps/core/components/home/home.js.
            // .when('/',                      route.forComponent('core: home'))

            // Case 3: This works like Case 2, but using an empty controller.
            // .when('/',                      route.forComponent({
            //                                     app: 'core',
            //                                     filepath: 'home',
            //                                     controller: 'myEmptyController',
            //                                     controllerUrl: '/shared/modules/my-empty-controller.js'
            //                                 }))

            // .when('/path-to/some-stuff',      route.forComponent('core: some-dir/something'))
            // .when('/path-to/more-stuff',      route.forComponent('core: some-dir/anything'))

            .when('/bears',                 route.forComponent('core: bears'))

            .otherwise({
                redirectTo: function(params, path, search) {
                    console.log('otherwise...');
                    return '/';
                }
            });
        /* beautify preserve:end */

    }

});

Based on the article Dynamically Loading Controllers and Views… by Dan Wahlin, I wrote my own route provider, which sets up routes for components, using the Folders-by-Feature Structure shown in the Application Structure section above. A few things to notice:

  • The ´routeProvider.route.forComponent(options)´ method creates a standard ´route´ argument for the ´$routeProvider.when(path, route)´ method.
  • The ´options´ argument can be a string or an object literal.
  • If ´options´ is a string, then its format must be: ´'<app>: <filepath>[ as <alias>]’´.
  • If ´options´ is an object, then its properties are the same as the ´route´ argument (documentation),
    • PLUS:
      • ´app´: required
      • ´filepath´: required
      • ´controllerUrl´: optional
  • Set ´options.controller´ to specify a special controller name. By default, a controller name is built by taking the last part of ´filepath´, making it camel case, and adding ´Controller´. It must be the registration name.
  • Set ´options.controller´ to ´”´ to indicate that the view has no controller.
  • Set ´options.templateUrl´ and / or ´options.controllerUrl´ to specify a special view and / or controller file. By default, view and controller files are built by taking the last part of ´filepath´ and adding ´.html´ and ´.js´. They must be the path part of a valid URL, i.e. based off ´client/´, like ´/shared/modules/my-empty-controller.js´.
  • A ´filepath´ is always based off ´client/ apps/ <app>/´ (reduced to ´…/´ below here).
    • Use a ´filepath´ like ´/ path/ to/ file´ (with a leading slash)
      for addressing files like ´…/ path/ to/ file.*´.
    • Use a ´filepath´ like ´path/ to/ file´ (without a leading slash, with middle slashes)
      for addressing files like ´…/ components/ path/ to/ file.*´.
    • Use a ´filepath´ like ´file´ (without a leading slash, without middle slashes)
      for addressing files like ´…/ components/ file/ file.*´.

The ´forComponent(options)´ method returns a route object with a ´resolve´ property which is a map of dependencies to be injected into the controller. Given that the ´load´ dependency returns a promise while loading the controller, the router will wait for it to be resolved. That is all explained in the documentation. The nice paradox is that, when the router gets the promise of the dependency to inject into the controller, there is no controller yet, i.e. the dependency is the controller itself. It’s a bit weird, but works like a charm, and all under the hood.

Compare the routes with the application structure above to understand how they collaborate. For example, when the router matches the ´’/login’´ path, it applies the route returned by ´forComponent(‘auth: login as vm’)´. In particular,

  • if the current app is ´auth´ then the ´client/ apps/ auth/ components/ login/ login.js´ controller is required before being bound to the ´client/ apps/ auth/ components/ login/ login.html´ view that Angular automatically downloads. Instead,
  • if the current app is not ´auth´ then the browser is redirected to the ´/login´ path of the ´auth´ app, which will cause the ´client/ apps/ auth/ index.html´ layout to be loaded. That will set the current app to ´auth´, and allow the routing flow to proceed like above.

Remember that the routes file is loaded (and ´forComponent´ is run) at bootstrap time, but each controller is lazily loaded only when the router matches its path, if ever.

client/shared/modules/my-route.js

(function() {
    'use strict';

    // See https://github.com/DanWahlin/CustomerManager/blob/master/CustomerManager/app/customersApp/services/routeResolver.js 

    // Note that this service is a module hosting a provider (it is loaded by the routes definition file)
    // (we are not using MyProject.CodeSetup here because this is not a component of an application)

    angular
        .module('myRoute', [])
        .constant('_', window._)
        .provider('route', provider); // 'route' is seen outside as 'routeProvider'

    function provider() {

        this.$get = function() {
            return this;
        };

        this.route = (function() {  // stick to AngularJS name convention to blend seamlessly

            return {
                forComponent: ForComponent  // stick to AngularJS name convention to blend seamlessly 
            };


            function NamedMatch(source, regexp, names) {
                var result = {};
                var matches = source.match(regexp);
                for (var i = 0, iTop = names.length; i < iTop; i++) {
                    if (! names[i]) continue;
                    result[names[i]] = matches[i];
                }
                return result;
            }

            function InitRoute(options) {
                var result = {};
                if (_.isPlainObject(options)) {
                    result = options;
                } else {
                    var simplified = options.replace(/^\s+|\s+$/, '').replace(/\s+/, ' ').replace(/ ?: ?/, ':');
                    var formatAppPathAlias = /(?:([\w-]+):)?([\/\w-]+)(?: as ([\w-]+))?/i;
                    result = NamedMatch(simplified, formatAppPathAlias, ['', 'app', 'filepath', 'controllerAs']);
                }
                return result;
            }

            function RedirectTo(appRoot) {
                return {
                    redirectTo: function(params, path, search) {
                        // Replace instead of assign, because we get here only when crossing an SPA border.
                        // In such a case, the current URL is "wrong". Examples: "/auth/#/", "/core/#/login"...
                        var new_path = appRoot + '/#' + path;
                        window.location.replace(new_path);
                        return; // do not return a string !
                    }
                };
            }

            function RealPath(filepath) {
                var result = filepath;
                if (filepath[0] == '/') {
                    // expecting a filepath relative to '<appRoot>'
                } else {
                    // expecting a filepath relative to '<appRoot>/components'
                    if (filepath.search('/') > 0) {
                        // explicit filepath (always without extension)
                        // like: 'some-folder/some-file' --> '<appRoot>/components/some-folder/some-file'
                    } else {
                        // implicit filepath (always without extension)
                        // like: 'some-file'             --> '<appRoot>/components/some-file/some-file'
                        result += '/' + filepath;
                    }
                    result = '/components/' + result;
                }
                return result;
            }

            function DefaultRoute(filepath) {
                var formatPathToFile = /^((?:\/[\w-]+)*)\/([\w-]+)$/;
                var file = NamedMatch(filepath, formatPathToFile, ['', '', 'file']).file;
                var result = {
                    title: _.startCase(file),
                    templateUrl: filepath + '.html',
                    controller: _.camelCase(file) + 'Controller',
                    controllerUrl: filepath + '.js'
                };
                return result;
            }

            function RequireDependencies($q, $rootScope, dependencies) {
                var defer = $q.defer();
                require(dependencies, function() {
                    defer.resolve();
                    $rootScope.$apply();
                });
                return defer.promise;
            }

            function LoadController(route) {
                var result = {};
                var dependencies = route.controller ? [route.controllerUrl] : [];
                if (dependencies.length) {
                    var promiseName = 'load ' + route.controller;
                    var promiseMap = {};
                    promiseMap[promiseName] = ['$q', '$rootScope', function ($q, $rootScope) {
                        // we are going to a route inside the same SPA we are into
                        // because we took care earlier about crossing the SPA border
                        return RequireDependencies($q, $rootScope, dependencies);
                    }];
                    result.resolve = _.extend(route.resolve || {}, promiseMap);
                }
                return result;
            }

            function ForComponent(options) {
                var result = InitRoute(options);
                if (!result.filepath) {
                    throw 'Expected a filepath for the component.';
                }

                var appName = result.app;
                var appRoot = '/apps/' + appName;
                if (appName && MyProject.AppName() !== appName) {
                    return RedirectTo(appRoot);
                }

                result.filepath = appRoot + RealPath(result.filepath);
                result = _.extend(DefaultRoute(result.filepath), result);
                result = _.extend(result, LoadController(result));
                return result;
            }
        })();

    }

})();

 

How to setup permissions and roles on NodeJS

At aercolino/mean-app you’ll see a partially developed NodeJS app. I’m still working on it, but I’ve recently added a permission model that works with permissions like this:

    'anybody canEdit theirStuff': {
        anybody: 'User',
        theirStuff: {
            model: /.*/,
            restriction: function (anybody, theirStuff) { 
                return theirStuff.owner_id && (theirStuff.owner_id === anybody.id);
            }
        }
    }

I like it because it’s quite clean and very self-documenting. I personally find it very frustrating that permissions use to be buried into code in many places and need always some computer guy to translate them into business terms.

The name / value pair above completely defines a permission. The name is not only an English description but also active code to be interpreted by the model using the value. The name is SUBJECT + ACTION + OBJECT and the value should contain descriptors for each of those parts.

Notice that the meaning of all those parts comes from their descriptors in the value. For example, there is no conventional meaning assigned to the ´canEdit´ action except for it to be matched against the action passed to the Can function. Compare with this:

    'anAdmin canDo everything': {
        anAdmin: {
            model: 'User',
            restriction: function (user) {
                return user.isAdmin();
            }
        },
        canDo: /.*/,
        everything: /.*/
    }

And here are some console logs just to see how it works.

Screen Shot 2015-12-16 at 12.48.40

What do you think? :-)

Usage

To illustrate how to use these permissions I’m going to implement a use case where a developer wants to check permissions before allowing updates to models. I’m going to only show needed changes to existing files: the complete files are in the repository on GitHub.

Load the Can function

File ´/server/start.js´: Make the permission model available to the app. The ´Can´ function returns a promise which will be resolved with the first matching permission name or ´false´.

global.Can = require('./app/components/permissions/permission.model').Can;

Add support for the Can function

File ´/server/app/shared/CRUD.controller.js´ (old): This is how the ´Update´ function looked like before adding a check for verifying whether the update is allowed or not.

    function Update(req, res) {

        Item.findById(req.params.item_id, function(error, item) {

            if (error) {
                return stuff.SendFailure(res, error, 'Bad Request');
            }

            if (!item) {
                error = 'Expected a valid id.';
                return stuff.SendFailure(res, error, 'Bad Request');
            }

            FilterFields(fields, req.body, function (fFields) {

                fFields.forEach(function (fField) {
                    item[fField.name] = fField.value;
                });

                item.save(function(error) {

                    if (error) {
                        return stuff.SendFailure(res, error, 'Bad Request');
                    }

                    stuff.SendSuccess(res, null, Item.modelName + ' updated!');

                });

            });
            
        });

    }

File ´/server/app/shared/CRUD.controller.js´ (new): This is how the ´Update´ function looks like after adding a check for verifying whether the update is allowed or not.

    function Update(req, res) {

        Item.findById(req.params.item_id, function(error, item) {

            if (error) {
                return stuff.SendFailure(res, error, 'Bad Request');
            }

            if (!item) {
                error = 'Expected a valid id.';
                return stuff.SendFailure(res, error, 'Bad Request');
            }

            function UpdateItem() {
                FilterFields(fields, req.body, function (fFields) {

                    fFields.forEach(function (fField) {
                        item[fField.name] = fField.value;
                    });

                    item.save(function(error) {

                        if (error) {
                            return stuff.SendFailure(res, error, 'Bad Request');
                        }

                        stuff.SendSuccess(res, null, Item.modelName + ' updated!');

                    });

                });
            }

            if (self.AllowUpdate) {
                Promise.resolve(self.AllowUpdate(item, req))
                    .then(function (allowed) {
                        if (allowed) {
                            UpdateItem();
                        } else {
                            var error = req.currentUser.name + ' cannot update ' + item.constructor.modelName + ' ' + item.id;
                            stuff.SendFailure(res, error, 'Unauthorized');
                        }
                    })
                    .catch(function (error) {
                        return stuff.SendFailure(res, error, 'Bad Request');
                    });
            } else {
                UpdateItem();
            }

        });

    }

Instead of directly calling the ´Can´ function from the ´Update´ function, I prefer to call it from an ´AllowUpdate´ method, optionally defined on the model. This solution is much more flexible because it allows to easily tell apart when a check is required and when it is not: if the ´AllowUpdate´ is defined, it will be used, otherwise the update will take place straight away. Additionally, this solution decouples the permissions functionality from the (shared) CRUD functionality, enabling the developer to allow an update or not for whatever reason, not necessarily by checking a permission.

Notice that:

  1. I had to wrap the call to ´FilterFields(…)´ (which is the updating block) into a local function so that it can be called after the check or immediately.
  2. The ´AllowUpdate´ result could be a promise or not, and ´Promise.resolve(self.AllowUpdate(item, req))´ takes care of that.
  3. The ´AllowUpdate´ arguments could be a subject and an object, i.e. a user and an item, thus mimicking the arguments for the descriptors of the permissions. I decided instead to pass the item and the request.
    1. The order is important for highlighting that this is not (necessarily) related to a permission.
    2. The request carries the session, whose current user is the big deal.
  4. The error we signal when the update is not allowed is independent from the permission model: We just say that the ´AllowUpdate´ check failed.

Call the Can function

File ´/server/app/components/users/user.controller.js (old)´: This is how the ´User´ controller looked like before adding an ´AllowUpdate´ method.

'use strict';

var fields = [
    'name', 
    function (password) { 
        var Promise = require('es6-promise').Promise;
        return new Promise(function (resolve, reject) {
            var Hash = require(absPath + '/app/components/auth/hash');
            Hash({plaintext: password}, function (error, result) {
                if (error) {
                    reject(Error(error));
                } else {
                    delete result.plaintext;
                    resolve(result);
                }
            });
        });
    }, 
    function (admin) { 
        return !!admin.length; 
    }
];

var Item = require('./user.model');
var Controller = require(absPath + '/app/shared/CRUD.controller');
module.exports = Controller(Item, fields);

Notice that if no ´AllowUpdate´ method was necessary, this code would still work perfectly well with the permission setup I’m describing here.

File ´/server/app/components/users/user.controller.js (new)´: This is how the ´User´ controller looks like after adding an ´AllowUpdate´ method.

'use strict';

var fields = [
    'name', 
    function (password) { 
        var Promise = require('es6-promise').Promise;
        return new Promise(function (resolve, reject) {
            var Hash = require(absPath + '/app/components/auth/hash');
            Hash({plaintext: password}, function (error, result) {
                if (error) {
                    reject(Error(error));
                } else {
                    delete result.plaintext;
                    resolve(result);
                }
            });
        });
    }, 
    function (admin) { 
        return !!admin.length; 
    }
];

var Item = require('./user.model');
var Controller = require(absPath + '/app/shared/CRUD.controller');
var self = Controller(Item, fields);
module.exports = self;


self.AllowUpdate = function (item, req) {
    return Can(req.currentUser, 'edit', item)
        .then(function (allowed) {
            if (allowed) {
                log.info('%s can edit %s %s because %s', req.currentUser.name, item.constructor.modelName, item.id, allowed);
            }
            return allowed;
        });
}

Notice how I take advantage of the resolved value to ´log.info()´ about the matching permission right here, while the ´AllowUpdate´ result is going to be taken into account from the shared CRUD controller code.

Permissions

File ´/server/app/components/permissions/permissions.js´: Here is where the available permissions are listed. This is an example with all the different shapes (hopefully). Add and remove as needed.

'use strict';

module.exports = {

    'Translators canTranslate DocumentsNeedingTranslation': {},

    'Translators canTranslate documentsNeedingTranslation': {
        documentsNeedingTranslation: {
            model: 'Document',
            restriction: function (_, document) {
                return document.translations.length < 2;
            }
        }
    },

    'anybody canEdit TheirStuff': {
        anybody: 'User'
    },

    'anybody canEdit theirStuff': {
        anybody: 'User',
        theirStuff: {
            model: /.*/,
            restriction: function (anybody, theirStuff) { 
                return theirStuff.owner_id && (theirStuff.owner_id === anybody.id);
            }
        }
    },

    'anAdmin canDo everything': {
        anAdmin: {
            model: 'User',
            restriction: function (user) {
                return user.isAdmin();
            }
        },
        canDo: /.*/,
        everything: /.*/
    }
}

Here is a bunch of things to know about permissions.

  • ´Translators canTranslate DocumentsNeedingTranslation´
    • A permission name is a sentence SUBJECT + ACTION + OBJECT.
    • An action must begin with ´can´.
    • An actor is either a subject or an object.
    • Actors starting with an uppercase letter are role names.
    • Roles are defined by instances of the ´Role´ model, thus they cannot be defined in the permission value.
  • ´Translators canTranslate documentsNeedingTranslation´
    • Actors starting with a lowercase letter are item names.
    • Items must be defined by descriptors in the permission value.
    • A descriptor can be a hash with a ´model´ string and a ´restriction´ function.
    • The corresponding actor must then be an item of the given model, which also satisfies the given restriction.
    • The restriction function always gets two arguments: the subject item and the object item, in this order.
    • An underscore in the signature means that the corresponding item is not relevant. (by convention)
    • The name of an argument should always be whatever makes the most sense for self documentation.
    • Permissions can overlap with each other, like this permission overlaps with the previous one. They’re not nice, one is probably superfluous, but the permission model gets happily along with them.
  • ´anybody canEdit TheirStuff´
  • ´anybody canEdit theirStuff´
    • A descriptor can be a model string. The corresponding actor must then be an item of the given model.
    • A descriptor can contain a ´model´ RegEx. The corresponding actor must then be an item of a matching model.
    • As a policy, to keep things simple, a RegEx R (fully) matches a string S if
      • ´S.replace(R, ”) === ”´
    • The descriptor of the item ´theirStuff´ is an implementation of Duck Typing.
    • Compare the descriptor of the item ´theirStuff´ with the role ´TheirStuff´ and its setup below.
  • ´anAdmin canDo everything´
    • Actions can be defined by RegEx descriptors in the permission value.
    • A match-all RegEx for the action and the object makes this permission VERY generic.

Role and Permission Models

Role Model

File ´/server/app/components/roles/role.model.js´: This is the role model.

'use strict';

var mongoose = require('mongoose');

var schema   = new mongoose.Schema({
    name: {
        type: String,
        required: true,
        unique: true
    },

    model: {
        type: String,
        required: true
    },

    restriction: mongoose.Schema.Types.Mixed    
});

module.exports = mongoose.model('Role', schema);

Here is a bunch of things to know about the permission model.

  • The model must be a string, even when it should be a RegEx.
    • To differentiate a RegEx string from a non-RegEx string, wrap it into slashes (with optional modifiers at the end).
    • In other words, the string to use for a RegEx is its source with properly escaped backslashes.
  • If the restriction R is a JSON object, then an item I of the model M has this role if
    • ´M.count(Extend({_id: I._id}, R)) === 1´.
    • R is considered like Mongoose / MongoDB criteria.
  • If the restriction R is a non-empty string, then an item I of the model M has this role if
    • ´R(subject, object) == true´.
    • R is considered like a function name.
    • The function name can contain dots.
    • If the function name is a (static) method of an unavailable Model, it will be automatically required.
  • If the restriction R is FALSEy, then an item I of the model M has this role if
    • ´item.roles.indexOf(name) > -1´.
    • This is the case corresponding to a classical ´Role´ model, where a given role is associated to a given user by adding the role name to the user roles.
  • If the restriction R is TRUEy, then each item I of the model M has this role.
    • This role acts like an alias of the model.
    • The usefulness of this case is still unknown…

Their Stuff

Here is how the role ´TheirStuff´ I used in a permission above could be implemented.

{
    "_id" : ObjectId("5668394b896c2786c11b88a0"),
    "name" : "TheirStuff",
    "model" : "/.*/",
    "restriction" : "User.owns"
}

File ´/server/app/components/users/user.model.js´: This is the user model, after adding both the instance method ´isAdmin´ the static method ´owns´ used before and right above respectively.

'use strict';

var mongoose = require('mongoose');

var schema   = new mongoose.Schema({
    name: {
        type: String,
        required: true,
        unique: true
    },
    password: {
        type: {
            algorithm:  String,
            digest:     String,
            iterations: Number,
            salt:       String,
            key:        String
        },
        required: true,
        select: false
    },
    owner_id: mongoose.Schema.Types.ObjectId,
    roles: [String]
});

schema.methods.isAdmin = function () {
    var result = this.roles.indexOf('Admin') > -1;
    return result;
};

schema.statics.owns = function (anybody, theirStuff) { 
    var result = theirStuff.owner_id && (theirStuff.owner_id === anybody.id);
    return result;
};

var self = mongoose.model('User', schema);
module.exports = self;

Permission Model

File ´/server/app/components/permissions/permission.model.js´: This is the permission model.

I’m not going to illustrate its supporting functions (peruse them here, if you like), but here is its main code.

'use strict';

// TODO: review error management, maybe creating Errors with stuff.DefineError
// TODO: add support for setting a custom priority at which to check a permission
// TODO: add support for breaking out of the chain of permissions to check
// TODO: add support for negation like: 'HandcuffedPeople CANTedit TheirStuff'

var Promise = require('es6-promise').Promise;
var TypeOf  = require(absPath + '/app/shared/stuff').TypeOf;
var Apply   = require(absPath + '/app/shared/stuff').Apply;
var Find    = require(absPath + '/app/shared/stuff').ArrayFind;
var Extend  = require('util')._extend;

var source = require('./permissions');
var permissions = Compile(source);

var self = {
    Can: Can
};

module.exports = self;

return;

Here is a bunch of things to know about the permission model.

  • When the permission model is required the permissions list is loaded and compiled.
  • The compilation process builds a structure that the global ´Can´ function will later use.
  • A permission answers a ´Can´ question if their actions, subjects, and objects respectively match each other.
  • Permissions are selected, sorted, chained, and then all checked in turn, one after the other.
  • Sorting is based on a very rough complexity measure, detected during compilation.
  • As soon as a full match is found, the ´Can´ function immediately resolves with the permission name.
  • If no full match exists, then the ´Can´ function finally resolves with a ´false´.

Enjoy.

How to support promised predicates in Array find

I’m building ACL support in NodeJS. For example, I’ll allow to write:

if (Can(currentUser, 'edit', theDocument)) {
    buttons.push('Edit');
}

For any given action, my ´Can´ function must check a list of permissions. If one matches, then the action ´Can´ asks for is granted, otherwise (no permission exists) it’s denied. Of course I’m only interested in the first permission to match, and to allow for optimizations, I want permissions to be checked in the order I provide.

It’s easy to see that my problem is solved by the Array.prototype.find method. The problem I have, though, is that it only works with immediate predicates, but my checks can entail both immediate and promised predicates. For example, I allow predicates to access the database.

I googled my problem and found this StackOverflow page. Bergi’s answer gives both recursive and non-recursive solutions. (Side note. There was a time when recursive was opposed to iterative. With promises, that’s no longer the case. In fact, the non-recursive solution is a chain of ´catch´ handlers. An iteration is used to build the chain but promises themselves, throwing exceptions, control the iteration.) Benjamin Gruenbaum’s answer gives a recursive solution.

Here are their issues.

  1. The predicate is hacky because it signals a ´false´ by throwing an exception.
  2. The promise management is hacky because (a) it must cater for the predicate with ´.catch()´, and (b) it signals the “not found” outcome with ´reject()´.
  3. The promise management and the predicate are very coupled.
  4. The contract is different from that of the Array.prototype.find method.

So I came up with this one.

function ArrayFind(array, Predicate, thisArg) {
    function MyPredicate(element, index, arr) {
        return Promise.resolve(Predicate.call(thisArg, element, index, arr))
            .then(function (value) {
                return value ? element : undefined;
            });
    }
    function MyResult(element) {
        this.element = element;
    }

    return array.reduce(function(sequence, element, index, arr) {
        return sequence
            .then(function(found) {
                if (found) {
                    throw new MyResult(found);
                }
                return MyPredicate(element, index, arr);
            });
        }, Promise.resolve())
        .catch(function(reason) {
            if (reason instanceof MyResult) {
                return reason.element;
            }
            throw reason;
        });
}

Apart from being a global function instead of an Array instance method, the contract is exactly the same as that of the Array.prototype.find method.

  1. The only hack I used is to immediately exit when an element is found instead of continuing until the end of the ´.then()´ chain. But how I implemented it is both robust (as in Robustness) and hidden (as in Information Hiding). To make sure I do not mistake a rightful exception with my hack, I throw my own fake exception which is a wrapper around the found element. Thus, my fake exception is caught by the last ´.catch()´ and the element is returned.
  2. The predicate can be both immediate or a promise, thanks to ´Promise.resolve(Predicate…)´. If it’s immediate, it can throw an exception if it has to, not if it doesn’t hold true. If it’s a promise, it can reject() if it has to, not if it doesn’t hold true.

Examples

Here are some examples.

    function ImmediatePredicate(n) {
        console.log('-- ' + n);
        if (n % 3 == 0) throw 'dirty'; 
        return n % 2 == 0;
    }

    function PromisePredicate(n) { 
      return new Promise(function(resolve, reject) { 
        window.setTimeout(function() {
          console.log('-- ' + n);
          if (n % 3 == 0) reject('dirty'); 
          resolve(n % 2 == 0);
        }, Math.random() * 2000 + 1000); 
      }); 
    }

    function MixedPredicate(n) {
        if (n < 10) return ImmediatePredicate(n);
        return PromisePredicate(n);
    }

The result is a promise

Here is how ArrayFind compares to Array.prototype.find when no exceptions are thrown:

[1,13,5,4,7].find(ImmediatePredicate)
VM834:3 -- 1
VM834:3 -- 13
VM834:3 -- 5
VM834:3 -- 4
4

ArrayFind([1,13,5,4,7], ImmediatePredicate)
VM834:3 -- 1
VM834:3 -- 13
VM834:3 -- 5
VM834:3 -- 4
Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined}

Of course the result is a Promise instead of the found element, but we can append additional handlers, like

ArrayFind([1,13,5,4,7], ImmediatePredicate)
  .then(function(result){
    console.log(result);
  })
  .catch(function(reason){
    console.warn(reason)
  })

VM834:3 -- 1
VM834:3 -- 13
VM834:3 -- 5
VM834:3 -- 4
VM880:4 4
Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined}

An exception makes the promise reject

Here is how ArrayFind compares to Array.prototype.find when an exception is thrown:

[1,3,5,4,7].find(ImmediatePredicate)
VM834:3 -- 1
VM834:3 -- 3
VM834:4 Uncaught dirty

ArrayFind([1,3,5,4,7], ImmediatePredicate)
  .then(function(result){
    console.log(result);
  })
  .catch(function(reason){
    console.warn(reason)
  })

VM834:3 -- 1
VM834:3 -- 3
VM925:7 dirty
Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined}

Immediate and promised predicates work equally well

Here you can see how ArrayFind supports at the same time immediate and promised predicates:

ArrayFind([1,13,5,4,7], MixedPredicate)
  .then(function(result){
    console.log(result);
  })
  .catch(function(reason){
    console.warn(reason)
  })

VM834:3 -- 1
Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined}
VM834:12 -- 13
VM834:3 -- 5
VM834:3 -- 4
VM955:4 4


ArrayFind([1,3,5,4,7], MixedPredicate)
  .then(function(result){
    console.log(result);
  })
  .catch(function(reason){
    console.warn(reason)
  })

VM834:3 -- 1
VM834:3 -- 3
VM974:7 dirty
Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined}


ArrayFind([1,12,5,4,7], MixedPredicate)
  .then(function(result){
    console.log(result);
  })
  .catch(function(reason){
    console.warn(reason)
  })

VM834:3 -- 1
Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined}
VM834:12 -- 12
VM975:7 dirty

You can’t appreciate from the output above, but the immediate predicate really outputs immediately. :-)

Also notice that in the second to last example the warned ´dirty´ comes from the immediate predicate because 3 < 10, while in the last example it comes from the promised predicate because 12 >= 10.

© 2017 Notes Log

Theme by Anders NorenUp ↑