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

Source: entity/js/api/Relation.js

define([
    'entity/js/api/DataModel',
    'entity/js/api/RelationCardinality',
    'entity/js/api/RelationDeleteRule',
    'entity/js/spi/DataModelObject'
], function (
        DataModel,
        RelationCardinality,
        RelationDeleteRule,
        DataModelObject
        ) {

    'use strict';

    /**
     * Describes a relation between two {@link entity/js/api/Entity Entities}.
     *
     * <p>
     * The source {@link entity/js/api/Entity Entity} is the one which owns this <code>Relation</code> object and whose record always holds a reference to the
     * target {@link entity/js/api/Entity Entity}. The relation also describes its {@link entity/js/api/RelationCardinality RelationCardinality}.
     * </p>
     *
     * <p>
     * If the cardinality is {@link entity/js/api/RelationCardinality.MANY_TO_ONE RelationCardinality.MANY_TO_ONE}, then it means that the source {@link entity/js/api/Entity Entity}
     * owns the mapping relation {@link entity/js/api/Property Property} (which has always type set to {@link entity/js/api/PropertyType.REFERENCE PropertyType.REFERENCE}). If the
     * cardinality is {@link entity/js/api/RelationCardinality.ONE_TO_MANY RelationCardinality.ONE_TO_MANY}, the target {@link entity/js/api/Entity Entity} owns the mapping relation
     * {@link entity/js/api/Property Property}.
     * </p>
     *
     * @AbcsExtension stable
     * @exports entity/js/api/Relation
     * @version 17.1.1
     *
     * @constructor
     * @private
     *
     * @param {Object|function} model object literal w/ following properties:     *
     * @param {string} model.sourceEntityId
     * @param {string} model.targetEntityId
     * @param {EntityCardinality} model.cardinality
     * @param {string} model.mappingPropertyId
     * @param {boolean} model.isForeign
     * @param {string} model.defaultDisplayNameProperty
     * @param {boolean} model.childRelationship marks if this relation is a parent/child
     * relation, which has slighly different meaning and behaves differently from the
     * normal relation.
     * @param {Entity} entity the parent entity
     * @param {Object} baseModel the base model
     *
     * @returns {entity/js/api/Relation}
     *
     * @see {@link entity/js/api/Entity Entity} representing an entity object
     * @see {@link entity/js/api/RelationCardinality RelationCardinality} listing all possible cardinalitites
     * @see {@link bop/js/api/entity/DataModelFactory#createRelation DataModelFactory.createRelation(..)} to understand how instances of <code>Relation</code> are created
     */
    var Relation = function (model, entity, baseModel) {
        AbcsLib.checkThis(this);

        if (baseModel) {
            Relation._checkModel(baseModel);
        } else {
            Relation._checkModel(model);

            // legacy code might pass this in, and we don't want it in the model
            delete model.dataModel;
        }

        DataModelObject.call(this, model, entity, baseModel);
    };

    AbcsLib.extend(Relation, DataModelObject);

    Relation._KEY_SOURCE_ENTITY_ID = 'sourceEntityId';
    Relation._KEY_TARGET_ENTITY_ID = 'targetEntityId';
    Relation._KEY_CARDINALITY = 'cardinality';
    Relation._KEY_MAPPING_PROPERTY_ID = 'mappingPropertyId';
    Relation._KEY_TARGET_PROPERTY_TYPE = 'targetPropertyType';
    Relation._KEY_IS_FOREIGN = 'isForeign';
    Relation._KEY_DEFAULT_DISPLAY_NAME_PROP = 'defaultDisplayNameProperty';
    Relation._KEY_CHILD_RELATIONSHIP = 'childRelationship';
    Relation._KEY_ACCESSOR_ID = 'accessorId';
    Relation._KEY_FOREIGN_RELATION_MAP = 'foreignRelations';
    Relation._KEY_DELETE_RULE = 'deleteRule';
    // 2 props only held on foreign M:M relations - should NEVER be persisted
    Relation._KEY_INTERSECTION_ENTITY_ID = 'intersectionEntityId';
    Relation._KEY_INTERSECTION_PROPERTY_IDS = 'intersectionPropertyIds';

    Relation.REVERSE = '$reverse';

    /**
     * Checks that an incoming model has any mandatory properties defined.
     * @param {Object} model the model to check
     */
    Relation._checkModel = function(model) {
        AbcsLib.checkDefined(model[Relation._KEY_SOURCE_ENTITY_ID], Relation._KEY_SOURCE_ENTITY_ID);
        AbcsLib.checkDefined(model[Relation._KEY_TARGET_ENTITY_ID], Relation._KEY_TARGET_ENTITY_ID);
        AbcsLib.checkDefined(model[Relation._KEY_CARDINALITY], Relation._KEY_CARDINALITY);

        switch (model[Relation._KEY_CARDINALITY]) {
            case RelationCardinality.MANY_TO_ONE:
            case RelationCardinality.ONE_TO_MANY:
                AbcsLib.checkDefined(model[Relation._KEY_MAPPING_PROPERTY_ID], Relation._KEY_MAPPING_PROPERTY_ID);
                break;
            default:
                // no-op
        }
    };

    /**
     * Determines whether this relation is owned by the entity you got it from or
     * is a computed (foreign) relation where this entity represents the target
     * entity.
     *
     * Relation objects you get from Entity.getForeignRelations() function
     * will return true from this method.
     *
     * @AbcsAPI unstable
     * @returns {boolean}
     */
    Relation.prototype.isForeign = function () {
        return !!this.getModelValue(Relation._KEY_IS_FOREIGN);
    };

    /**
     *
     * @returns {string}
     */
    Relation.prototype.getDefaultDisplayProperty = function () {
        return this.getModelValue(Relation._KEY_DEFAULT_DISPLAY_NAME_PROP);
    };

    /**
     * Gets the relation {@link entity/js/api/RelationCardinality cardinality} of this <code>Relation<code>.
     *
     * @returns {entity/js/api/RelationCardinality}
     */
    Relation.prototype.getCardinality = function () {
        return this.getModelValue(Relation._KEY_CARDINALITY);
    };

    /**
     * Gets the relation {@link entity/js/api/RelationDeleteRule delete rule} of this <code>Relation<code>.
     *
     * @returns {entity/js/api/RelationDeleteRule}
     */
    Relation.prototype.getDeleteRule = function () {
        var deleteRule = this.getModelValue(Relation._KEY_DELETE_RULE);
        // for backward compatibility
        if (!deleteRule) {
            deleteRule = this.isChildRelationship() ? RelationDeleteRule.CASCADE : RelationDeleteRule.RESTRICT;
        }
        return deleteRule;
    };

    /**
     * Finds an entity with the given id.
     * @param {type} id the entity id to find
     * @returns {Entity} the entity if found
     */
    Relation.prototype._findOtherEntity = function(id) {
        var res;
        var dm = this._getDataModel();
        if (!dm) {
            dm = DataModel.getInstance();
        }

        if (dm) {
            res = dm.getEntities().getEntityById(id);
            if (!res) {
                // check if the id was from an extension entity
                dm.getEntities().getEntities().forEach(function(ent){
                    var ext = ent.getExtEntity();
                    if (ext && ext.getId() === id) {
                        res = ent;
                    }
                });
            }
        }
        return res;
    };

    /**
     * Gets the DataModel this Relation is part of. This uses the parent context
     * Entity.
     * @returns {DataModel} the data model, or null if there is no parent context.
     */
    Relation.prototype._getDataModel = function() {
        var res;
        var ent = this.getParentObject();
        if (ent) {
            var ep = ent.getEntityProvider();
            res = ep ? ep.getDataModel() : undefined;
        }
        return res;
    };

    /**
     * Gets the source {@link entity/js/api/Entity Entity} of this <code>Relation<code>.
     * <p>
     * NB If this Relation is a customisation of an external Business Object,
     * this will return the external Business Object entity, not the extension
     * entity that owns the Relation.  This is in contrast to the behaviour of
     * getSourceEntityId which returns the id of the owning extension entity.
     * Therefore getSourceEntityId() cannot be guaranteed to be the same as
     * getSourceEntity().getId().  See BUFP-9671 for more information.
     * </p>
     * @returns {entity/js/api/Entity}
     */
    Relation.prototype.getSourceEntity = function () {
        return this._findOtherEntity(this.getSourceEntityID());
    };

    /**
     * Gets the relation source entity ID.
     *
     * <p>
     * NB If this Relation is a customisation of an external Business Object,
     * this will return the id of the extension entity that owns the Relation,
     * not the external Business Object entity.  This is in contrast to the
     * behaviour of getSourceEntity which returns the external Business Object
     * entity.
     * Therefore getSourceEntityId() cannot be guaranteed to be the same as
     * getSourceEntity().getId().  See BUFP-9671 for more information.
     * </p>
     *
     * @returns {String}
     */
    Relation.prototype.getSourceEntityID = function () {
        return this.getModelValue(Relation._KEY_SOURCE_ENTITY_ID);
    };

    /**
     * Gets the target {@link entity/js/api/Entity Entity} of this <code>Relation<code>.
     *
     * @returns {entity/js/api/Entity}
     */
    Relation.prototype.getTargetEntity = function () {
        return this._findOtherEntity(this.getTargetEntityID());
    };

    /**
     * Gets the relation target entity ID.
     *
     * @AbcsAPI unstable
     * @returns {String}
     */
    Relation.prototype.getTargetEntityID = function () {
        return this.getModelValue(Relation._KEY_TARGET_ENTITY_ID);
    };

    /**
     * Whether this relation is child 1:N relationship. If false it means that
     * this is a regular reference.
     *
     * @returns {boolean}
     */
    Relation.prototype.isChildRelationship = function () {
        return this.getModelValue(Relation._KEY_CHILD_RELATIONSHIP);
    };

    /**
     * Return the PropertyType of the target property. Can be undefined which
     * means the target type is the default (NUMBER).
     *
     * @returns {entity/js/api/PropertyType}
     */
    Relation.prototype.getTargetPropertyType = function () {
        return this.getModelValue(Relation._KEY_TARGET_PROPERTY_TYPE);
    };

    /**
     * Gets the mapping property ID of this relation.
     *
     * @returns {String}
     */
     Relation.prototype.getMappingPropertyID = function() {
        return this.getModelValue(Relation._KEY_MAPPING_PROPERTY_ID);
    };

    /**
     * Gets the mapping property of this relation.
     *
     * @returns {Property}
     */
    Relation.prototype.getMappingProperty = function () {
        var sourceEntity = this.getSourceEntity();
        var targetEntity = this.getTargetEntity();
        var mappingPropertyID = this.getMappingPropertyID();
        switch (this.getCardinality()) {
            case RelationCardinality.MANY_TO_ONE:
                //source entity contains the mapping property
                return sourceEntity.getProperty(mappingPropertyID);

            case RelationCardinality.ONE_TO_MANY:
                //target entity contains the mapping property
                return targetEntity.getProperty(mappingPropertyID);

            case RelationCardinality.ONE_TO_ONE:
                // Do we know somehow who owns the relation in case of 1-1 ??
                // Until someone knows it, I'm checking both options
                var sourceMappingProperty = sourceEntity.getProperty(mappingPropertyID);
                if (sourceMappingProperty) {
                    return sourceMappingProperty;
                }
                var targetMappingProperty = targetEntity.getProperty(mappingPropertyID);
                if (targetMappingProperty) {
                    return targetMappingProperty;
                }
                break;

            case RelationCardinality.MANY_TO_MANY:
                // Just return the key of the source
                return sourceEntity.getKeyProperty();

            default:
                //return nothing
                break;
        }
    };

    /**
     * Checks whether the cardinality of this relation is Many-to-One.
     *
     * @returns {Boolean} - true if the cardinality of this relation is Many-to-One, false otherwise
     */
    Relation.prototype.isManyToOne = function () {
        return this.getCardinality() === RelationCardinality.MANY_TO_ONE;
    };

    /**
     * Checks whether the cardinality of this relation is One-to-Many.
     *
     * @returns {Boolean} - true if the cardinality of this relation is One-to-Many, false otherwise
     */
    Relation.prototype.isOneToMany = function () {
        return this.getCardinality() === RelationCardinality.ONE_TO_MANY;
    };

    /**
     * Gets the accessor ID of this relation.
     *
     * @returns {String}
     */
    Relation.prototype.getAccessorID = function() {
        return this.getModelValue(Relation._KEY_ACCESSOR_ID);
    };

    /**
     * Gets the reverse accessor ID of this relation.
     *
     * @returns {String}
     */
    Relation.prototype.getReverseAccessorID = function() {
        return this.getForeignRelationValue(Relation.REVERSE, Relation._KEY_ACCESSOR_ID);
    };

    /**
    * Foreign Relations are primarily derived from the existence of one or more
    * other Relationships.  For example, for each relationship between entities
    * E1 and E2 there will be a foreign relationship of the opposite cardinality
    * between E2 and E1.  In addition, if an Entity E3 has "required" Many to One
    * relationships to E1 and E2, there will be a derived Many to Many foreign
    * relationship between E1 and E2 (and another between E2 and E1).
    * If there is any metadata required to be persisted for these derived relationships
    * it is held in a map accessible from this function.
    * To get the correct map, use the key Relation.REVERSE_ACCESSOR for the
    * reverse relationship, or the property id of the "other" relation that contributes to
    * the many-to-many relationship.  Then also use the name of the property that
    * is required, for example "accessorId".
    * @param {String} key The key to identify the correct foreign relation (see function description).
    * @param {type} name The name of the property value to be returned.
    * @returns {Object} The required value if set or undefined.
    */
    Relation.prototype.getForeignRelationValue = function(key, name) {
        var map = this.getModelValue(Relation._KEY_FOREIGN_RELATION_MAP);
        return map && map[key] && map[key][name];
    };

    /**
    * The id of the intersection entity implementing this relationship.
    * This will only return a value for foreign relationships of cardinality
    * many-to-many.
    * @returns {String} intersection entity id
    */
    Relation.prototype.getIntersectionEntityID = function () {
        return this.getModelValue(Relation._KEY_INTERSECTION_ENTITY_ID);
    };

    /**
    * The ids of the two reference properties that are implementing this relationship.
    * This will only return a value for foreign relationships of cardinality
    * many-to-many.
    * @returns {String[]} Array of two property IDs
    */
    Relation.prototype.getIntersectionPropertyIds = function () {
        return this.getModelValue(Relation._KEY_INTERSECTION_PROPERTY_IDS);
    };

    return Relation;

});