API Docs for: 0.8.0
Show:

File: include/service/base_object_service.js

/*
    Copyright (C) 2016  PencilBlue, LLC

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/
'use strict';

//dependencies
var util              = require('../util');
var ErrorUtils        = require('../error/error_utils');
var async             = require('async');
var AsyncEventEmitter = require('async-eventemitter');
var Sanitizer         = require('sanitize-html');

module.exports = function(pb) {

    //pb Dependencies
    var events = new AsyncEventEmitter();

    /**
     * Provides functionality to perform CRUD operations on collections.  It
     * also allows for plugins to register for events in order to interact with
     * objects as they are being processed.  This makes it possible to add,
     * remove, or modify fields before they are passed to the entity that
     * triggered the operation.
     * @class BaseObjectService
     * @constructor
     * @param {Object} context
     * @param {String} context.type
     */
    function BaseObjectService(context){
        this.context = context;

        //error checking
        if (!util.isObject(context)) {
            throw new Error('The context parameter must be an object');
        }
        else if (!util.isString(context.type)) {
            throw new Error('The context.type parameter must be a string');
        }

        /**
         * Represents the name of the collection to interact with
         * @property type
         * @type {String}
         */
        this.type = context.type;

        /**
         * An instance of DAO to be used to interact with the persistence layer
         * @property dao
         * @type {DAO}
         */
        this.dao = new pb.SiteQueryService({site: context.site, onlyThisSite: context.onlyThisSite});
    }

    /**
     * The maximum allowed number of results allowed to be returned when using
     * the paging wrapper
     * @private
     * @static
     * @readonly
     * @property MAX_RESULTS
     * @type {Integer}
     */
    var MAX_RESULTS = 1000;

    /**
     * The event that is triggered before the count query is executed
     * @static
     * @readonly
     * @property BEFORE_COUNT
     * @type {String}
     */
    BaseObjectService.BEFORE_COUNT = "beforeCount";

    /**
     * The event that is triggered before the query is executed to retrieve
     * results
     * @static
     * @readonly
     * @property BEFORE_GET_ALL
     * @type {String}
     */
    BaseObjectService.BEFORE_GET_ALL = "beforeGetAll";

    /**
     * The event that is triggered when querying for one or more resources
     * @static
     * @readonly
     * @property GET_ALL
     * @type {String}
     */
    BaseObjectService.GET_ALL = "getAll";

    /**
     * The event that is triggered before the query is executed to retrieve
     * an item by ID
     * @static
     * @readonly
     * @property BEFORE_GET
     * @type {String}
     */
    BaseObjectService.BEFORE_GET = "beforeGet";

    /**
     * The event that is triggered when retrieving a resource by ID
     * @static
     * @readonly
     * @property GET_ALL
     * @type {String}
     */
    BaseObjectService.GET = "get";

    /**
     * The event that is triggered when a DTO is passed to the save function
     * @static
     * @readonly
     * @property FORMAT
     * @type {String}
     */
    BaseObjectService.FORMAT = "format";

    /**
     * The event that is triggered when a DTO is passed to the save function
     * and after the format event has completed
     * @static
     * @readonly
     * @property MERGE
     * @type {String}
     */
    BaseObjectService.MERGE = "merge";

    /**
     * The event that is triggered when a DTO is passed to the save function
     * and after the merge event has completed
     * @static
     * @readonly
     * @property VALIDATE
     * @type {String}
     */
    BaseObjectService.VALIDATE = "validate";

    /**
     * The event that is triggered when a DTO is passed to the save function
     * and aftr the validate event has completed.  When validation failures
     * occur this event will not fire.
     * @static
     * @readonly
     * @property BEFORE_SAVE
     * @type {String}
     */
    BaseObjectService.BEFORE_SAVE = "beforeSave";

    /**
     * The event that is triggered when a DTO is passed to the save function
     * and after the save operation has completed successfully.
     * @static
     * @readonly
     * @property AFTER_SAVE
     * @type {String}
     */
    BaseObjectService.AFTER_SAVE = "afterSave";

    /**
     * The event that is triggered when the delete function is called.
     * @static
     * @readonly
     * @property BEFORE_DELETE
     * @type {String}
     */
    BaseObjectService.BEFORE_DELETE = "beforeDelete";

    /**
     * The event that is triggered when the delete function is called and after
     * the delete operation has completed successfully.
     * @static
     * @readonly
     * @property AFTER_DELETE
     * @type {String}
     */
    BaseObjectService.AFTER_DELETE = "afterDelete";

    /**
     * @static
     * @property ContentSanitizationRulesOverride
     * @type {object|null}
     */
    BaseObjectService.ContentSanitizationRulesOverride = null;

    /**
     * @static
     * @property DefaultSanitizationRulesOverride
     * @type {object|null}
     */
    BaseObjectService.DefaultSanitizationRulesOverride = null;

    /**
     * Retrieves the object type supported by the service
     * @method getType
     * @return {String} The object type supported
     */
    BaseObjectService.prototype.getType = function() {
        return this.type;
    };

    /**
     * Retrieves a context object to be passed to event listeners
     * @method getContext
     * @param {String|Object|Number|Boolean} [data]
     * @return {Object}
     */
    BaseObjectService.prototype.getContext = function(data) {
        return util.merge(this.context, {
            service: this,
            data: data
        });
    };

    /**
     * Executes a query for resources against the persistence layer. The
     * function will callback with an array of results.  The function will
     * trigger the "getAll" event.  Also note that there is hard limit on the
     * number of results the returned.
     * @method getAll
     * @param {Object} [options]
     * @param {Object} [options.select]
     * @param {Object} [options.where]
     * @param {Array} [options.order]
     * @param {Integer} [options.limit]
     * @param {Integer} [options.offset]
     * @param {Function} cb A callback that takes two parameters.  The first is
     * an error, if occurred. The second is an array representing the results
     * of the query.
     */
    BaseObjectService.prototype.getAll = function(options, cb) {
        if (util.isFunction(options)) {
            cb      = options;
            options = {};
        }

        //fire off the beforeGetAll event to allow plugins to modify queries
        var self    = this;
        var context = this.getContext(options);
        self._emit(BaseObjectService.BEFORE_GET_ALL, context, function(err) {
            if (util.isError(err)) {
                return cb(err);
            }

            //set a reasonable limit
            //TODO evaluate if this should be at the service level or controller
            //level
            var limit = BaseObjectService.getLimit(options.limit);

            var opts = {
                select: options.select,
                where: options.where,
                order: options.order,
                limit: limit,
                offset: options.offset
            };
            self.dao.q(self.type, opts, function(err, results) {
                if (util.isError(err)) {
                    return cb(err);
                }

                context.data = results;
                self._emit(BaseObjectService.GET_ALL, context, function(err) {
                    cb(err, results);
                });
            });
        });
    };

    /**
     * Executes a count of resources against the persistence layer. The
     * function will callback with an array of results.
     * @method count
     * @param {Object} [options]
     * @param {Object} [options.where]
     * @param {Function} cb A callback that takes two parameters.  The first is
     * an error, if occurred. The second is the number of results that match
     * the specified query
     */
    BaseObjectService.prototype.count = function(options, cb) {
        if (util.isFunction(options)) {
            cb      = options;
            options = {};
        }

        var self = this;
        var context = this.getContext(options);
        this._emit(BaseObjectService.BEFORE_COUNT, context, function(err) {
            if (util.isError(err)) {
                return cb(err);
            }

            self.dao.count(self.type, options.where, cb);
        });
    };

    /**
     * Executes a query for resources against the persistence layer. The
     * function will callback with an object that contains a total count and an
     * array of results.  The function will trigger the "getAll" event.  Also
     * note that there is hard limit on the number of results the returned.
     * @method getAllWithCount
     * @param {Object} [options]
     * @param {Object} [options.select]
     * @param {Object} [options.where]
     * @param {Array} [options.order]
     * @param {Integer} [options.limit]
     * @param {Integer} [options.offset]
     * @param {Function} cb A callback that takes two parameters.  The first is
     * an error, if occurred. The second is an object representing the results
     * of the query.
     */
    BaseObjectService.prototype.getAllWithCount = function(options, cb) {
        if (util.isFunction(options)) {
            cb      = options;
            options = {};
        }

        var tasks = {

            data: util.wrapTask(this, this.getAll, [options]),

            total: util.wrapTask(this, this.count, [options])
        };
        async.parallel(tasks, function(err, pageDetails) {
            if (util.isError(err)) {
                return cb(err);
            }

            pageDetails = BaseObjectService.getPagedResult(pageDetails.data, pageDetails.total, BaseObjectService.getLimit(options.limit), options.offset || 0);
            cb(null, pageDetails);
        });
    };

    /**
     * Retrieves a resource by ID. The function will callback with the object
     * that was found or NULL if no object could be found. The function will
     * trigger the "get" event.
     * @method get
     * @param {string} id
     * @param {object} [options]
     * @param {Function} cb (Error, object|null) A callback that takes two parameters.  The first is
     * an error, if occurred. The second is the object with the specified ID
     */
    BaseObjectService.prototype.get = function(id, options, cb) {
        if (util.isFunction(options)) {
            cb      = options;
            options = {};
        }

        //set the id in the options so that plugins can inspect the ID
        var self = this;
        options.id = id;

        //fire before event so that plugins can modify the options and perform any validations
        var context = this.getContext(options);
        this._emit(BaseObjectService.BEFORE_GET, context, function(err) {
            if (util.isError(err)) {
                return cb(err);
            }

            self._get(id, options, function(err, obj) {
                if (util.isError(err)) {
                    return cb(err);
                }

                context.data = obj;
                self._emit(BaseObjectService.GET, context, function(err) {
                    cb(err, obj);
                });
            });
        });
    };

    /**
     * @protected
     * @method _get
     * @param {String} id
     * @param {Object} [options]
     * @param {Function} cb
     */
    BaseObjectService.prototype._get = function(id, options, cb) {
        this.dao.loadById(id, this.type, options, cb);
    };

    /**
     * Retrieves a single resource by the specified query. The function will
     * callback with the object that was found or NULL if no object could be
     * found. The function will trigger the "getAll" event.
     * @method getSingle
     * @param {Object} [options]
     * @param {Object} [options.select]
     * @param {Object} [options.where]
     * @param {Array} [options.order]
     * @param {Integer} [options.offset]
     * @param {Function} cb A callback that takes two parameters.  The first is
     * an error, if occurred. The second is the object that matches the specified query
     */
    BaseObjectService.prototype.getSingle = function(options, cb) {
        if (util.isFunction(options)) {
            cb      = options;
            options = {};
        }
        options.limit = 1;

        this.getAll(options, function(err, results) {
            cb(err, util.isArray(results) && results.length ? results[0] : null);
        });
    };

    /**
     * Attempts to persist the DTO as an add.  The function executes a series of events:
     * 1) The format event is fired
     * 2) When an ID is provided the object is retrieved from the database otherwise a new object is created.
     * 3) The merge event is triggered
     * 4) The validate event is triggered. If validation errors are detected the process halts and the function calls back with an error.
     * 5) The beforeSave event is triggered
     * 6) The object is persisted
     * 7) The afterSave event is triggered
     *
     * @method add
     * @param {object} dto
     * @param {Object} [options]
     * @param {Function} cb A callback that takes two parameters.  The first is
     * an error, if occurred. The second is the object that matches the specified query
     */
    BaseObjectService.prototype.add = function(dto, options, cb) {
        if (util.isFunction(options)) {
            cb      = options;
            options = {};
        }
        options.isCreate = true;
        this.save(dto, options, cb);
    };

    /**
     * Attempts to persist the DTO as an update.  The function executes a series of events:
     * 1) The format event is fired
     * 2) When an ID is provided the object is retrieved from the database otherwise a new object is created.
     * 3) The merge event is triggered
     * 4) The validate event is triggered. If validation errors are detected the process halts and the function calls back with an error.
     * 5) The beforeSave event is triggered
     * 6) The object is persisted
     * 7) The afterSave event is triggered
     *
     * @method update
     * @param {object} dto
     * @param {Object} [options]
     * @param {Function} cb A callback that takes two parameters.  The first is
     * an error, if occurred. The second is the object that matches the specified query
     */
    BaseObjectService.prototype.update = function(dto, options, cb) {
        if (util.isFunction(options)) {
            cb      = options;
            options = {};
        }
        options.isCreate = false;
        this.save(dto, options, cb);
    };

    /**
     * Attempts to persist the DTO.  The function executes a series of events:
     * 1) The format event is fired
     * 2) When an ID is provided the object is retrieved from the database otherwise a new object is created.
     * 3) The merge event is triggered
     * 4) The validate event is triggered. If validation errors are detected the process halts and the function calls back with an error.
     * 5) The beforeSave event is triggered
     * 6) The object is persisted
     * 7) The afterSave event is triggered
     *
     * @method save
     * @param {object} dto
     * @param {Object} [options]
     * @param {Boolean} [options.isCreate]
     * @param {Function} cb A callback that takes two parameters.  The first is
     * an error, if occurred. The second is the object that matches the specified query
     */
    BaseObjectService.prototype.save = function(dto, options, cb) {
        if (util.isFunction(options)) {
            cb      = options;
            options = {};
        }

        //basic error checking
        if (util.isNullOrUndefined(dto)) {
            return cb(new Error('The dto parameter cannnot be null'));
        }

        //do any object formatting that should be done before validation
        var self    = this;
        var context = this.getContext(dto);
        context.isCreate = util.isBoolean(options.isCreate) ? options.isCreate : util.isNullOrUndefined(dto[pb.DAO.getIdField() || dto.id]);
        context.isUpdate = !context.isCreate;
        self._emit(BaseObjectService.FORMAT, context, function(err) {
            if (util.isError(err)) {
                return cb(err);
            }

            //detect if we are doing an update or insert.  On update retrieve
            //the obj and call the merge event handlers
            self._retrieveOnUpdateAndMerge(dto, options, function(err, obj) {
                if (util.isError(err) || util.isNullOrUndefined(obj)) {
                    return cb(err, obj);
                }

                //remove the dto from context and set the obj as the data to focus on
                context.data             = obj;
                context.validationErrors = [];

                //perform all validations
                self._emit(BaseObjectService.VALIDATE, context, function(err) {
                    if (util.isError(err)) {
                        return cb(err);
                    }

                    //check for validation errors
                    var validationErrors = BaseObjectService.consolidateValidationResults(context.validationErrors);
                    if (validationErrors.length) {
                        return cb(BaseObjectService.validationError(validationErrors));
                    }

                    //do any pre-save stuff
                    self._emit(BaseObjectService.BEFORE_SAVE, context, function(err) {
                        if (util.isError(err)) {
                            return cb(err);
                        }

                        //persist the object
                        options.object_type = self.type;
                        self.dao.save(obj, options, function(err/*, result*/) {
                            if (util.isError(err)) {
                                return cb(err);
                            }

                            //do any pre-save stuff
                            self._emit(BaseObjectService.AFTER_SAVE, context, function(err) {
                                cb(err, obj);
                            });
                        });
                    });
                });
            });
        });
    };

    /**
     * When an ID is available in the DTO the function attempts to retrieve the
     * existing object.  If it is not available a new object is created.  The
     * merge event is then called.  After the merge is complete the callback is
     * executed with the merged object.
     * @private
     * @method _retrieveOnUpdateAndMerge
     * @param {Object} dto
     * @param {Object} options
     * @param {Boolean} [options.isCreate]
     * @param {Function} cb
     */
    BaseObjectService.prototype._retrieveOnUpdateAndMerge = function(dto, options, cb) {

        //function used as callback handler so we can simplify if/else logic
        var self     = this;
        var where    = this.getIdWhere(dto);
        var isCreate = util.isBoolean(options.isCreate) ? options.isCreate : !where;
        var isUpdate = !isCreate;

        //ensure we have a valid where clause for update
        if (isUpdate && (util.isNullOrUndefined(where) || where === {})) {

            var error = BaseObjectService.validationError(
                [BaseObjectService.validationFailure('id', 'A valid ID could not be extracted from the request body to lookup the existing resource')]
            );
            return cb(error);
        }

        var onObjectRetrieved = function(err, obj) {
            if (util.isError(err) || util.isNullOrUndefined(obj)) {
                return cb(err, obj);
            }

            var context = self.getContext(dto);
            context.object = obj;
            context.isUpdate = isUpdate;
            context.isCreate = isCreate;

            self._emit(BaseObjectService.MERGE, context, function(err) {
                cb(err, obj);
            });
        };

        //when we are doing an update load the object
        if (isUpdate) {
            this.dao.loadByValues(where, this.type, onObjectRetrieved);
        }
        else {

            //we are creating a new object so pass it along
            onObjectRetrieved(null, {});
        }
    };

    /**
     * Creates the where clause that creates a lookup by the key that indicates
     * uniqueness for the collection
     * @method getIdWhere
     * @param {Object} dto
     * @return {Object}
     */
    BaseObjectService.prototype.getIdWhere = function(dto) {
        var idField = pb.DAO.getIdField();
        if (dto.id) {
            return pb.DAO.getIdWhere(dto.id);
        }
        else if (dto[idField]) {
            return pb.DAO.getIdWhere(dto[idField]);
        }
        return null;
    };

    /**
     * Deletes an object by ID
     * @method deleteById
     * @param {String} id
     * @param {Object} options
     * @param {Function} cb
     */
    BaseObjectService.prototype.deleteById = function(id, options, cb) {
        if (util.isFunction(options)) {
            cb      = options;
            options = {};
        }
        options.where = pb.DAO.getIdWhere(id);

        this.deleteSingle(options, cb);
    };

    /**
     * Deletes a single item based on the specified query in the options
     * @method deleteSingle
     * @param {Object} [options] See BaseObjectService#getSingle
     * @param {object} [options.where]
     * @param {Function} cb
     */
    BaseObjectService.prototype.deleteSingle = function(options, cb) {
        if (util.isFunction(options)) {
            cb      = options;
            options = {};
        }

        var self = this;
        this.getSingle(options, function(err, obj) {
            if (util.isError(err) || util.isNullOrUndefined(obj)) {
                return cb(err, obj);
            }

            var context = self.getContext(obj);
            self._emit(BaseObjectService.BEFORE_DELETE, context, function(err) {
                if (util.isError(err)) {
                    return cb(err, null);
                }

                self.dao.delete(options.where, self.type, options, function(err/*, result*/) {
                    if (util.isError(err)) {
                        return cb(err, obj);
                    }

                    self._emit(BaseObjectService.AFTER_DELETE, context, function(err) {
                        cb(err, obj);
                    });
                });
            });
        });
    };

    /**
     *
     * @protected
     * @method _emit
     * @param {String} event
     * @param {Object} data
     * @param {Function} cb
     */
    BaseObjectService.prototype._emit = function(event, data, cb) {
        pb.log.silly('BaseObjectService: Emitting events: [%s, %s.%s]', event, this.type, event);

        var self  = this;
        var tasks = [];

        //global events
        if (events.listeners(event).length > 0) {
            tasks.push(function(callback) {
                events.emit(event, data, callback);
            });
        }

        //object specific
        var eventName = self.type + '.' + event;
        if (events.listeners(eventName).length > 0) {
            tasks.push(function(callback) {

                events.emit(eventName, data, callback);
            });
        }
        async.series(tasks, cb);
    };

    /**
     * Creates a properly formed validation failure
     * @static
     * @method validationFailure
     * @param {String} field
     * @param {String} message
     * @param {String} [code]
     */
    BaseObjectService.validationFailure = function(field, message, code) {
        return {
            field: field || null,
            message: message || '',
            code: code || null
        };
    };

    /**
     * Inspects the raw set of validation results to ensure that plugins that
     * don't follow proper procedure have their results excluded.
     * @static
     * @method consolidateValidationResults
     * @param {Array} results
     * @return {Array}
     */
    BaseObjectService.consolidateValidationResults = function(results) {
        if (!util.isArray(results)) {
            return null;
        }

        //filter out any malformed validation results from plugins
        var validationErrors = [];
        results.forEach(function(validationError) {
            if (!util.isObject(validationError)) {
                return;
            }
            validationErrors.push(validationError);
        });

        return validationErrors;
    };

    /**
     * Creates a new Error representative of a validation error
     * @static
     * @method validationError
     * @param {Array|string} validationFailures
     * @return {Error}
     */
    BaseObjectService.validationError = function(validationFailures) {
        return ErrorUtils.badRequest({message: 'Validation Failure', validationErrors: validationFailures});
    };

    /**
     * Creates a new Error representative of the inability to locate the requested resource
     * @deprecated Will be removed in 1.0
     * @static
     * @method notFound
     * @param {String} [message]
     * @return {Error}
     */
    BaseObjectService.notFound = function(message) {
        return ErrorUtils.notFound(message);
    };

    /**
     * Strips HTML formatting from a string value
     * @static
     * @method sanitize
     * @param {String} value
     * @param {Object} [config]
     * @return {String}
     */
    BaseObjectService.sanitize = function(value, config) {
        if (!value) {
            return value;
        }
        else if (!util.isObject(config)) {
            config = BaseObjectService.getDefaultSanitizationRules();
        }
        return Sanitizer(value, config);
    };

    /**
     * The sanitization rules that apply to Pages, Articles, and other fields
     * that are allowed to have HTML
     * @static
     * @method getContentSanitizationRules
     * @return {Object}
     */
    BaseObjectService.getContentSanitizationRules = function() {
        return BaseObjectService.ContentSanitizationRulesOverride || {
            allowedTags: [ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol', 'nl', 'li', 'b', 'i', 'strong', 'em', 'strike', 'code', 'hr', 'br', 'div', 'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre', 'img', 'u', 'span' ],
            allowedAttributes: {
                a: [ 'href', 'name', 'target', 'class', 'align'],
                img: [ 'src', 'class', 'align'],
                p: ['align', 'class'],
                h1: ['style', 'class', 'align'],
                h2: ['style', 'class', 'align'],
                h3: ['style', 'class', 'align'],
                h4: ['style', 'class', 'align'],
                h5: ['style', 'class', 'align'],
                h6: ['style', 'class', 'align'],
                div: ['style', 'class', 'align'],
                span: ['style', 'class', 'align'],
                table: ['style', 'class', 'align'],
                tr: ['style', 'class', 'align'],
                th: ['style', 'class', 'align'],
                td: ['style', 'class', 'align']
            },

            // Lots of these won't come up by default because we don't allow them
            selfClosing: [ 'img', 'br', 'hr', 'area', 'base', 'basefont', 'input', 'link', 'meta' ],

            // URL schemes we permit
            allowedSchemes: [ 'http', 'https', 'ftp', 'mailto' ]
        };
    };

    /**
     * Sets a system-wide override for the set of sanitization rules for content.  The function will throw if a non-object
     * is passed.  NULL can be used to reset any override that is put into place.
     * @static
     * @method setDefaultSanitizationRules
     * @param {object} rules
     */
    BaseObjectService.setContentSanitizationRules = function(rules) {
        if (typeof rules !== 'object' || util.isArray(rules)) {
            throw new Error('The content sanitization rules must be an object or null');
        }
        BaseObjectService.ContentSanitizationRulesOverride = rules;
    };

    /**
     * Sets a system-wide override for the default set of sanitization rules.  The function will throw if a non-object
     * is passed.  NULL can be used to reset any override that is put into place.
     * @static
     * @method setDefaultSanitizationRules
     * @param {object} rules
     */
    BaseObjectService.setDefaultSanitizationRules = function(rules) {
        if (typeof rules !== 'object' || util.isArray(rules)) {
            throw new Error('The default sanitization rules must be an object or null');
        }
        BaseObjectService.DefaultSanitizationRulesOverride = rules;
    };

    /**
     * Retrieves the default sanitization rules for string fields.
     * @static
     * @method getDefaultSanitizationRules
     * @return {Object}
     */
    BaseObjectService.getDefaultSanitizationRules = function() {
        return BaseObjectService.DefaultSanitizationRulesOverride || {
            allowedTags: [],
            allowedAttributes: {}
        };
    };

    /**
     * Parses an ISO date string.  When an invalid date string is pass a NULL
     * value is returned.
     * @static
     * @method getDate
     * @param {String} dateStr
     * @return {Date}
     */
    BaseObjectService.getDate = function(dateStr) {
        var val = Date.parse(dateStr);
        return isNaN(val) ? null : new Date(val);
    };

    /**
     * Determines the maximum number of results that can be returned for a
     * query.  The specified limit must be a positive integer.  The result will
     * be the minimum of the MAX_RESULTS constant and the specified limit.
     * @static
     * @method getLimit
     * @param {Integer} limit
     * @return {Integer}
     */
    BaseObjectService.getLimit = function(limit) {
        return util.isNullOrUndefined(limit) || isNaN(limit) || limit <= 0 ? MAX_RESULTS : Math.min(limit, MAX_RESULTS);
    };

    /**
     * Builds a paged result object
     * @static
     * @method getPagedResult
     * @param {Array} dataArray The array of items to return
     * @param {Integer} total The total number of items available in the collection
     * @param {Integer} [limit] The maximum number of items to return
     * @param {Integer} [offset] The number of items skipped
     */
    BaseObjectService.getPagedResult = function(dataArray, total, limit, offset) {
        return {
            count: dataArray.length,
            data: dataArray,
            total: total,
            limit: limit,
            offset: offset
        };
    };

    /**
     * Extracts a boolean value from the provided value.  Null or undefined values will return false.  Strings of '1' or
     * 'true' (case sensitive) will return TRUE.  All other values will return false.
     * @static
     * @method parseBoolean
     * @param {string|boolean} val
     * @return {boolean}
     */
    BaseObjectService.parseBoolean = function(val) {
        if (util.isNullOrUndefined(val)) {
            return false;
        }
        if (util.isBoolean(val)) {
            return val;
        }

        //check for other truths
        switch (val) {
            case '1':
            case 'true':
            case 1:
                return true;
            default:
                return false;
        }
    };

    /**
     * Registers a listener for the specified event.
     * @static
     * @method on
     * @param {String} event
     * @param {Function} listener
     * @return {*}
     */
    BaseObjectService.on = function(event, listener) {
        return events.on(event, listener);
    };

    /**
     * Registers a listener to fire a single time for the specfied event
     * @static
     * @method once
     * @param {String} event
     * @param {Function} listener
     * @return {*}
     */
    BaseObjectService.once = function(event, listener) {
        return events.once(event, listener);
    };

    /**
     * Removes the listener from the specified event
     * @static
     * @method removeListener
     * @param {String} event
     * @param {Function} listener
     * @return {*}
     */
    BaseObjectService.removeListener = function(event, listener) {
        return events.removeListener(event, listener);
    };

    /**
     * Removes all listeners for the specified event
     * @static
     * @method removeAllListeners
     * @param {String} event
     * @return {*}
     */
    BaseObjectService.removeAllListeners = function(event) {
        return events.removeAllListeners(event);
    };

    /**
     * Sets the maximum number of listeners for the emitter
     * @static
     * @method setMaxListeners
     * @param {Integer} n
     * @return {EventEmitter}
     */
    BaseObjectService.setMaxListeners = function(n) {
        return events.setMaxListeners(n);
    };

    /**
     * Returns a list of the listeners for the specified event
     * @static
     * @method listeners
     * @param {String} event
     * @return {Array}
     */
    BaseObjectService.listeners = function(event) {
        return events.listeners(event);
    };

    return BaseObjectService;
};