'use strict';
//region Imports
var util = require('util');
var Argument = require('./system/argument-check.js');
var CollectionBase = require('./collection-base.js');
var ModelError = require('./shared/model-error.js');
var MODEL_STATE = require('./shared/model-state.js');
var CLASS_NAME = 'EditableChildCollection';
//endregion
/**
* Factory method to create definitions of asynchronous editable child collections.
*
* Valid collection item types are:
*
* * EditableChildModel
*
* @function bo.EditableChildCollection
* @param {string} name - The name of the collection.
* @param {EditableChildModel} itemType - The model type of the collection items.
* @returns {EditableChildCollection} The constructor of an asynchronous editable child collection.
*
* @throws {@link bo.system.ArgumentError Argument error}: The collection name must be a non-empty string.
* @throws {@link bo.shared.ModelError Model error}: The item type must be an EditableChildModel.
*/
var EditableChildCollectionFactory = function (name, itemType) {
name = Argument.inConstructor(CLASS_NAME)
.check(name).forMandatory('name').asString();
// Check tree reference.
if (typeof itemType !== 'string') {
// Verify the model type of the item type.
if (itemType.modelType !== 'EditableChildModel')
throw new ModelError('invalidItem',
itemType.prototype.name, itemType.modelType,
CLASS_NAME, 'EditableChildModel');
}
/**
* @classdesc
* Represents the definition of an asynchronous editable child collection.
* @description
* Creates a new asynchronous editable child collection instance.
*
* _The name of the model type available as:
* __<instance>.constructor.modelType__, returns 'EditableChildCollection'._
*
* Valid parent model types are:
*
* * EditableRootModel
* * EditableChildModel
*
* @name EditableChildCollection
* @constructor
* @param {object} parent - The parent business object.
* @param {bo.shared.EventHandlerList} [eventHandlers] - The event handlers of the instance.
*
* @extends CollectionBase
*
* @throws {@link bo.system.ArgumentError Argument error}:
* The parent object must be an EditableRootModel or EditableChildModel instance.
*/
var EditableChildCollection = function (parent, eventHandlers) {
CollectionBase.call(this);
// Verify the model type of the parent model.
parent = Argument.inConstructor(name).check(parent).for('parent').asModelType([
'EditableRootModel',
'EditableChildModel'
]);
// Resolve tree reference.
if (typeof itemType === 'string') {
if (itemType === parent.$modelName)
itemType = parent.constructor;
else
throw new ModelError('invalidTree', itemType, parent.$modelName);
}
var self = this;
var items = [];
/**
* The name of the model.
*
* @name EditableChildCollection#$modelName
* @type {string}
* @readonly
*/
this.$modelName = name;
/**
* The count of the child objects in the collection.
*
* @name EditableChildCollection#count
* @type {number}
* @readonly
*/
Object.defineProperty(self, 'count', {
get: function () {
return items.length;
},
enumerable: false
});
//region Transfer object methods
/**
* Transforms the business object collection to a plain object array to send to the client.
* <br/>_This method is usually called by the parent object._
*
* @function EditableChildCollection#toCto
* @returns {Array.<object>} The client transfer object.
*/
this.toCto = function () {
var cto = [];
items.forEach(function (item) {
cto.push(item.toCto());
});
return cto;
};
/**
* Rebuilds the business object collection from a plain object array sent by the client.
* <br/>_This method is usually called by the parent object._
*
* @function EditableChildCollection#fromCto
* @param {Array.<object>} data - The array of client transfer objects.
* @param {external.cbFromCto} callback - Returns the eventual error.
*/
this.fromCto = function (data, callback) {
if (!(data instanceof Array))
callback(null);
var dataNew = data.filter(function () { return true; });
var itemsLive = [];
var itemsLate = [];
var count = 0;
var error = null;
var index;
function finish() {
if (++count == dataNew.length) {
return callback(error);
}
}
function handleOldNew() {
count = 0;
// Remove non existing items.
for (index = 0; index < itemsLate.length; index++) {
items[itemsLate[index]].remove();
}
// Insert non existing data.
if (dataNew.length) {
dataNew.forEach(function (cto) {
itemType.create(parent, eventHandlers, function (err, item) {
if (err) {
error = error || err;
finish();
} else {
item.fromCto(cto, function (err) {
if (err)
error = error || err;
else
items.push(item);
finish();
});
}
});
});
} else
return callback(null);
}
// Discover changed items.
for (index = 0; index < items.length; index++) {
var dataFound = false;
var i = 0;
for (; i < dataNew.length; i++) {
if (items[index].keyEquals(data[i])) {
itemsLive.push({ item: index, cto: i });
dataFound = true;
break;
}
}
if (dataFound)
dataNew.splice(i, 1);
else
itemsLate.push(index);
}
// Update existing items.
if (itemsLive.length)
for (index = 0; index < itemsLive.length; index++) {
var ix = itemsLive[index];
items[ix.item].fromCto(data[ix.cto], function (err) {
if (err)
error = error || err;
if (++count == itemsLive.length)
handleOldNew();
});
}
else
handleOldNew();
};
//endregion
//region Actions
/**
* Creates a new item and adds it to the collection at the specified index.
*
* @function EditableChildCollection#create
* @param {number} index - The index of the new item.
* @param {external.cbDataPortal} callback - Returns the newly created editable business object.
*/
this.createItem = function (index, callback) {
if (!callback) {
callback = index;
index = items.length;
}
itemType.create(parent, eventHandlers, function (err, item) {
if (err)
callback(err);
var ix = parseInt(index, 10);
ix = isNaN(ix) ? items.length : ix;
items.splice(ix, 0, item);
callback(null, item);
});
};
/**
* Initializes the items in the collection with data retrieved from the repository.
*
* _This method is called by the parent object._
*
* @function EditableChildCollection#fetch
* @protected
* @param {Array.<object>} [data] - The data to load into the business object collection.
* @param {external.cbDataPortal} callback - Returns the eventual error.
*/
this.fetch = function (data, callback) {
if (data instanceof Array && data.length) {
var count = 0;
var error = null;
data.forEach(function (dto) {
itemType.load(parent, dto, eventHandlers, function (err, item) {
if (err)
error = error || err;
else
items.push(item);
// Check if all items are done.
if (++count === data.length) {
callback(error);
}
});
});
} else
callback(null);
};
/**
* Saves the changes of the business object collection to the repository.
*
* _This method is called by the parent object._
*
* @function EditableChildCollection#save
* @protected
* @param {object} connection - The connection data.
* @param {external.cbDataPortal} callback - Returns the eventual error.
*/
this.save = function (connection, callback) {
var count = 0;
var error = null;
if (items.length) {
items.forEach(function (item) {
item.save(connection, function (err) {
error = error || err;
// Check if all items are done.
if (++count === items.length) {
items = items.filter(function (item) {
return item.getModelState() !== MODEL_STATE.getName(MODEL_STATE.removed);
});
callback(error);
}
});
});
} else
callback(null);
};
/**
* Marks all items in the collection to be deleted from the repository on next save.
*
* @function EditableChildCollection#remove
*/
this.remove = function () {
items.forEach(function (item) {
item.remove();
});
};
/**
* Indicates whether all items of the business collection are valid.
*
* _This method is called by the parent object._
*
* @function EditableChildCollection#isValid
* @protected
* @returns {boolean}
*/
this.isValid = function () {
return items.every(function (item) {
return item.isValid();
});
};
/**
* Executes validation on all items of the collection.
*
* _This method is called by the parent object._
*
* @function EditableChildCollection#checkRules
* @protected
*/
this.checkRules = function () {
items.forEach(function (item) {
item.checkRules();
});
};
/**
* Gets the broken rules of all items of the collection.
*
* _This method is called by the parent object._
*
* @function EditableChildCollection#getBrokenRules
* @protected
* @param {string} [namespace] - The namespace of the message keys when messages are localizable.
* @returns {Array.<bo.rules.BrokenRulesOutput>} The broken rules of the collection.
*/
this.getBrokenRules = function(namespace) {
var bro = [];
items.forEach(function (item) {
var childBrokenRules = item.getBrokenRules(namespace);
if (childBrokenRules)
bro.push(childBrokenRules);
});
return bro.length ? bro : null;
};
//endregion
//region Public array methods
/**
* Gets a collection item at a specific position.
*
* @function EditableChildCollection#at
* @param {number} index - The index of the required item in the collection.
* @returns {EditableChildModel} The required collection item.
*/
this.at = function (index) {
return items[index];
};
/**
* Executes a provided function once per collection item.
*
* @function EditableChildCollection#forEach
* @param {external.cbCollectionItem} callback - Function that produces an item of the new collection.
*/
this.forEach = function (callback) {
items.forEach(callback);
};
/**
* Tests whether all items in the collection pass the test implemented by the provided function.
*
* @function EditableChildCollection#every
* @param {external.cbCollectionItem} callback - Function to test for each collection item.
* @returns {boolean} True when callback returns truthy value for each item, otherwise false.
*/
this.every = function (callback) {
return items.every(callback);
};
/**
* Tests whether some item in the collection pass the test implemented by the provided function.
*
* @function EditableChildCollection#some
* @param {external.cbCollectionItem} callback - Function to test for each collection item.
* @returns {boolean} True when callback returns truthy value for some item, otherwise false.
*/
this.some = function (callback) {
return items.some(callback);
};
/**
* Creates a new array with all collection items that pass the test
* implemented by the provided function.
*
* @function EditableChildCollection#filter
* @param {external.cbCollectionItem} callback - Function to test for each collection item.
* @returns {Array.<EditableChildModel>} The new array of collection items.
*/
this.filter = function (callback) {
return items.filter(callback);
};
/**
* Creates a new array with the results of calling a provided function
* on every item in this collection.
*
* @function EditableChildCollection#map
* @param {external.cbCollectionItem} callback - Function to test for each collection item.
* @returns {Array.<*>} The new array of callback results.
*/
this.map = function (callback) {
return items.map(callback);
};
/**
* Sorts the items of the collection in place and returns the collection.
*
* @function EditableChildCollection#sort
* @param {external.cbCompare} [fnCompare] - Function that defines the sort order.
* If omitted, the collection is sorted according to each character's Unicode
* code point value, according to the string conversion of each item.
* @returns {EditableChildCollection} The sorted collection.
*/
this.sort = function (fnCompare) {
return items.sort(fnCompare);
};
//endregion
// Immutable object.
Object.freeze(this);
};
util.inherits(EditableChildCollection, CollectionBase);
/**
* The name of the model type.
*
* @property {string} EditableChildCollection.constructor.modelType
* @default EditableChildCollection
* @readonly
*/
Object.defineProperty(EditableChildCollection, 'modelType', {
get: function () { return CLASS_NAME; }
});
return EditableChildCollection;
};
module.exports = EditableChildCollectionFactory;