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