'use strict';
//region Imports
var util = require('util');
var config = require('./shared/configuration-reader.js');
var Argument = require('./system/argument-check.js');
var CollectionBase = require('./collection-base.js');
var ModelError = require('./shared/model-error.js');
var ExtensionManagerSync = require('./shared/extension-manager-sync.js');
var EventHandlerList = require('./shared/event-handler-list.js');
var TransferContext = require('./shared/transfer-context.js');
var RuleManager = require('./rules/rule-manager.js');
var BrokenRuleList = require('./rules/broken-rule-list.js');
var AuthorizationAction = require('./rules/authorization-action.js');
var AuthorizationContext = require('./rules/authorization-context.js');
var BrokenRulesResponse = require('./rules/broken-rules-response.js');
var DataPortalAction = require('./shared/data-portal-action.js');
var DataPortalContext = require('./shared/data-portal-context.js');
var DataPortalEvent = require('./shared/data-portal-event.js');
var DataPortalEventArgs = require('./shared/data-portal-event-args.js');
var DataPortalError = require('./shared/data-portal-error.js');
var CLASS_NAME = 'ReadOnlyRootCollectionSync';
var MODEL_DESC = 'Read-only root collection';
var M_FETCH = DataPortalAction.getName(DataPortalAction.fetch);
//endregion
/**
* Factory method to create definitions of synchronous read-only root collections.
*
* Valid collection item types are:
*
* * ReadOnlyChildModelSync
*
* @function bo.ReadOnlyRootCollectionSync
* @param {string} name - The name of the collection.
* @param {ReadOnlyChildModelSync} itemType - The model type of the collection items.
* @param {bo.shared.RuleManager} rules - The validation and authorization rules.
* @param {bo.shared.ExtensionManagerSync} extensions - The customization of the collection.
* @returns {ReadOnlyRootCollectionSync} The constructor of a synchronous read-only root collection.
*
* @throws {@link bo.system.ArgumentError Argument error}: The collection name must be a non-empty string.
* @throws {@link bo.system.ArgumentError Argument error}: The rules must be a RuleManager object.
* @throws {@link bo.system.ArgumentError Argument error}: The extensions must be a ExtensionManagerSync object.
* @throws {@link bo.shared.ModelError Model error}: The item type must be a ReadOnlyChildModelSync.
*/
var ReadOnlyRootCollectionSyncFactory = function (name, itemType, rules, extensions) {
var check = Argument.inConstructor(CLASS_NAME);
name = check(name).forMandatory('name').asString();
rules = check(rules).forMandatory('rules').asType(RuleManager);
extensions = check(extensions).forMandatory('extensions').asType(ExtensionManagerSync);
// Verify the model type of the item type.
if (itemType.modelType !== 'ReadOnlyChildModelSync')
throw new ModelError('invalidItem',
itemType.prototype.name, itemType.modelType,
CLASS_NAME, 'ReadOnlyChildModelSync');
// Get data access object.
var dao = extensions.getDataAccessObject(name);
/**
* @classdesc
* Represents the definition of a synchronous read-only root collection.
* @description
* Creates a new synchronous read-only root collection instance.
*
* _The name of the model type available as:
* __<instance>.constructor.modelType__, returns 'ReadOnlyRootCollectionSync'._
*
* @name ReadOnlyRootCollectionSync
* @constructor
* @param {bo.shared.EventHandlerList} [eventHandlers] - The event handlers of the instance.
*
* @extends CollectionBase
*
* @throws {@link bo.system.ArgumentError Argument error}:
* The event handlers must be an EventHandlerList object or null.
*
* @fires ReadOnlyRootCollectionSync#preFetch
* @fires ReadOnlyRootCollectionSync#postFetch
*/
var ReadOnlyRootCollectionSync = function (eventHandlers) {
CollectionBase.call(this);
eventHandlers = Argument.inConstructor(name)
.check(eventHandlers).forOptional('eventHandlers').asType(EventHandlerList);
var self = this;
var items = [];
var brokenRules = new BrokenRuleList(name);
var isValidated = false;
var dataContext = null;
var connection = null;
var totalItems = null;
/**
* The name of the model.
*
* @name ReadOnlyRootCollectionSync#$modelName
* @type {string}
* @readonly
*/
this.$modelName = name;
/**
* The count of the child objects in the collection.
*
* @name ReadOnlyRootCollectionSync#count
* @type {number}
* @readonly
*/
Object.defineProperty(self, 'count', {
get: function () {
return items.length;
},
enumerable: false
});
/**
* The count of all available items if provided by the data access object.
*
* @name ReadOnlyRootCollectionSync#totalItems
* @type {number}
* @readonly
*/
Object.defineProperty(self, 'totalItems', {
get: function () {
return totalItems;
},
enumerable: false
});
// Set up event handlers.
if (eventHandlers)
eventHandlers.setup(self);
//region Transfer object methods
function getTransferContext () {
return new TransferContext(null, null, null);
}
function baseToCto() {
var cto = [];
items.forEach(function (item) {
cto.push(item.toCto());
});
if (totalItems)
cto.totalItems = totalItems;
return cto;
}
/**
* Transforms the business object collection to a plain object array to send to the client.
*
* @function ReadOnlyRootCollectionSync#toCto
* @returns {Array.<object>} The client transfer object.
*/
this.toCto = function () {
if (extensions.toCto)
return extensions.toCto.call(self, getTransferContext());
else
return baseToCto();
};
//endregion
//region Permissions
function getAuthorizationContext(action, targetName) {
return new AuthorizationContext(action, targetName || '', brokenRules);
}
function canDo (action) {
return rules.hasPermission(
getAuthorizationContext(action)
);
}
function canExecute (methodName) {
return rules.hasPermission(
getAuthorizationContext(AuthorizationAction.executeMethod, methodName)
);
}
//endregion
//region Data portal methods
//region Helper
function getDataContext (connection) {
if (!dataContext)
dataContext = new DataPortalContext(dao);
return dataContext.setState(connection, false);
}
function raiseEvent (event, methodName, error) {
self.emit(
DataPortalEvent.getName(event),
new DataPortalEventArgs(event, name, null, methodName, error)
);
}
function wrapError (error) {
return new DataPortalError(MODEL_DESC, name, DataPortalAction.fetch, error);
}
//endregion
//region Fetch
function data_fetch (filter, method) {
// Check permissions.
if (method === M_FETCH ? canDo(AuthorizationAction.fetchObject) : canExecute(method)) {
try {
// Open connection.
connection = config.connectionManager.openConnection(extensions.dataSource);
// Launch start event.
/**
* The event arises before the collection instance will be retrieved from the repository.
* @event ReadOnlyRootCollectionSync#preFetch
* @param {bo.shared.DataPortalEventArgs} eventArgs - Data portal event arguments.
* @param {ReadOnlyRootCollectionSync} oldObject - The collection instance before the data portal action.
*/
raiseEvent(DataPortalEvent.preFetch, method);
// Execute fetch.
var dto = null;
if (extensions.dataFetch) {
// *** Custom fetch.
dto = extensions.dataFetch.call(self, getDataContext(connection), filter, method);
} else {
// *** Standard fetch.
// Root element fetches data from repository.
dto = dao.$runMethod(method, connection, filter);
}
// Get the count of all available items.
if (dto.totalItems &&
(typeof dto.totalItems === 'number' || dto.totalItems instanceof Number) &&
dto.totalItems % 1 === 0)
totalItems = dto.totalItems;
else
totalItems = null;
// Load children.
if (dto instanceof Array) {
dto.forEach(function (data) {
var item = itemType.load(self, data, eventHandlers);
items.push(item);
});
}
// Launch finish event.
/**
* The event arises after the collection instance has been retrieved from the repository.
* @event ReadOnlyRootCollectionSync#postFetch
* @param {bo.shared.DataPortalEventArgs} eventArgs - Data portal event arguments.
* @param {ReadOnlyRootCollectionSync} newObject - The collection instance after the data portal action.
*/
raiseEvent(DataPortalEvent.postFetch, method);
// Close connection.
connection = config.connectionManager.closeConnection(extensions.dataSource, connection);
} catch (e) {
// Wrap the intercepted error.
var dpError = wrapError(e);
// Launch finish event.
raiseEvent(DataPortalEvent.postFetch, method, dpError);
// Close connection.
connection = config.connectionManager.closeConnection(extensions.dataSource, connection);
// Rethrow error.
throw dpError;
}
}
}
//endregion
//endregion
//region Actions
/**
* Initializes a business object collection to be retrieved from the repository.
* <br/>_This method is called by a factory method with the same name._
*
* @function ReadOnlyRootCollectionSync#fetch
* @protected
* @param {*} [filter] - The filter criteria.
* @param {string} [method] - An alternative fetch method of the data access object.
*
* @throws {@link bo.system.ArgumentError Argument error}:
* The method must be a string or null.
* @throws {@link bo.rules.AuthorizationError Authorization error}:
* The user has no permission to execute the action.
* @throws {@link bo.shared.DataPortalError Data portal error}:
* Fetching the business object has failed.
*/
this.fetch = function(filter, method) {
method = Argument.inMethod(name, 'fetch')
.check(method).forOptional('method').asString();
data_fetch(filter, method || M_FETCH);
};
//endregion
//region Validation
/**
* Indicates whether all the validation rules of the business objects in the
* collection, including the ones of their child objects, succeeds. A valid collection
* may have broken rules with severity of success, information and warning.
*
* _By default read-only business object collections are supposed to be valid._
*
* @function ReadOnlyRootCollectionSync#isValid
* @returns {boolean} True when the business object is valid, otherwise false.
*/
this.isValid = function() {
return items.every(function (item) {
return item.isValid();
});
};
/**
* Executes all the validation rules of all business objects in the collection,
* including the ones of their child objects.
*
* _By default read-only business object collections are supposed to be valid._
*
* @function ReadOnlyRootCollectionSync#checkRules
*/
this.checkRules = function() {
brokenRules.clear();
items.forEach(function (item) {
item.checkRules();
});
isValidated = true;
};
/**
* Gets the broken rules of the business object collection.
*
* _By default read-only business object collections are supposed to be valid._
*
* @function ReadOnlyRootCollectionSync#getBrokenRules
* @param {string} [namespace] - The namespace of the message keys when messages are localizable.
* @returns {bo.rules.BrokenRulesOutput} The broken rules of the business object collection.
*/
this.getBrokenRules = function(namespace) {
var bro = brokenRules.output(namespace);
items.forEach(function (item) {
var childBrokenRules = item.getBrokenRules(namespace);
if (childBrokenRules)
bro.addChild(property.name, childBrokenRules);
});
return bro.$length ? bro : null;
};
/**
* Gets the response to send to the client in case of broken rules.
*
* _By default read-only business object collections are supposed to be valid._
*
* @function ReadOnlyRootCollectionSync#getResponse
* @param {string} [message] - Human-readable description of the reason of the failure.
* @param {string} [namespace] - The namespace of the message keys when messages are localizable.
* @returns {bo.rules.BrokenRulesResponse} The broken rules response to send to the client.
*/
this.getResponse = function (message, namespace) {
var output = this.getBrokenRules(namespace);
return output ? new BrokenRulesResponse(output, message) : null;
};
//endregion
//region Public array methods
/**
* Gets a collection item at a specific position.
*
* @function ReadOnlyRootCollectionSync#at
* @param {number} index - The index of the required item in the collection.
* @returns {ReadOnlyChildModelSync} The required collection item.
*/
this.at = function (index) {
return items[index];
};
/**
* Executes a provided function once per collection item.
*
* @function ReadOnlyRootCollectionSync#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 ReadOnlyRootCollectionSync#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 ReadOnlyRootCollectionSync#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 ReadOnlyRootCollectionSync#filter
* @param {external.cbCollectionItem} callback - Function to test for each collection item.
* @returns {Array.<ReadOnlyChildModelSync>} 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 ReadOnlyRootCollectionSync#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 ReadOnlyRootCollectionSync#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 {Array.<ReadOnlyChildModelSync>} The sorted collection.
*/
this.sort = function (fnCompare) {
return items.sort(fnCompare);
};
//endregion
// Immutable object.
Object.freeze(this);
};
util.inherits(ReadOnlyRootCollectionSync, CollectionBase);
/**
* The name of the model type.
*
* @property {string} ReadOnlyRootCollectionSync.constructor.modelType
* @default ReadOnlyRootCollectionSync
* @readonly
*/
Object.defineProperty(ReadOnlyRootCollectionSync, 'modelType', {
get: function () { return CLASS_NAME; }
});
//region Factory methods
/**
* Retrieves a read-only business object collection from the repository.
*
* @function ReadOnlyRootCollectionSync.fetch
* @param {*} [filter] - The filter criteria.
* @param {string} [method] - An alternative fetch method of the data access object.
* @param {bo.shared.EventHandlerList} [eventHandlers] - The event handlers of the instance.
* @returns {ReadOnlyRootCollectionSync} The required read-only business object collection.
*
* @throws {@link bo.system.ArgumentError Argument error}:
* The method must be a string or null.
* @throws {@link bo.system.ArgumentError Argument error}:
* The event handlers must be an EventHandlerList object or null.
* @throws {@link bo.rules.AuthorizationError Authorization error}:
* The user has no permission to execute the action.
* @throws {@link bo.shared.DataPortalError Data portal error}:
* Fetching the business object collection has failed.
*/
ReadOnlyRootCollectionSync.fetch = function(filter, method, eventHandlers) {
var instance = new ReadOnlyRootCollectionSync(eventHandlers);
instance.fetch(filter, method);
return instance;
};
//endregion
return ReadOnlyRootCollectionSync;
};
module.exports = ReadOnlyRootCollectionSyncFactory;