'use strict';
var CLASS_NAME = 'PropertyManager';
var Argument = require('../system/argument-check.js');
var MethodError = require('../system/method-error.js');
var PropertyInfo = require('./property-info.js');
var DataType = require('../data-types/data-type.js');
var ModelError = require('./model-error.js');
/**
* @classdesc Provides methods to manage the properties of a business object model.
* @description Creates a new property manager object.
*
* @memberof bo.shared
* @constructor
* @param {string} name - The name of the business object model.
* @param {...bo.shared.PropertyInfo} [property] - Description of a model property.
*
* @throws {@link bo.system.ArgumentError Argument error}: The name must be a non-empty string.
* @throws {@link bo.system.ArgumentError Argument error}: The property must be PropertyInfo object.
*/
function PropertyManager (name /*, property1, property2 [, ...] */) {
var items = [];
var changed = false; // for children
var children = [];
var isFrozen = false;
var check = Argument.inConstructor(CLASS_NAME);
/**
* The name of the business object model.
* @type {string}
* @readonly
*/
this.name = check(name).forMandatory('name').asString();
Array.prototype.slice.call(arguments, 1)
.forEach(function (arg) {
items.push(check(arg).forMandatory().asType(PropertyInfo, 'properties'));
changed = true;
});
//region Item management
/**
* Adds a new property to the business object model.
*
* @param {bo.shared.PropertyInfo} property - Description of the model property to add.
*
* @throws {@link bo.system.ArgumentError Argument error}: The property must be PropertyInfo object.
* @throws {@link bo.shared.ModelError Model error}: Cannot change the definition after creation.
*/
this.add = function (property) {
if (isFrozen)
throw new ModelError('frozen', this.name);
items.push(Argument.inMethod(CLASS_NAME, 'add')
.check(property).forMandatory('property').asType(PropertyInfo));
changed = true;
};
/**
* Creates a new property for the business object model.
* </br></br>
* The data type can be any one from the {@link bo.dataTypes} namespace
* or a custom data type based on {@link bo.dataTypes.DataType DataType} object,
* or can be any business object model or collection defined by the
* model types available in the {@link bo} namespace (i.e. models based on
* {@link bo.ModelBase ModelBase} or {@link bo.CollectionBase CollectionBase}
* objects).
* </br></br>
* The flags parameter is ignored when data type is a model or collection.
*
* @param {string} name - The name of the property.
* @param {*} type - The data type of the property.
* @param {bo.shared.PropertyFlag} [flags] - Other attributes of the property.
* @returns {bo.shared.PropertyInfo} The definition of the property.
*
* @throws {@link bo.system.ArgumentError Argument error}: The name must be a non-empty string.
* @throws {@link bo.system.ArgumentError Argument error}: The type must be a data type, a model or a collection.
* @throws {@link bo.system.ArgumentError Argument error}: The flags must be PropertyFlag items.
* @throws {@link bo.shared.ModelError Model error}: Cannot change the definition after creation.
*/
this.create = function (name, type, flags) {
if (isFrozen)
throw new ModelError('frozen', this.name);
var property = new PropertyInfo(name, type, flags);
items.push(property);
changed = true;
return property;
};
/**
* Determines whether a property belongs to the business object model.
*
* @param {bo.shared.PropertyInfo} property - Property definition to be checked.
* @returns {boolean} True if the model contains the property, otherwise false.
*
* @throws {@link bo.system.ArgumentError Argument error}: The property must be PropertyInfo object.
*/
this.contains = function (property) {
property = Argument.inMethod(CLASS_NAME, 'contains')
.check(property).forMandatory('property').asType(PropertyInfo);
return items.some(function (item) {
return item.name === property.name;
});
};
/**
* Gets the property with the given name.
*
* @param {string} name - The name of the property.
* @param {string} [message] - The error message in case of not finding the property.
* @param {...*} [messageParams] - Optional interpolation parameters of the message.
* @returns {bo.shared.PropertyInfo} The requested property definition.
*
* @throws {@link bo.system.ArgumentError Argument error}: The name must be a non-empty string.
* @throws {@link bo.system.ArgumentError Argument error}: The business object has no property
* with the given name.
*/
this.getByName = function (name, message) {
name = Argument.inMethod(CLASS_NAME, 'getByName')
.check(name).forMandatory('name').asString();
for (var i = 0; i < items.length; i++) {
if (items[i].name === name)
return items[i];
}
throw new MethodError(message || 'noProperty', CLASS_NAME, 'getByName', 'name', this.name, name);
};
/**
* Gets the property definitions of the business object model as an array.
*
* @returns {Array.<bo.shared.PropertyInfo>} The array of model properties.
*/
this.toArray = function () {
var array = items.filter(function (item) {
return item.type instanceof DataType;
});
array.name = this.name;
return array;
};
//endregion
//region Public array methods
/**
* Executes the provided function once per property definition.
*
* @param {external.cbCollectionItem} callback - Function that produces an element
* of the model properties, taking three arguments: property, index, array.
*/
this.forEach = function (callback) {
items.forEach(callback);
};
/**
* Creates a new array with all properties that pass the test implemented by the provided function.
*
* @param {external.cbCollectionItem} callback - Function to test each element of the properties,
* taking three arguments: property, index, array.
* Return true to keep the property definition, false otherwise.
* @returns {Array.<bo.shared.PropertyInfo>} A new array with all properties that passed the test.
*/
this.filter = function (callback) {
return items.filter(callback);
};
/**
* Creates a new array with the results of calling a provided function
* on every element of the model properties.
*
* @param {external.cbCollectionItem} callback - Function that produces an element of the new array,
* taking three arguments: property, index, array.
* @returns {Array} A new array with items produced by the function.
*/
this.map = function (callback) {
return items.map(callback);
};
//endregion
//region Children
function checkChildren () {
if (changed) {
children = items.filter(function (item) {
return !(item.type instanceof DataType);
});
changed = false;
}
}
/**
* Gets the child models and collections of the current model.
*
* @returns {Array.<bo.shared.PropertyInfo>} - The array of the child properties.
*/
this.children = function () {
checkChildren();
return children;
};
/**
* Gets the count of the child models and collections of the current model.
*
* @returns {Number} The count of the child properties.
*/
this.childCount = function () {
checkChildren();
return children.length;
};
/**
* Verifies the model types of child models and freezes properties of the model.
*
* @param {Array.<string>} allowedTypes - The names of the model types of the allowed child models.
*
* @throws {@link bo.system.ArgumentError Argument error}: The allowed types must be
* an array of string values or a single string value.
* @throws {@link bo.shared.ModelError Model error}: The type of a model property
* should be an allowed type.
*/
this.verifyChildTypes = function (allowedTypes) {
allowedTypes = Argument.inMethod(CLASS_NAME, 'verifyChildTypes')
.check(allowedTypes).forMandatory('allowedTypes').asArray(String);
checkChildren();
var child;
for (var i = 0; i < children.length; i++) {
var matches = false;
child = children[i];
for (var j = 0; j < allowedTypes.length; j++) {
if (child.type.modelType == allowedTypes[j]) {
matches = true;
break;
}
}
if (!matches)
throw new ModelError('invalidChild',
this.name, child.name, child.type.modelType, allowedTypes.join(' | '));
}
isFrozen = true;
};
//endregion
//region Keys
/**
* Gets the key of the current model.
* </br></br>
* If the model has no key properties, the method returns the data transfer object,
* If the model has one key property, then it returns the current value of the that property.
* If the model has more key properties, an object will be returned whose properties will hold
* the current values of the key properties.
*
* @protected
* @param {internal~getValue} getPropertyValue - A function that returns
* the current value of the given property.
* @returns {*} The key value of the model.
*
* @throws {@link bo.system.ArgumentError Argument error}: The getPropertyValue argument must be a function.
*/
this.getKey = function (getPropertyValue) {
getPropertyValue = Argument.inMethod(CLASS_NAME, 'getKey')
.check(getPropertyValue).forMandatory('getPropertyValue').asFunction();
// No properties - no keys.
if (!items.length)
return undefined;
var key;
// Get key properties.
var keys = items.filter(function (item) {
return item.isKey;
});
// Evaluate result.
switch (keys.length) {
case 0:
// No keys: dto will be used.
key = {};
items.forEach(function (item) {
if (item.isOnDto)
key[item.name] = getPropertyValue(item);
});
break;
case 1:
// One key: key value will be used.
key = getPropertyValue(keys[0]);
break;
default:
// More keys: key object will be used.
key = {};
keys.forEach(function (item) {
key[item.name] = getPropertyValue(item);
});
}
return key;
};
/**
* Determines that the passed data contains current values of the model key.
*
* @protected
* @param {object} data - Data object whose properties can contain the values of the model key.
* @param {internal~getValue} getPropertyValue - A function that returns
* the current value of the given property.
* @returns {boolean} True when the values are equal, false otherwise.
*
* @throws {@link bo.system.ArgumentError Argument error}: The data argument must be an object.
* @throws {@link bo.system.ArgumentError Argument error}: The getPropertyValue argument must be a function.
*/
this.keyEquals = function (data, getPropertyValue) {
var check = Argument.inMethod(CLASS_NAME, 'keyEquals');
data = check(data).forMandatory('data').asObject();
getPropertyValue = check(getPropertyValue).forMandatory('getPropertyValue').asFunction();
// Get key properties.
var keys = items.filter(function (item) {
return item.isKey;
});
// Get key values.
var values = {};
if (keys.length) {
keys.forEach(function (item) {
values[item.name] = getPropertyValue(item);
});
} else {
items.forEach(function (item) {
if (item.isOnCto)
values[item.name] = getPropertyValue(item);
});
}
// Compare key values to data.
for (var propertyName in values) {
if (values.hasOwnProperty(propertyName)) {
if (data[propertyName] === undefined || data[propertyName] !== values[propertyName])
return false;
}
}
return true;
};
//endregion
// Immutable object.
Object.freeze(this);
}
module.exports = PropertyManager;