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;
});