data/data-manager.js

/*
 * Your installation or use of this SugarCRM file is subject to the applicable
 * terms available at
 * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/.
 * If you do not agree to all of the applicable terms or do not have the
 * authority to bind the entity as an authorized representative, then do not
 * install or use this SugarCRM file.
 *
 * Copyright (C) SugarCRM Inc. All rights reserved.
 */

const Acl = require('../core/acl');
const Utils = require('../utils/utils');
const Bean = require('./bean');
const BeanCollection = require('./bean-collection');
const MixedBeanCollection = require('./mixed-bean-collection');

/**
 * The data manager handles the beans and collections life cycle. It provides
 * an API to declare data classes, instantiate them, and synchronize the data
 * with the server. It relies on the data structure defined by the application
 * metadata.
 *
 * ** The data manager provides:**
 *
 * - An interface to declare bean and collection classes from metadata
 * - Factory methods for creating instances of beans and bean collections
 * - Factory methods for creating instances of bean relations and relation
 *   collections
 * - A custom implementation of `Backbone.sync`
 *
 * **Data model metadata**
 *
 * The metadata is used to describe the data model. It contains information
 * about module fields and relationships.
 * From the following sample metadata, the data manager would declare two
 * classes: Opportunities and Contacts.
 * ```
 * var metadata = {
 *     modules: {
 *         Opportunities: {
 *             fields: {
 *                 name: { ... },
 *                 ...
 *             }
 *         },
 *         Contacts: { ... }
 *    },
 *    relationships: {
 *        opportunities_contacts: { ... },
 *        ...
 *    }
 * };
 * ```
 *
 * **Working with beans**
 *
 * Declare bean classes from metadata payload.
 * `declareModels` should be called at application start-up and whenever the
 * metadata changes:
 * ```
 * const DataManager = require('./data-manager');
 * DataManager.declareModels(metadata);
 * ```
 * You may now create bean instances using factory methods.
 *
 * ```
 * var opportunity = DataManager.createBean(
 *     'Opportunities',
 *     { name: 'Cool opportunity' }
 * );
 * // You can save a bean using the standard `Backbone.Model.save` method.
 * // The save method will use the data manager's sync method to communicate
 * // changes to the remote server.
 * opportunity.save();
 *
 * // Create an empty collection of contacts.
 * var contacts = DataManager.createBeanCollection('Contacts');
 * // Fetch a list of contacts
 * contacts.fetch();
 * ```
 *
 * **Working with relationships**
 *
 * ```
 * var attrs = {
 *     firstName: 'John',
 *     lastName: 'Smith',
 *     // relationship field
 *     opportunityRole: 'Influencer'
 * }
 * // Create a new instance of a contact related to an existing opportunity
 * var contact = DataManager.createRelatedBean(opportunity, null, 'contacts', attrs);
 * // This will save the contact and create the relationship
 * contact.save(null, { relate: true });
 *
 * // Create an instance of contact collection related to an existing opportunity
 * var contacts = DataManager.createRelatedCollection(opportunity, 'contacts');
 * // This will fetch related contacts
 * contacts.fetch({ relate: true });
 *
 * ```
 *
 * @module Data/DataManager
 */

// Bean class cache
var _models = {};

// Bean collection class cache
var _collections = {};

/**
 * Gets the relationship name given the module and the link name.
 *
 * @param {string} module The module name.
 * @param {string} link The link name.
 * @return {string|boolean} relationship The relationship name or `false` if it
 *   is empty or undefined.
 * @private
 */
const getRelationshipName = function(module, link) {
    let vardefs = SUGAR.App.metadata.getModule(module);
    let relationship = vardefs.fields[link] && vardefs.fields[link].relationship;
    if (!relationship) {
        return false;
    }

    return relationship;
};

/**
 * @alias module:Data/DataManager
 */
