JavaScript Extension Development API for Oracle Visual Builder Cloud Service - Classic Applications

Source: entity/js/api/Entity.js

/**
 * TODO: findRelationToEntity: better naming? - consolidate with getRelation (for property)? - or join and make it accepting various params?
 * TODO: getOwnRelations: Fix the naming?
 */
define([
    'bop/js/api/entity/EntityProviders',
    'core/js/api/Implementations',
    'entity/js/api/DataModel',
    'entity/js/api/Property',
    'entity/js/api/PropertyClassification',
    'entity/js/api/Relation',
    'entity/js/api/RelationCardinality',
    'entity/js/spi/Attributed',
    'translations/js/api/I18n'
], function (
        EntityProviders,
        Implementations,
        DataModel,
        Property,
        PropertyClassification,
        Relation,
        RelationCardinality,
        Attributed,
        I18n
        ) {

    'use strict';

    /**
     * Represents an entity object.
     *
     * <p>To get an instance of an entity with a given ID, see the example below
     * or consult methods in {@link module:api/js/Entities Entities}.</p>
     *
     * @AbcsAPI stable
     * @version 15.4.5
     * @exports entity/js/api/Entity
     *
     * @class
     * @private
     *
     * @param {Object} model
     * @param {EntityProvider} entityProvider
     * @param {Object} baseModel
     *
     * @example
     * <caption>
     *  Gets an instance of an entity with a given ID.
     * </caption>
     * var entity = Abcs.Entities().findById('my.custom.bop.Employee');
     *
     * @see {@link module:api/js/Entities}
     */
    var Entity = function (model, entityProvider, baseModel) {
        var self = this;
        AbcsLib.checkThis(self);
        model = Entity._normalizeModel(model);
        if (baseModel) {
            AbcsLib.checkDefined(baseModel.id, 'baseModel.id');
        } else if (model) {
            AbcsLib.checkDefined(model.id, 'model.id');
        }

        var _entityProvider;
        if (!entityProvider && model.entityProvider) {
            // Support legacy code passing in entityProvider as part of the model.
            _entityProvider = model.entityProvider;
            delete model.entityProvider;
        } else {
            _entityProvider = entityProvider;
        }

        /**
         * Returns entity provider used by this entity.
         *
         * @returns {String}
         */
        this.getEntityProvider = function () {
            return _entityProvider;
        };

        /**
         * Sets the owning {@link bop/js/spi/EntityProvider} for this Entity instance.
         *
         * <p>
         * BE AWARE:
         * This is an infrastructure method which is supposed to be called only by EntityProviders. The reason why it's here
         * is to avoid each Entity instance accepting EP instance in it's constructor. Since every Entity that could possibly
         * live inside Abcs DataModel should be registered using EP, this should be ensured. But if you think about calling
         * this method from somewhere else, it's very suspicious and you should consult such change with the platform team first.
         * </p>
         *
         * @param {bop/js/spi/EntityProvider} entityProvider
         */
        this.setEntityProvider = function(entityProvider) {
            _entityProvider = entityProvider;
        };

        Attributed.call(this, model, undefined, baseModel);
    };

    AbcsLib.extend(Entity, Attributed);

    Entity._LOGGER = Logger.get('entity/js/api/Entity');

    Entity._KEY_ID = 'id';
    Entity._KEY_LOOKUP = 'lookup';
    Entity._KEY_HIDDEN = 'hidden';
    Entity._KEY_PROPERTIES = 'properties';
    Entity._KEY_RELATIONS = 'relations';
    Entity._KEY_SECURITY = 'security';
    Entity._KEY_STANDALONE = 'standalone';
    Entity._KEY_TRIGGERS = 'triggers';
    Entity._KEY_VALIDATORS = 'validators';
    Entity._KEY_OBJECT_FUNCTIONS = 'objectFunctions';
    Entity._KEY_ENTITY_AGGREGATION = 'entityAggregation';

    Entity._ATTRIBUTE_KEY_SINGULAR_NAME = 'singularName';
    Entity._ATTRIBUTE_KEY_PLURAL_NAME = 'pluralName';
    Entity._ATTRIBUTE_KEY_DESCRIPTION = 'description';
    Entity._ATTRIBUTE_EXT_SERVICE_FOR = 'extServiceFor';

    /**
     * Prefix for extention entity IDs to avoid them clashing with Dave's entities.
     */
    Entity._ATTRIBUTE_EXT_ENTITY_FOR = 'extEntityFor';

    /**
     * Needs to be used by all subclasses to avoid passing a String in to the
     * various super constructors.
     * @param {Object|String|function} model the model passed to constructor
     * @returns {Object|function} the model to pass on (ultimately to DataModelObject).
     */
    Entity._normalizeModel = function(model) {
        if (AbcsLib.isString(model)) {
            //the passed parameter is a string, which is interpreted as desired entity ID
            //convert to the common notation...
            model = {
                id: model
            };
        }
        return model;
    };

    /**
     * For known child lists, return the constructor key that is passed to
     * Implementations.createInstance to wrap the underlying model.
     * @override
     * @param {String} key
     * @returns {string}
     */
    Entity.prototype.getChildConstructorKey = function(key) {
        switch (key) {
            case Entity._KEY_PROPERTIES :
                return 'Property';
            case Entity._KEY_RELATIONS :
                return 'Relation';
            default :
                return undefined;
        }
    };

    /**
     * For known child lists, return the key that provides an "identity" for
     * uniqueness within siblings.
     * @override
     * @param {String} key
     * @returns {string}
     */
    Entity.prototype.getChildIdentityKey = function(key) {
        switch (key) {
            case Entity._KEY_PROPERTIES :
                return Property._KEY_ID;
            case Entity._KEY_RELATIONS :
                return Relation._KEY_MAPPING_PROPERTY_ID;
            default :
                return undefined;
        }
    };

    /**
     * Gets a map with the translatable attribute info for Entity.
     * @override
     * @returns {Object} model key -> I18n key.
     */
    Entity.prototype.getTranslatableAttributes = function() {
        var map = {};
        map[Entity._ATTRIBUTE_KEY_SINGULAR_NAME]
                = I18n.key(I18n.Type.Entity, this.getId(), Entity._ATTRIBUTE_KEY_SINGULAR_NAME);
        map[Entity._ATTRIBUTE_KEY_PLURAL_NAME]
                = I18n.key(I18n.Type.Entity, this.getId(), Entity._ATTRIBUTE_KEY_PLURAL_NAME);
        map[Entity._ATTRIBUTE_KEY_DESCRIPTION]
                = I18n.key(I18n.Type.Entity, this.getId(), Entity._ATTRIBUTE_KEY_DESCRIPTION);
        return map;
    };

    /**
     * Finds out if this entity has an extension entity or not.
     *
     * <p>
     * Extension entity exists when an External BO is extended with a custom field. In such case
     * ABCS creates an extension entity which lies directly in our own DB and it always holds ID
     * of the original entity so that they can be mashed-up.
     * </p>
     *
     * @returns {Boolean}
     */
    Entity.prototype.hasExtEntity = function () {
        return this._extEntity;
    };

    /**
     * Gets the extension entity for this entity.
     *
     * <p>
     * Extension entity exists when an External BO is extended with a custom field. In such case
     * ABCS creates an extension entity which lies directly in our own DB and it always holds ID
     * of the original entity so that they can be mashed-up.
     * </p>
     *
     * @returns {Entity}
     */
    Entity.prototype.getExtEntity = function () {
        return this._extEntity;
    };

    /**
     * Checks whether this entity is standalone or not.
     *
     * <p>
     * Standalone means that entity can live without any context (thus can be drop to an empty
     * page). If an entity is not-standalone, it means that it has some data restriction which
     * limits it's usage only to places where those restrictions are available. E.g. if an entity
     * does provide just one operation with a mandatory input parameter, it can be placed only to
     * a page which has context information with that input parameter value available.
     * </p>
     *
     * @returns {Boolean}
     */
    Entity.prototype.isStandalone = function () {
        var relations = this.getOwnRelations();

        // If there is at least one N-1 relation-ship where this entity exists as the source entity (holding the real value),
        // it means the entity exists as a Child in Parent-Child relation-ship in which case it's considered as not-standalone
        for (var i = 0; i < relations.length; i++) {
            var relation = relations[i];
            if (relation.isChildRelationship()) {
                var cardinality = relation.getCardinality();
                var sourceEntity = relation.getSourceEntity();
                if (sourceEntity === this && cardinality === RelationCardinality.MANY_TO_ONE) {
                    return false;
                }
            }
        }

        // In a normal world, we would just return "true" here. But in ABCS, there is always some of FA which in this case
        // don't describe their Relations the same way as any normal BO, making the algorithm above to fail for them. Because
        // of that, I'm making this fall-back in case the above code didn't recognize child BO and once the FA part is fixed,
        // we can change this call to just return "true".
        return DataModel.getInstance().operations().isStandalone(this);
    };

    /**
     * Checks whether this <code>Entity</code> is hidden or not.
     *
     * @returns {Boolean}
     */
    Entity.prototype.isHidden = function () {
        return !!this.getModelValue(Entity._KEY_HIDDEN);
    };

    /**
     * Does this object represent a simple lookup entity?
     */
    Entity.prototype.isLookupEntity = function () {
        return !!this.getModelValue(Entity._KEY_LOOKUP);
    };

    /**
     * Internal type of entity which is not accessible directly.
     */
    Entity.prototype._isExtensionEntity = function () {
        return this.getAttribute(Entity._ATTRIBUTE_EXT_ENTITY_FOR) !== undefined;
    };

    /**
     * Finds out if this entity is an internal one or the external one.
     * Returns {@constant true} in case of internal one.
     *
     * @returns {Boolean}
     */
    Entity.prototype.isInternal = function () {
        return EntityProviders.isInternal(this.getEntityProvider());
    };

    /**
     * Returns the entity's unique identifier.
     *
     * @AbcsAPI stable
     * @version 15.4.5
     *
     * @returns {String} entity identifier
     */
    Entity.prototype.getId = function () {
        return this.getModelValue(Entity._KEY_ID);
    };

    /**
     * Gets human readable singular name of this entity, e.g. 'Customer'
     *
     * @AbcsAPI stable
     * @version 15.4.5
     *
     * @returns {String}
     */
    Entity.prototype.getSingularName = function () {
        return this.getAttributeAsString(Entity._ATTRIBUTE_KEY_SINGULAR_NAME);
    };

    /**
     * Gets human readable singular name of this entity, e.g. 'Customer'
     *
     * @deprecated use {@link #getSingularName} instead.
     * @returns {String}
     */
    Entity.prototype.getName = Entity.prototype.getSingularName;

    /**
     * Gets human readable plural name of this entity, e.g. 'Customers'
     *
     * @AbcsAPI stable
     * @version 15.4.5
     *
     * @returns {String}
     */
    Entity.prototype.getPluralName = function () {
        return this.getAttributeAsString(Entity._ATTRIBUTE_KEY_PLURAL_NAME);
    };

    /**
     * Gets human readable description of this entity. e.g. 'List of our external customers'
     *
     * @AbcsAPI stable
     * @version 15.4.5
     *
     * @returns {String}
     */
    Entity.prototype.getDescription = function () {
        return this.getAttributeAsString(Entity._ATTRIBUTE_KEY_DESCRIPTION);
    };

    /**
     * Returns key property of this entity or undefined if no key
     * property is defined for this entity.
     *
     * @todo does each entity has to have a "key"? Isn't this just too close to the DB nature?
     * @returns {Property}
     */
    Entity.prototype.getKeyProperty = function () {
        var entityAggregation = this.getModelValue(Entity._KEY_ENTITY_AGGREGATION);
        if (entityAggregation) {
            // Workaround for aggregation, to be update once BUFP-13040 is done.
            var groupBy = entityAggregation.groupBy;
            if (groupBy) {
                return this.getProperty(groupBy[0].propertyId);
            }
        } else {
            return this.findChild(Entity._KEY_PROPERTIES, function(property) {
                return property.isKey();
            });
        }
    };

    /**
     * Returns list of assotiated properties the entity owns.
     *
     * @AbcsAPI stable
     * @version 15.4.5
     *
     * @returns {entity/js/api/Property[]} list of assotiated properties
     *
     * @example
     * <caption>
     *  Prints all properties of an entity stored in variable <code>entity</code>.
     * </caption>
     * console.log('Entity ' + entity.getId() + ' has the following properties:');
     * entity.getProperties().forEach(function (property) {
     *     console.log(property.getId() + ' with name ' + property.getSingularName());
     * };
     */
    Entity.prototype.getProperties = function (skipExtEntities, includeIgnoredProperties) {
        var result = [];
        this.forEachChild(Entity._KEY_PROPERTIES, function (property) {
            if (includeIgnoredProperties
                    || property.getClassification() !== PropertyClassification.IGNORE) {
                result.push(property);
            }
        });

        if (this.hasExtEntity() && !skipExtEntities) {
            Array.prototype.push.apply(result, this.getExtEntity().getProperties(false, includeIgnoredProperties));
        }
        if (!includeIgnoredProperties) {
            return result;
        }
        return result;
    };

    /**
     * Checks whether this entity has at least one property with classification type BASIC.
     *
     * @returns {boolean}
     */
    Entity.prototype.hasBasicProperties = function () {
        return this.getBasicProperties().length > 0;
    };

    /**
     * Returns list of associated formula {@link Property}.
     *
     * @returns {Property[]}
     */
    Entity.prototype.getFormulas = function () {
        var formulas = [];
        this.getProperties().forEach(function (property) {
            if (property.getFormula()) {
                formulas.push(property);
            }
        });
        return formulas;
    };

    /**
     * Checks whether this entity has at least one formula or not.
     *
     * @returns {Boolean}
     */
    Entity.prototype.hasFormulas = function () {
        return this.getFormulas().length > 0;
    };

    /**
     * Returns list of properties which has classification type BASIC.
     *
     * @returns {Property[]} list of basic properties
     */
    Entity.prototype.getBasicProperties = function () {
        var props = [];
        this.getProperties().forEach(function (property) {
            if (property.getClassification() === PropertyClassification.BASIC) {
                props.push(property);
            }
        });
        return props;
    };

    /**
     * Return entity's property identified by the given ID - ignoring the case.
     *
     * @AbcsAPI stable
     * @version 15.4.5
     *
     * @param {String} id - the ID of the property
     * @returns {entity/js/api/Property}
     */
    Entity.prototype.getProperty = function (id, skipExtEntities) {
        var result;
        if (id) {
            result = this.findChildByIdentity(Entity._KEY_PROPERTIES, id);
            if (!result && !skipExtEntities && this.hasExtEntity()) {
                result = this.getExtEntity().getProperty(id, false);
            }
        }
        return result;
    };

    /**
     * Returns list of all entity's relations.
     *
     * @param {Boolean} skipExtEntities exclude relations from extension entity
     * @returns {entity/js/api/Relation[]} list of entity relations.
     */
    Entity.prototype.getRelations = function (skipExtEntities) {
        return this.getOwnRelations(skipExtEntities).concat(this.getForeignRelations());
    };

    /**
     * Returns list of entity relations owned by this entity.
     * @param {Boolean} skipExtEntities exclude relations from extension entity
     * @returns {Relation[]} list of entity relations.
     */
    Entity.prototype.getOwnRelations = function (skipExtEntities) {
        var main = this.getChildList(Entity._KEY_RELATIONS).toArray();
        if (this.hasExtEntity() && !skipExtEntities) {
            return main.concat(this.getExtEntity().getChildList(Entity._KEY_RELATIONS).toArray());
        }
        return main;
    };

    /**
     * Gets a relation which is using the given {@link Property}.
     *
     * <p>
     * That means, it will find a relation which has mappingProperty the same as the passed property.
     * </p>
     *
     * @param {Property} property
     * @returns {Relation} relation corresponding to the given property or null if it doesn't exist
     */
    Entity.prototype.getRelation = function (property) {
        var rel;
        if (property) {
            rel = this.findChildByIdentity(Entity._KEY_RELATIONS, property.getId());
            if (!rel && this.hasExtEntity()) {
                rel = this.getExtEntity().getRelation(property);
            }
        }
        return rel;
    };

    /**
     * Gets an array of all relations established between this and the given entity.
     *
     * @param {entity/js/api/Entity} entity
     * @returns {entity/js/api/Relation[]} - Array of relations established between this and the given entity.
     */
    Entity.prototype.getRelationsForEntity = function (entity) {
        var result = [];
        if (entity) {
            result = this.getOwnRelations().filter(function(relation) {
                return relation.getTargetEntityID() === entity.getId();
            });
        }
        return result;
    };

    /**
     * Gets an existing {@link Relation} to the given {@link Entity}.
     *
     * @param {Entity} entity
     * @returns {Relation} existing {@link Relation} or undefined if there's any
     */
    Entity.prototype.findRelationToEntity = function (/*entity*/) {
        var self = this;
        var relations = self.getOwnRelations();
        for (var i = 0; i < relations.length; i++) {
            var relation = relations[i];
            if (relation.getTargetEntityID() === self.getId()) {
                return relation;
            }
        }
    };

    /**
     * Returns list of entity relations owned by other entities where this entity is a member of the relation.
     *
     * TBD: Fix the naming? - getComputedRelations()?
     *
     * @todo possibly use better mechanism than the search over all entities.
     *
     * @returns {Relation[]} list of entity relations.
     */
    Entity.prototype.getForeignRelations = function () {
        var self = this;
        var relations = [];

        //FIXME, grrr ughly hack for tests
        var dataModel;

        var entityProvider = self.getEntityProvider();
        if (entityProvider) {
            if (entityProvider.getDataModel) {
                dataModel = entityProvider.getDataModel();
            }
        }
        if (!dataModel) {
            dataModel = DataModel.getInstance();
        }

        dataModel.getEntities().getEntities().forEach(function (entity) {
            entity.getOwnRelations().forEach(function (relation) {
// HOTIFIX for BUFP-406; TODO: XXX: for Marek to double check
//                if(relation.getTargetEntity() === self) {
                if (relation.getTargetEntity() && relation.getTargetEntity().getId() === self.getId()) {
                    //create inverse relation
                    try {
                        relations.push(Implementations.createInstance('Relation', {
                            sourceEntityId: relation.getTargetEntity().getId(),
                            targetEntityId: relation.getSourceEntity().getId(),
                            cardinality: RelationCardinality.getInverseCardinality(relation.getCardinality()),
                            mappingPropertyId: relation.getMappingPropertyID(),
                            targetPropertyType: relation.getTargetPropertyType(),
                            isForeign: true /* isForeign === true */,
                            defaultDisplayNameProperty: relation.getDefaultDisplayProperty(),
                            childRelationship: relation.isChildRelationship(),
                            deleteRule: relation.getDeleteRule(),
                            accessorId: relation.getReverseAccessorID(),
                            foreignRelations: {
                                $reverse: {
                                    accessorId: relation.getAccessorID()
                                }
                            }
                        }, self));
                    } catch (e) {
                        Entity._LOGGER.error('failed to create foreign Relation object for relation ' + AbcsLib.stringify(relation), e);
                        throw e;
                    }

                    var mappingProperty = relation.getMappingProperty();
                    if (relation.getCardinality() === RelationCardinality.MANY_TO_ONE &&
                        mappingProperty && mappingProperty.isRequired()) {
                        var intersectionBO = relation.getSourceEntity();
                        var ownRels = intersectionBO.getOwnRelations();
                        var numRels = ownRels.length;
                        if (numRels > 1) {
                            for (var r2 = 0; r2 < numRels; r2++) {
                                var rel2 = ownRels[r2];
                                var sourceEntityId = self.getId();
                                var targetEntityId = rel2.getTargetEntityID();
                                if (rel2.getCardinality() === RelationCardinality.MANY_TO_ONE &&
                                    (sourceEntityId !== targetEntityId || rel2.getMappingPropertyID() > mappingProperty.getId())) { // by testing not only !== but also > if target BO is the same, we avoid "dups" in the list
                                    var prop2 = rel2 && rel2.getMappingProperty();
                                    if (prop2 && prop2.isRequired()) {
                                        //create M:M
                                        try {
                                            var mmRel = Implementations.createInstance('Relation', {
                                                sourceEntityId: sourceEntityId,
                                                targetEntityId: targetEntityId,
                                                cardinality: RelationCardinality.MANY_TO_MANY,
                                                isForeign: true,
                                                intersectionEntityId: intersectionBO.getId(),
                                                intersectionPropertyIds: [mappingProperty.getId(), prop2.getId()],
                                                accessorId: relation.getForeignRelationValue(prop2.getId(), 'accessorId'),
                                                foreignRelations: {
                                                    $reverse: {
                                                        accessorId: rel2.getForeignRelationValue(mappingProperty.getId(), 'accessorId')
                                                    }
                                                }
                                            });
                                            relations.push(mmRel);
                                        } catch (e) {
                                            Entity._LOGGER.error('failed to create foreign Relation object for relation ' + AbcsLib.stringify(relation), e);
                                            throw e;
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            });
        });

        return relations;

    };

    /**
     * Get EntityAggregation information for this entity.
     *
     * @returns {Object} The entityAggregation object.
     */
    Entity.prototype.getEntityAggregation = function() {
        return AbcsLib.clone(this.getModelValue(Entity._KEY_ENTITY_AGGREGATION));
    };

    /**
     * Set EntityAggregation information on the entity, making it an entity
     * that produces aggregation data over other entities.
     *
     * @param {Object} entityAggregation
     * @returns {undefined}
     */
    Entity.prototype.setEntityAggregation = function(entityAggregation) {
        this.setModelValue(Entity._KEY_ENTITY_AGGREGATION, entityAggregation);
    };

    /**
     * Returns the entity's Security Settings.
     *
     * @returns {Object} Security Settings Object
     */
    Entity.prototype.getAccessControlSettings = function () {
        // Until there is a child object representing security (which can
        // fire change events) return a clone to force modifiers to call
        // setAccessControlSettings.
        return AbcsLib.clone(this.getModelValue(Entity._KEY_SECURITY));
    };

    /**
     * Returns list of triggers the entity owns.
     *
     * @returns {Object[]} list of triggers
     */
    Entity.prototype.getTriggers = function () {
        // Until there is a child object representing triggers (which can
        // fire change events) return a clone to force modifiers to call
        // setTriggers.
        return AbcsLib.clone(this.getModelValue(Entity._KEY_TRIGGERS));
    };

    /**
     * Returns list of validators the entity owns.
     *
     * @returns {Object[]} list of validators
     */
    Entity.prototype.getValidators = function () {
        // Until there is a child object representing triggers (which can
        // fire change events) return a clone to force modifiers to call
        // setValidators.
        return AbcsLib.clone(this.getModelValue(Entity._KEY_VALIDATORS));
    };

    /**
     * Returns list of object functions the entity owns.
     *
     * @returns {Object[]} list of object functions
     */
    Entity.prototype.getObjectFunctions = function () {
        // Until there is a child object representing objectFunctions (which can
        // fire change events) return a clone to force modifiers to call
        // setObjectFunctions.
        return AbcsLib.clone(this.getModelValue(Entity._KEY_OBJECT_FUNCTIONS));
    };

    return Entity;
});