const DataManager = _.extend({
    /**
     * Reference to the base bean model class constructor. Defaults to
     * {@link Data/Bean}.
     *
     * @type {Function}
     */
    beanModel: Bean,

    /**
     * Reference to the base bean collection class constructor. Defaults to
     * {@link Data/BeanCollection}.
     *
     * @type {Function}
     */
    beanCollection: BeanCollection,

    /**
     * Reference to the base mixed bean collection class constructor. Defaults
     * to {@link Data/MixedBeanCollection}.
     *
     * @type {Function}
     */
    mixedBeanCollection: MixedBeanCollection,

    /**
     * Initializes the data manager.
     */
    init: function() {
        var sync = _.bind(this.sync, this);
        this.beanModel.prototype.sync = sync;
        this.beanCollection.prototype.sync = sync;

        SUGAR.App.events.register(
            /**
             * Fires when the sync operation starts.
             *
             * Three parameters are passed to the callback:
             *
             *  * operation name (`method`)
             *  * reference to the model/collection
             *  * options
             *
             * ```
             * const Events = require('../core/events');
             * Events.on('data:sync:start', function(method, model, options) {
             *     SUGAR.App.logger.debug('Started operation ' + method + ' on ' + model);
             * });
             * ```
             * @event data:sync:start
             */
            "data:sync:start",
            this
        );

        SUGAR.App.events.register(
            /**
             * Fires when the sync operation ends.
             *
             * Four parameters are passed to the callback:
             *
             *  * operation name (`method`)
             *  * reference to the model/collection
             *  * options
             *  * request (SUGAR.Api.HttpRequest)
             *
             * ```
             * const Events = require('../core/events');
             * Events.on('data:sync:complete', function(method, model, options, request) {
             *     SUGAR.App.logger.debug("Finished operation " + method + " on " + model);
             * });
             * ```
             * @event data:sync:complete
             */
            "data:sync:complete",
            this
        );

        SUGAR.App.events.register(
            /**
             * Fires when the sync operation ends successfully.
             *
             * Four parameters are passed to the callback:
             *
             *  - operation name (`method`)
             *  - reference to the model/collection
             *  - options
             *  - request (SUGAR.Api.HttpRequest)
             *
             * ```
             * const Events = require('../core/events');
             * Events.on('data:sync:success', function(method, model, options, request) {
             *     SUGAR.App.logger.debug('Finished operation ' + method + ' on ' + model);
             * });
             * ```
             * @event data:sync:success
             */
            "data:sync:success",
            this
        );

        SUGAR.App.events.register(
            /**
             * Fires when the sync operation ends unsuccessfully.
             *
             * Four parameters are passed to the callback:
             *
             *  * operation name (`method`)
             *  * reference to the model/collection
             *  * options
             *  * error (SUGAR.Api.HttpError)
             *
             * ```
             * const Events = require('../core/events');
             * Events.on('data:sync:error', function(method, model, options, error) {
             *     SUGAR.App.logger.debug('Operation failed ' + method + ' on ' + model);
             * });
             * ```
             * @event data:sync:error
             */
            "data:sync:error",
            this
        );

        SUGAR.App.events.register(
            /**
             * Fires when the sync operation was aborted.
             *
             * Four parameters are passed to the callback:
             *
             *  * operation name (`method`)
             *  * reference to the model/collection
             *  * options
             *  * request {@link SUGAR.Api.HttpRequest}
             *
             * ```
             * const Events = require('../core/events');
             * SUGAR.App.events.on('data:sync:abort', function(method, model, options, request) {
             *     SUGAR.App.logger.debug('Operation aborted ' + method + ' on ' + model);
             * });
             * ```
             * @event data:sync:abort
             */
            'data:sync:abort',
            this
        );
    },

    /**
     * Resets class declarations.
     *
     * @param {string} [module] The module name from which to remove the bean
     *   and collection class. If not specified, resets bean and collection
     *   classes of all modules.
     */
    reset: function (module) {
        this.resetModel(module);
        this.resetCollection(module);
    },

    /**
     * Resets bean class declarations.
     *
     * @param {string} [module] The module name from which to remove the bean
     *   class. If not specified, resets bean classes of all modules.
     */
    resetModel: function (module) {
        if (module) {
            delete _models[module];
        } else {
            _models = {};
        }
    },

    /**
     * Resets collection class declarations.
     *
     * @param {string} [module] The module name from which to remove the
     *   collection class. If not specified, resets collection classes of all
     *   modules.
     */
    resetCollection: function (module) {
        if (module) {
            delete _collections[module];
        }  else {
            _collections = {};
        }
    },

    /**
     * Declares bean model and collection classes for a given module and caches
     * them.
     *
     * @param {string} moduleName Module name.
     * @param {Object} module Module metadata object.
     * @param {string} platform The platform name.
     * @param {Object} [modelController] The bean controller to declare.
     * @param {Object} [collectionController] The collection controller to declare.
     */
    declareModel: function(moduleName, module, platform, modelController, collectionController) {
        // Bug 54814 init fields to something sane if module metadata is empty
        platform = platform || SUGAR.App.config.platform || 'base';

        this.declareModelClass(moduleName, module, platform, modelController);
        this.declareCollectionClass(moduleName, platform, collectionController);
    },

    /**
     * Declares the bean model class for a given module.
     *
     * @param {string} moduleName The module name.
     * @param {string} module The module metadata.
     * @param {string} platform The platform name.
     * @param {Object} [modelController] The model controller.
     * @return {Function} The created class.
     */
    declareModelClass: function(moduleName, module, platform, modelController) {
        var platformNamespace = Utils.capitalize(platform);
        moduleName = moduleName || platformNamespace + 'Model';

        this.resetModel(moduleName);

        var fields = module ? module.fields : {};
        var defaults = null;

        _.each(_.values(fields), function(field) {
            if (!_.isUndefined(field['default'])) {
                if (defaults === null) {
                    defaults = {};
                }
                defaults[field.name] = field['default'];
            }
        });

        var baseProperties = {
            /**
             * The hash of field names and default values.
             *
             * This hash should be used instead of the `Backbone.Model#default`
             * property. Setting default values is done in
             * {@link Data/Bean#initialize} only if the given model is new.
             *
             * @memberOf Data/Bean
             * @type {Object}
             * @instance
             */
            _defaults: defaults,

            /**
             * The module name.
             *
             * @memberOf Data/Bean
             * @type {string}
             * @instance
             * @readonly
             */
            module: moduleName,

            /**
             * The list of fields and their vardefs.
             *
             * @memberOf Data/Bean
             * @type {Object}
             * @instance
             * @readonly
             */
            fields: fields
        };

        var superModel = _models[platformNamespace + 'Model'] ||
            _models.BaseModel ||
            this.beanModel;

        modelController = _.extend(modelController || {}, baseProperties);

        return Utils.extendClass(_models, superModel, moduleName, modelController, platformNamespace);
    },

    /**
     * Declares bean collection class for a given module.
     *
     * @param {string} moduleName The module name.
     * @param {string} platform The platform name.
     * @param {Object} [collectionController] The controller.
     * @return {Function} The created class.
     */
    declareCollectionClass: function(moduleName, platform, collectionController) {
        var platformNamespace = Utils.capitalize(platform);
        var modelName = moduleName || platformNamespace + 'Model';
        var model = _models[modelName];

        moduleName = moduleName || platformNamespace + 'Collection';

        if (!model) {
            return;
        }
        this.resetCollection(moduleName);

        var baseCollectionProperties = {
            model: model,

            /**
             * The module name.
             *
             * @memberOf Data/BeanCollection
             * @type {string}
             * @instance
             * @readonly
             */
            module: moduleName,


            /**
             * Current collection offset for pagination.
             *
             * @memberOf Data/BeanCollection
             * @type {number}
             * @instance
             * @readonly
             */
            offset: 0
        };

        var superCollection = _collections[platformNamespace + 'Collection'] ||
            _collections.BaseCollection ||
            this.beanCollection;

        collectionController = _.extend(collectionController || {}, baseCollectionProperties);

        return Utils.extendClass(
            _collections,
            superCollection,
            moduleName,
            collectionController,
            platformNamespace
        );
    },

    /**
     * Merges a model with its metadata.
     *
     * @param {string} name The module name.
     * @param {Object} [module] The module metadata.
     */
    mergeModel: function(name, module) {
        var fields = module ? module.fields : {};
        var defaults = null;

        _.each(_.values(fields), function(field) {
            if (!_.isUndefined(field['default'])) {
                if (defaults === null) {
                    defaults = {};
                }
                defaults[field.name] = field['default'];
            }
        });

        var baseProperties = {
            /**
             * Same as {@link Data/Bean#_defaults}
             * @inheritdoc
             */
            _defaults: defaults,

            /**
             * Same as {@link Data/Bean#module}
             * @inheritdoc
             */
            module: name,

            /**
             * Same as {@link Data/Bean#fields}
             * @inheritdoc
             */
            fields: fields
        };

        _.extend(_models[name].prototype, baseProperties);
    },

    /**
     * Gets all bean classes.
     *
     * @return {Object} The hash of bean classes.
     */
    getModelClasses: function() {
        return _models;
    },

    /**
     * Gets all collection classes.
     *
     * @return {Object} The hash of collection classes.
     */
    getCollectionClasses: function() {
        return _collections;
    },

    /**
     * Declares bean models and collections classes for each module definition.
     *
     * Data manager uses {@link Core.MetadataManager#getModules} method to get
     * metadata if `modules` parameter is not specified.
     *
     * @param {Object} [modules] metadata hash in which keys are module names
     *   and values are module definitions.
     */
    declareModels: function(modules) {
        modules = modules || SUGAR.App.metadata.getModules();
        _.each(modules, function(module, name) {
            if (!_models[name]) {
                this.declareModel(name, module);
            } else {
                this.mergeModel(name, module);
            }
        }, this);
    },

    /**
     * Gets a bean class.
     *
     * @param {string} module The module name to get the bean class from.
     * @return {Object} The bean class for the given `module`, or
     *   `this.beanModel` if not found.
     */
    getBeanClass: function(module) {
        return _models[module] || this.beanModel;
    },

    /**
     * Creates an instance of a bean. Example of usage:
     *
     * ```
     * // Create an account bean. The account's name property will be set to
     * // "Acme".
     * const DataManager = require('./data-manager');
     * var account = DataManager.createBean('Accounts', { name: 'Acme' });
     *
     * // Create a team set bean with a given ID
     * var teamSet = DataManager.createBean('TeamSets', { id: 'xyz' });
     *
     * ```
     *
     * @param {string} module The module name.
     * @param {Object} [attrs] Initial values of bean attributes, which
     *   will be set on the bean.
     * @param {Object} [options] A hash of options.
     * @return {Data/Bean} A new instance of a bean.
     */
    createBean: function(module, attrs, options) {
        return _models[module] ? new _models[module](attrs, options) : new DataManager.beanModel(attrs, options);
    },

    /**
     * Creates instance of a bean collection. Example of usage:
     *
     * ```
     * const DataManager = require('./data-manager');
     * // Creates an empty collection of account beans.
     * var accounts = DataManager.createBeanCollection('Accounts');
     * ```
     *
     * @param {string} module The module name.
     * @param {Data/Bean[]} [models] Initial array or collection of models.
     * @param {Object} [options] A hash of options.
     * @return {Data/BeanCollection} A new instance of a bean collection.
     */
    createBeanCollection: function(module, models, options) {
        return _collections[module] ?
            new _collections[module](models, options) :
            new DataManager.beanCollection(models, options);
    },

    /**
     * Creates an instance of related {@link Data/Bean} or updates an
     * existing bean with link information.
     *
     * ```
     * // Create a new contact related to the given opportunity.
     * const DataManager = require('./data-manager');
     * var contact = DataManager.createRelatedBean(opportunity, '1', 'contacts', {
     *    'first_name': 'John',
     *    'last_name': 'Smith',
     *    'contact_role': 'Decision Maker'
     * });
     * contact.save(null, { relate: true });
     * ```
     *
     * @param {Data/Bean} bean1 Instance of the first bean.
     * @param {Data/Bean|string} beanOrId2 Instance or ID of the second
     *   bean. A new instance is created if this parameter is `null`.
     * @param {string} link Relationship link name.
     * @param {Object} [attrs] Bean attributes hash.
     * @return {Data/Bean} A new instance of the related bean or existing
     *   bean instance updated with relationship link information.
     */
    createRelatedBean: function(bean1, beanOrId2, link, attrs) {
        var relatedModule = this.getRelatedModule(bean1.module, link);

        attrs = attrs || {};
        if (_.isString(beanOrId2)) {
            attrs.id = beanOrId2;
            beanOrId2 = this.createBean(relatedModule, attrs);
        }
        else if (_.isNull(beanOrId2)) {
            beanOrId2 = this.createBean(relatedModule, attrs);
        }
        else {
            beanOrId2.set(attrs);
        }

        /**
         * Relationship link information.
         *
         * <pre>
         * {
         *     name: link name,
         *     bean: reference to the related bean
         *     isNew: flag indicating that it is a new relationship
         * }
         * </pre>
         *
         * The `link.isNew` flag is used to distinguish between an existing
         * relationship and a relationship that is about to be created. Please
         * refer to REST API specification for details. In brief, REST API
         * supports creating a new relationship for two existing records as
         * well as updating an existing relationship (updating relationship
         * fields). The `link.isNew` flag equals to `true` by default. The flag
         * is set to `false` by data manager once a relationship is created and
         * whenever relationships are fetched from the server.
         *
         * @memberOf Data/Bean
         * @type {Object}
         * @name link
         * @instance
         */
        beanOrId2.link = {
            name: link,
            bean: bean1,
            isNew: true
        };

        beanOrId2.setOption('relate', true);

        return beanOrId2;
    },

    /**
     * Creates a new instance of related bean collection.
     *
     * ```
     * // Create contacts collection for an existing opportunity.
     * const DataManager = require('./data-manager');
     * var contacts = DataManager.createRelatedCollection(opportunity, 'contacts');
     * contacts.fetch({ relate: true });
     *
     * ```
     *
     * The newly created collection is cached in the given bean instance.
     *
     * @param {Data/Bean} bean Bean to link the related beans to.
     * @param {string} link Relationship link name.
     * @param {Array|Data/BeanCollection} [models] An array of related beans to
     *   populate the newly created collection with.
     * @return {Data/BeanCollection} The created bean collection.
     */
    createRelatedCollection: function(bean, link, models) {
        var relatedModule = this.getRelatedModule(bean.module, link);
        var collection = this.createBeanCollection(relatedModule, models, {
            /**
             * Link information.
             *
             * <pre>
             * {
             *     name: link name,
             *     bean: reference to the related bean
             * }
             * </pre>
             *
             * @memberOf Data/BeanCollection
             * @instance
             */
            link: {
                name: link,
                bean: bean
            }
        });

        collection.setOption('relate', true);

        bean._setRelatedCollection(link, collection);
        return collection;
    },

    /**
     * Creates a collection of beans of different modules.
     *
     * @param {Array|Data/BeanCollection} [models] A list of models to
     *   populate the new collection with.
     * @param {Object} [options] A hash of options.
     * @return {Data/MixedBeanCollection} Collection of mixed modules
     *   collection.
     */
    createMixedBeanCollection: function(models, options) {
        return new DataManager.mixedBeanCollection(models, options);
    },

    /**
     * Checks if a given module can have multiple relationships via a given
     * link.
     *
     * @param {string} module Name of the module to do the check for.
     * @param {string} link Relationship link name.
     * @return {boolean} `true` if the module's link is a
     *   `many`-type relationship, `false` otherwise.
     */
    canHaveMany: function(module, link) {
        var meta = SUGAR.App.metadata.getModule(module);
        if (!meta || !meta.fields || !meta.fields[link]) {
            return false;
        }

        var linkType = meta.fields[link].link_type || meta.fields[link]['link-type'];
        if (linkType) {
            return linkType === 'many';
        }
        var name = meta.fields[link].relationship;
        var relationship = SUGAR.App.metadata.getRelationship(name);
        var rel = relationship.true_relationship_type || relationship.relationship_type;
        var t = rel.split('-');
        var type;
        if (meta.fields[link].side) {
            type = meta.fields[link].side === 'left' ? t[0] : t[2];
        } else if (relationship.lhs_module !== relationship.rhs_module) {
            type = module === relationship.rhs_module ? t[0] : t[2];
        } else {
            type = (t[0] === 'many' || t[2] === 'many') ? 'many' : 'one';
        }
        return type === 'many';
    },

    /**
     * Gets fields of type `relate` for a given link.
     *
     * Example:
     *
     * Assumptions:
     * - We have 2 modules `Accounts` and `Cases`.
     * - `Accounts` has a link field named `cases`, which represents a 1-to-many
     *   relationship between the 2 modules: A case is linked to one account and
     *   a account can be linked to many cases.
     *
     * To retrieve the field that matches this relationship on the Cases side,
     * you can call:
     * ```
     * var relateField = DataManager.getRelateField('Accounts', 'cases');
     * ```
     * and you would get the definition of the Cases field that exposes the
     * parent Account bean.
     *
     * @param {string} parentModuleName Name of the module that has a link field
     *   named `link`.
     * @param {string} link Link name.
     * @return {Array} Definitions of the `relate` fields if found or empty
     *   array if not found.
     */
    getRelateFields: function(parentModuleName, link) {
        //Overridden to provide safeguard against exception filed in NOMAD-2979
        //We couldn't reproduce the issue or find out exact place where data/metadata is inconsistent
        var parentModuleDef = SUGAR.App.metadata.getModule(parentModuleName);
        if (!parentModuleDef.fields[link]) {
            SUGAR.App.logger.error(`Calling 'getRelateFields' on '${parentModuleName}' with link '${link}' but no fields` +
                ` have been found. Please fix your metadata.`);
            return [];
        }

        var relationship = parentModuleDef.fields[link].relationship;
        var relatedModule = this.getRelatedModule(parentModuleName, link);
        var fields = SUGAR.App.metadata.getModule(relatedModule).fields;

        // Find the opposite link field on related module
        var f = _.find(fields, function(field) {
            return field.type == "link" && field.relationship == relationship;
        });

        if (f) {
            f = _.filter(fields, function(field) {
                return field.type == "relate" && field.link == f.name;
            });
        }

        return f || [];
    },

    /**
     * Gets relationship fields for a complex relationship.
     *
     * Some relationships may have relationship fields, that only makes sense in
     * the context of the relationship between 2 modules.
     * Use the the following, to get the relationships fields definition of a
     * relationship:
     * ```
     * let relationshipFields = DataManager.getRelationshipFields('Opportunities', 'contacts');
     * ```
     *
     * @param {string} parentModule Name of the module that has a link field
     *   called `link`.
     * @param {string} link Link name.
     * @return {Array} Relationship fields.
     * @deprecated
     */
    getRelationshipFields: function(parentModule, link) {
        SUGAR.App.logger.warn('DataManager#getRelationshipFields is deprecated since 7.10. It will be removed ' +
            'in a future release. Please use DataManager#getRelationFields instead');
        var ff = null;
        var linkField = SUGAR.App.metadata.getModule(parentModule).fields[link];
        if (linkField.rel_fields) {
            var relationship = linkField.relationship;
            var relatedModule = this.getRelatedModule(parentModule, link);
            var fields = SUGAR.App.metadata.getModule(relatedModule).fields;

            // Find the opposite link field on related module
            var f = _.find(fields, function(field) {
                return field.type == "link" && field.relationship == relationship;
            });

            // Find relationship_info field
            if (f) {
                f = _.find(fields, function(field) {
                    return field.link == f.name && field.link_type == "relationship_info";
                });
            }

            // Extract relationship fields
            if (f && f.relationship_fields) {
                var fieldNames = _.keys(linkField.rel_fields);
                _.each(f.relationship_fields, function(field, name) {
                    if (_.include(fieldNames, name)) {
                        if (!ff) ff = [];
                        ff.push(field);
                    }
                });
            }
        }

        return ff;

    },

    /**
     * Gets relationship fields for a complex relationship.
     *
     * Some relationships may have relationship fields, that only makes sense in
     * the context of the relationship between 2 modules.
     * Use the the following, to get the relationships fields definition of a
     * relationship:
     * ```
     * let relationshipFields = DataManager.getRelationFields('Opportunities', 'contacts');
     * ```
     *
     * @param {string} module Name of the module that has a link field
     *   called `link`.
     * @param {string} link Link name.
     * @return {Array|boolean} Relationship fields. `false` if no fields are
     *   found.
     */
    getRelationFields: function(module, link) {
        let moduleFields = SUGAR.App.metadata.getModule(module) && SUGAR.App.metadata.getModule(module).fields;
        if (!moduleFields) {
            return false;
        }

        let fields = _.filter(moduleFields, (field) => {
            return field.link === link && !_.isUndefined(field.rname_link);
        });

        if (!fields) {
            return false;
        }

        return _.pluck(fields, 'name');
    },

    /**
     * Given a module and a link field name, this method gets the link field
     * name of the other module of the relationship.
     *
     * @param {string} module The module name.
     * @param {string} link The link name for the given module.
     * @return {string|boolean} oppositeLink The link field name of the other module
     *   of the relationship. `false` if not found.
     */
    getOppositeLink: function(module, link) {
        let relationshipName = getRelationshipName(module, link);
        let relatedModule = this.getRelatedModule(module, link);
        let relatedVardefs = SUGAR.App.metadata.getModule(relatedModule);
        if (!relatedVardefs || !relatedVardefs.fields) {
            return false;
        }

        let oppositeLink = _.findWhere(relatedVardefs.fields, {type: 'link', relationship: relationshipName});

        if (!oppositeLink) {
            return false;
        }

        return oppositeLink.name;
    },

    /**
     * Gets related module name.
     *
     * @param {string} module Name of the parent module.
     * @param {string} link Relationship link name.
     * @return {string|boolean} The name of the related module. `false` if not
     *   found.
     */
    getRelatedModule: function(module, link) {
        var meta = SUGAR.App.metadata.getModule(module);
        if (!meta || !meta.fields || !meta.fields[link]) {
            return false;
        }

        let relationship = SUGAR.App.metadata.getRelationship(getRelationshipName(module, link));
        if (!relationship) {
            return meta.fields[link].module || false;
        }

        return meta.fields[link].module || (module === relationship.rhs_module ?
            relationship.lhs_module : relationship.rhs_module);
    },

    /*
     * Returns field definition of a given field.
     *
     * @param {string} module The module the field belongs to.
     * @param {string} idFieldName Name of the field to retrieve.
     */
    getRelatedNameField: function(module, idFieldName) {
        return _.find(SUGAR.App.metadata.getModule(module).fields, function(field) {
            if (field.name !== idFieldName && field.id_name === idFieldName) {
                return field;
            }
        }, this);
    },

    /**
     * Gets editable fields.
     *
     * @param {Data/Bean|Data/BeanCollection} model The bean or collection to
     *   get fields from.
     * @param {Array} [fields] Field names to be checked.
     * @return {Object} Hash of editable fields.
     */
    getEditableFields: function(model, fields) {
        var editableFields = ['id'], //Always have the id included (without the id, the routing will not work correctly)
            ignoreTypeList = ["parent", "relate"];

        fields = fields || [];

        // No fields were specified, try the model's attributes instead
        if (!fields.length) {
            fields = _.keys(model.attributes);
        }

        // Editable fields are fields that are either DB fields, such as
        // name, or related fields that do have a real DB field behind them,
        // such as opportunity_role (contact_role), that the user has access to edit.
        // The following code will filter out fields such as assigned_user_name.
        _.each(fields, function(fieldName) {
            var fieldValue;

            if(model.has(fieldName) && // Model has that field AND
                (model.fields[fieldName] && // Field exists in the model AND
                    (!model.fields[fieldName].source || // (The field does not have a source specified OR
                        model.fields[fieldName].source !== 'non-db' || // the field's source is something other than 'non-db' OR
                        !model.fields[fieldName].type || // The field does not have a field type specified OR
                        ignoreTypeList.indexOf(model.fields[fieldName].type) === -1)) && // The field's source is 'non-db', but the field's type is not in our ignore list) AND
                Acl.hasAccessToModel("edit", model, fieldName)) { // The user has access to edit the field

                fieldValue = model.get(fieldName);
                //FIXME: This if condition is deprecated. It relies on a
                //format created by VirtualCollection Mango plugin which
                //sidecar does not know about and needs to be be removed.
                if (fieldValue && (model.fields[fieldName].type === 'collection') &&
                    !_.isEmpty(fieldValue.links)) {
                    _.each(fieldValue.links, function(link) {
                        editableFields.push(link.link.name);
                    });
                } else {
                    editableFields.push(fieldName);
                }
            }
        });

        return model.toJSON({
            fields: editableFields
        });
    },

    /**
     * Custom implementation of `Backbone.sync` pattern. Syncs models with
     * the remote server using {@link SUGAR.Api}.
     *
     * @param {string} method The CRUD method: 'create', 'read', 'update', or
     *   'delete'.
     * @param {Data/Bean|Data/BeanCollection} model The bean/collection to
     *   be synced/read.
     * @param {Object} [options] A hash of options.
     * @return {Sugar.HttpRequest|boolean} The sync request, or `false` if you
     *   attempted to load a linked context with no id on a linked bean.
     * @fires <b>data:sync:start</b> globally and on the bean, before the
     *   sync call is made.
     */
    sync: function(method, model, options) {
        SUGAR.App.logger.trace('data-sync-' + (options.relate ? 'relate-' : '') + method + ": " + model);
        options       = this.parseOptionsForSync(method, model, options);
        var callbacks = this.getSyncCallbacks(method, model, options),
            request = null;

        model.dataFetched = false;
        // trigger global data:sync:start event
        this.trigger("data:sync:start", method, model, options);

        /**
         * Fires on model when the sync operation starts.
         *
         * Two parameters are passed to the callback:
         *
         *  * operation name (`method`)
         *  * options
         *
         * ```
         * model.on('data:sync:start', function(method, options) {
         *     SUGAR.App.logger.debug('Started operation ' + method + ' on ' + model);
         * });
         * ```
         * @event data:sync:start
         */
        model.trigger("data:sync:start", method, options);

        if (_.isFunction(options.endpoint)) {
            request = options.endpoint(
                method,
                model,
                options,
                callbacks
            );
        }
        else if (model.link && model.link.bean && options.relate === true) {
            // Related data is an object should contain:
            // - related bean (including relationship fields) in case of create method
            // - just relationship fields in case of update method
            // - null for read/delete method
            var relatedData = null;
            if (method == "create" || method == "update") {
                // Pass all fields: bean fields + relationship fields
                relatedData = this.getEditableFields(model, options.fields);
                // Change 'update' method to 'create' if the relationship is a new one
                if (method == "update" && model.link.isNew) {
                    method = "create";
                }
            }

            if (!model.link.bean.id) {
                SUGAR.App.logger.error('Attempted to load linked context with no id on the linked bean: ' +
                    model.link.bean);
                return false;
            }

            request = SUGAR.App.api.relationships(
                method,
                model.link.bean.module,
                {
                    id: model.link.bean.id,
                    link: model.link.name,
                    relatedId: model.id,
                    related: relatedData
                },
                options.params,
                callbacks,
                options.apiOptions
            );
        }
        else if (options.favorite) {

            request = SUGAR.App.api.favorite(
                model.module,
                model.id,
                model.isFavorite(),
                callbacks,
                options.apiOptions
            );
        } else if (options.following) {
            request = SUGAR.App.api.follow(
                model.module,
                model.id,
                model.get('following'),
                callbacks,
                options.apiOptions
            );
        } else if (options.collectionField) {
            request = SUGAR.App.api.collection(
                options.module,
                {
                    id: options.beanId,
                    field: options.collectionField
                },
                options.params,
                callbacks,
                options.apiOptions
            );
        } else {
            // Use global search API whenever a query is specified or a mixed collection is used
            if (options.query || (model instanceof MixedBeanCollection)) {
                request = SUGAR.App.api.search(
                    options.params,
                    callbacks,
                    options.apiOptions
                );
            }
            else if (model.module) {
                request = SUGAR.App.api.records(
                    method,
                    model.module,
                    method == "update" || method == "create" ? this.getEditableFields(model, options.fields) : model.attributes,
                    options.params,
                    callbacks,
                    options.apiOptions
                );
            } else {
                SUGAR.App.logger.error("Unable to sync model with no module");
            }
        }

        return request;
    },

    /**
     * Builds and returns the options to pass to the `sync` request.
     *
     * @param  {string} method The CRUD method.
     * @param {Data/Bean|Data/BeanCollection} model The bean/collection to
     *   be synced/read.
     * @param {Object} [options] A hash of options.
     * @return {Object} A hash of options.
     */
    parseOptionsForSync: function(method, model, options) {
        options = options || {};
        options.params = _.extend({}, options.params);
        options.filterDef = options.filter || model.filterDef;
        options.method = method;

        if (options.view && _.isString(options.view) && method == "read") {
            options.params.view = options.view;
        }

        if (options.fields && method == "read") {
            options.params.fields = options.fields.join(",");
        }

        // Track as recently viewed
        if(options.viewed === true){
            options.params.viewed = "1";
        }

        if ((method == "read") && (model instanceof BeanCollection)) {
            if (options.offset && options.offset !== 0) {
                options.params.offset = options.offset;
            }

            if (options.limit || (SUGAR.App.config && SUGAR.App.config.maxQueryResult)) {
                options.params.max_num = options.limit || SUGAR.App.config.maxQueryResult;
            }

            if (model.orderBy && model.orderBy.field) {
                options.params.order_by = model.orderBy.field + ":" + model.orderBy.direction;
            }

            if (options.myItems === true) {
                options.params.my_items = "1";
            }

            if (options.favorites === true) {
                options.params.favorites = "1";
            }

            if (!_.isEmpty(options.filterDef)) {
                var filterDef = Utils.deepCopy(options.filterDef);

                // We want to assign to params.filter the filter definition
                // itself (the value assigned to the "filter" key).
                if (_.has(filterDef, "filter")) {
                    filterDef = filterDef.filter;
                }

                if (!_.isArray(filterDef)) {
                    filterDef = [filterDef];
                }
                options.params.filter = filterDef;
            }

            if (!_.isEmpty(options.query)) {
                options.params.q = options.query;
                if (_.isEmpty(options.module_list) && model.module) {
                    // Set module list to be the collection's module
                    options.module_list = [model.module];
                }
            }

            if (options.module_list) {
                options.params.module_list = options.module_list.join(",");
            }
        }

        if ((method === 'update') && (options.lastModified)) {
            options.apiOptions = options.apiOptions || {};
            options.apiOptions.headers = options.apiOptions.headers || {};
            options.apiOptions.headers['X-TIMESTAMP'] = options.lastModified;
        }

        return options;
    },

    /**
     * Gets the `sync` callback functions.
     *
     * @param  {string} method The CRUD method.
     * @param {Data/Bean|Data/BeanCollection} model The bean/collection to
     *   be synced/read.
     * @param {Object} [options] A hash of options.
     * @return {Object} A hash containing the fallowing callback functions:
     *   'success', 'error', 'complete', 'abort'.
     */
    getSyncCallbacks: function(method, model, options) {
        return {
            success: this.getSyncSuccessCallback(method, model, options),
            error: this.getSyncErrorCallback(method, model, options),
            complete: this.getSyncCompleteCallback(method, model, options),
            abort: this.getSyncAbortCallback(method, model, options)
        };
    },

    /**
     * Gets the `success` callback function for the {@link #sync} method.
     *
     * @param  {string} method The CRUD method.
     * @param {Data/Bean|Data/BeanCollection} model The bean/collection to
     *   be synced/read.
     * @param {Object} [options] A hash of options.
     * @return {Function} The `success` callback function.
     * @fires <b>data:sync:success</b> globally and on the bean, once the
     *   sync call is made and is successful.
     */
    getSyncSuccessCallback: function(method, model, options) {
        var self = this;
        return function(data, request) {
            model.inSync = true;
            model.original_assigned_user_id = model.get("assigned_user_id");
            if ((method == "read") && (model instanceof BeanCollection)) {
                var offset = options.offset || 0;
                data = data || {};

                if (_.isNumber(data.next_offset)) {
                    model.offset = data.next_offset != -1 ? data.next_offset : offset + (data.records || []).length;
                    model.next_offset = data.next_offset;
                    model.page = model.getPageNumber(options);
                }

                if (!options.update) {
                    // We need to invalidate the cached version of the total
                    // when a reset is fired on the collection from actions
                    // such as filtering. If however, the total is being
                    // populated from the server response, that will be
                    // used instead.
                    model.total = _.isNumber(data.total) ? data.total : null;
                }

                if (model instanceof MixedBeanCollection) {
                    // We need `xmod_aggs` property which are the facets
                    // for search.
                    model.xmod_aggs = data.xmod_aggs || null;
                    model.tags = data.tags || null;

                    // In a case of a collection field fetch, `next_offset` is
                    // an object containing the next_offset of each link.
                    if (_.isObject(data.next_offset)) {
                        model.offset = data.next_offset;
                        model.next_offset = data.next_offset;
                    }
                }

                data = data.records || [];

                // Update collection filter/search properties on success
                self._updateCollectionProperties(model, options);
            }

            if (options.relate === true) {
                // Reset the flag to indicate that relationship(s) do exist.
                model.link.isNew = false;

                if (method != "read") {
                    // The response for create/update/delete relationship contains updated beans
                    if (model.link.bean) {
                        var syncedAttributes = model.link.bean.getSynced(),
                            updatedAttributes = _.reduce(data.record, function(memo, val, key) {
                                if (!_.isEqual(syncedAttributes[key], val)) {
                                    memo[key] = val;
                                }
                                return memo;
                            }, {});
                        model.link.bean.set(updatedAttributes);
                        //Once parent model is reset, reset internal synced attributes as well
                        model.link.bean.setSyncedAttributes(data.record);
                    }

                    data = data.related_record;
                    // Attributes will be set automatically for create/update but not for delete
                    // Also, break the link
                    if (method == "delete") {
                        model.set(data);
                        delete model.link;
                    }
                }
            }

            model.dataFetched = true;
            if (options.success) options.success(data);
            // trigger global data:sync:success event
            self.trigger("data:sync:success", method, model, options, request);
            model.inSync = null;
        };

    },

    /**
     * Gets the `error` callback function for the sync {@link #sync} method.
     *
     * Triggers the global `data:sync:complete` event (registered on
     * {@link Core.Events SUGAR.App.events}), as well as on the `model`.
     *
     * Executes the abort callback if we are aborting from a previous
     * collection fetch request.
     *
     * @param {string} method The CRUD method.
     * @param {Data/Bean|Data/BeanCollection} model The model/collection to
     *   be synced/read.
     * @param {Object} [options] A hash of options.
     * @param {Object} [options.error] Custom `error` callback function to be
     *   executed.
     * @return {Function} The wrapped `error` callback function.
     * @fires <b>data:sync:error</b> globally and on the bean, if the
     *   sync call returned an error.
     */
    getSyncErrorCallback: function(method, model, options) {
        return _.bind(function(error) {
            if (error.request && error.request.aborted) {
                var abortCallback = this.getSyncAbortCallback(method, model, options);
                return abortCallback(error.request);
            }
            SUGAR.App.error.handleHttpError(error, model, options);
            this.trigger('data:sync:error', method, model, options, error);
            /**
             * Fires on model when the sync operation ends unsuccessfully.
             *
             * Three parameters are passed to the callback:
             *
             *  - operation name (`method`)
             *  - options
             *  - error {@link SUGAR.Api.HttpError}
             *
             * ```
             * model.on('data:sync:error', function(method, options, error) {
             *     SUGAR.App.logger.debug('Operation failed:' + method + ' on ' + model);
             * });
             * ```
             *
             * @event data:sync:error
             */
            model.trigger('data:sync:error', method, options, error);

            if (_.isFunction(options.error)) {
                options.error(error);
            }
        }, this);
    },

    /**
     * Gets the `complete` callback function for the sync {@link #sync} method.
     *
     * @param {string} method The CRUD method.
     * @param {Data/Bean|Data/BeanCollection} model The bean/collection to
     *   be synced/read.
     * @param {Object} [options] A hash of options.
     * @param {Object} [options.complete] Custom `complete` callback
     *   function to be executed.
     * @return {Function} The wrapped `complete` callback function.
     * @fires <b>data:sync:complete</b> globally and on the bean/collection,
     *   once the sync call was complete.
     */
    getSyncCompleteCallback: function(method, model, options) {
        return _.bind(function(request) {
            this.trigger('data:sync:complete', method, model, options, request);
            /**
             * Fires on model when the sync operation ends.
             *
             * Three parameters are passed to the callback:
             *
             *  - operation name (`method`)
             *  - options
             *  - request {@link SUGAR.Api.HttpRequest}
             *
             * ```
             * model.on('data:sync:complete', function(method, options, request) {
             *     SUGAR.App.logger.debug('Finished operation ' + method + ' on ' + model);
             * });
             * ```
             *
             * @event data:sync:complete
             */
            model.trigger('data:sync:complete', method, options, request);

            if (_.isFunction(options.complete)) {
                options.complete(request);
            }

            // Prevent memory leaking
            options.previousModels = null;
            options = {};
        }, this);
    },

    /**
     * Gets the `abort` callback function for the sync {@link #sync} method.
     *
     * @param {string} method The CRUD method.
     * @param {Data/Bean|Data/BeanCollection} model The model/collection to
     *   be synced/read.
     * @param {Object} [options] A hash of options.
     * @param {Object} [options.abort] Custom `abort` callback
     *   function to be executed.
     * @return {Function} The wrapped `abort` callback function.
     * @fires <b>data:sync:abort</b> globally and on the bean/collection, if the
     *   sync request was aborted.
     */
    getSyncAbortCallback: function(method, model, options) {
        return _.bind(function(request) {
            this.trigger('data:sync:abort', method, model, options, request);
            /**
             * Fires on model when the sync operation ends.
             *
             * Three parameters are passed to the callback:
             *
             *  - operation name (`method`)
             *  - options
             *  - request {@link SUGAR.Api.HttpRequest}
             *
             * ```
             * model.on('data:sync:abort', function(method, options, request) {
             *     SUGAR.App.logger.debug('Operation aborted ' + method + ' on ' + model);
             * });
             * ```
             *
             * @event data:sync:abort
             */
            model.trigger('data:sync:abort', method, options, request);
        }, this);
    },

    /**
     * Updates various properties on the bean collection passed.
     *
     * @param {Data/BeanCollection} model The collection.
     * @param {Object} [options] A hash of options.
     * @private
     */
    _updateCollectionProperties: function (model, options) {
        options = options || {};

        /**
         * Flag indicating if a collection contains items assigned to the
         * current user.
         *
         * @memberOf Data/BeanCollection
         * @type {boolean}
         * @name myItems
         * @instance
         * @readonly
         */
        model.myItems = options.myItems;

        /**
         * Flag indicating if a collection contains current user's favorite
         * items.
         *
         * @memberOf Data/BeanCollection
         * @type {boolean}
         * @name favorites
         * @instance
         * @readonly
         */
        model.favorites = options.favorites;

        /**
         * Search query.
         *
         * @memberOf Data/BeanCollection
         * @type {string}
         * @name query
         * @instance
         * @readonly
         */
        model.query = options.query;

        /**
         * List of modules searched.
         *
         * @memberOf Data/MixedBeanCollection
         * @type {string}
         * @name modelList
         * @instance
         * @readonly
         */
        model.modelList = options.modelList;

        /**
         * Filter definition to filter the collection by.
         *
         * @memberOf Data/BeanCollection
         * @type {Array}
         * @name filterDef
         * @instance
         */
        model.filterDef = options.filterDef;
    }
}, Backbone.Events);

module.exports = DataManager;