/*
Copyright (C) 2016 PencilBlue, LLC
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
'use strict';
//dependencies
var util = require('../../util.js');
var async = require('async');
var HtmlEncoder = require('htmlencode');
module.exports = function CustomObjectServiceModule(pb) {
/**
* Provides a service to do the heavy lifting of retrieving custom objects with
* the ability to eagerly fetch the related objects.
* @class CustomObjectService
* @constructor
*/
function CustomObjectService(siteUid, onlyThisSite) {
this.typesCache = {};
this.typesNametoId = {};
this.site = pb.SiteService.getCurrentSite(siteUid);
this.siteQueryService = new pb.SiteQueryService({site: this.site, onlyThisSite: onlyThisSite});
}
//statics
/**
* @static
* @property CUST_OBJ_COLL
* @type {String}
*/
CustomObjectService.CUST_OBJ_COLL = 'custom_object';
/**
* @static
* @property CUST_OBJ_TYPE_COLL
* @type {String}
*/
CustomObjectService.CUST_OBJ_TYPE_COLL = 'custom_object_type';
/**
* @static
* @property CUST_OBJ_SORT_COLL
* @type {String}
*/
CustomObjectService.CUST_OBJ_SORT_COLL = 'custom_object_sort';
//constants
/**
*
* @private
* @static
* @property NAME_FIELD
* @type {String}
*/
var NAME_FIELD = 'name';
/**
*
* @private
* @static
* @property PEER_OBJECT_TYPE
* @type {String}
*/
var PEER_OBJECT_TYPE = 'peer_object';
/**
*
* @private
* @static
* @property CHILD_OBJECTS_TYPE
* @type {String}
*/
var CHILD_OBJECTS_TYPE = 'child_objects';
/**
*
* @private
* @static
* @property CUST_OBJ_TYPE_PREFIX
* @type {String}
*/
var CUST_OBJ_TYPE_PREFIX = 'custom:';
/**
*
* @private
* @static
* @property AVAILABLE_FIELD_TYPES
* @type {Object}
*/
var AVAILABLE_FIELD_TYPES = Object.freeze({
'text': pb.validation.isStr,
'number': pb.validation.isNum,
'wysiwyg': pb.validation.isStr,
'boolean': pb.validation.isBool,
'date': pb.validation.isDate,
'peer_object': pb.validation.isIdStr,
'child_objects': pb.validation.isArray
});
/**
*
* @private
* @static
* @property AVAILABLE_REFERENCE_TYPES
* @type {Object}
*/
var AVAILABLE_REFERENCE_TYPES = [
'article',
'page',
'section',
'topic',
'media',
'user'
];
/**
* Validates and persists a sort ordering for custom objects of a specific type
* @method saveSortOrdering
* @param {Object} sortOrder
* @param {Function} cb
*/
CustomObjectService.prototype.saveSortOrdering = function(sortOrder, cb) {
if (!pb.validation.isObj(sortOrder, true)) {
throw new Error('The custom object type must be a valid object.');
}
sortOrder.object_type = CustomObjectService.CUST_OBJ_SORT_COLL;
this.validateSortOrdering(sortOrder, function(err, errors) {
if (util.isError(err) || errors.length > 0) {
return cb(err, errors);
}
var dao = new pb.DAO();
dao.save(sortOrder, cb);
});
};
/**
* Validates a sort ordering for custom objects of a specific type
* @method validateSortOrdering
* @param {Object} sortOrder
* @param {Function} cb A callback that takes two parameters. The first is an
* error, if occurred and the second is an array of validation error objects.
* If the array is empty them it is safe to assume that the object is valid.
*/
CustomObjectService.prototype.validateSortOrdering = function(sortOrder, cb) {
if (!util.isObject(sortOrder)) {
throw new Error('The sortOrder parameter must be a valid object');
}
//validate sorted IDs
var errors = [];
if (!util.isArray(sortOrder.sorted_objects)) {
errors.push(CustomObjectService.err('sorted_objects', 'The sorted_objects property must be an array of IDs'));
}
else {
if (sortOrder.length === 0) {
errors.push(CustomObjectService.err('sorted_objects', 'The sorted objects ID list cannot be empty'));
}
for (var i = 0; i < sortOrder.length; i++) {
if (!pb.validation.isIdStr(sortOrder[i], true)) {
errors.push(CustomObjectService.err('sorted_objects.'+i, 'An invalid ID was found in the sorted_objects array at index '+i));
}
}
}
//validate that an object type Id is present
if (!pb.validation.isIdStr(sortOrder.custom_object_type, true)) {
errors.push(CustomObjectService.err('custom_object_type', 'An invalid ID value was passed for the custom_object_type property'));
}
//validate an object type exists
if (sortOrder.object_type !== CustomObjectService.CUST_OBJ_SORT_COLL) {
errors.push(CustomObjectService.err('object_type', 'The object_type value must be set to: '+CustomObjectService.CUST_OBJ_SORT_COLL));
}
cb(null, errors);
};
/**
* Retrieves custom objects of the specified type based on the specified options.
* @method findByTypeWithOrdering
* @param {Object|String} custObjType The custom object type descriptor object or the ID
* string of the type descriptor.
* @param {Object} [options={}] The filters and other flags. The options object
* supports the same fields as the DAO.query function.
* @param {Integer} [options.fetch_depth=0] The depth indicates how many levels
* of referenced child and peer objects to load. At the bottom level the
* references will be left as ID strings.
* @param {Function} cb A callback that takes two parameters. The first is any
* error, if occurred. The second is an array of objects sorted by the ordering
* assigned for the custom object or by name if no ordering exists.
*/
CustomObjectService.prototype.findByTypeWithOrdering = function(custObjType, options, cb) {
if (util.isFunction(options)) {
cb = options;
options = {};
}
else if (!util.isObject(options)) {
options = {};
}
var sortOrder = null;
var custObjects = null;
var self = this;
var tasks = [
//load objects
function(callback) {
self.findByType(custObjType, options, function(err, custObjectDocs) {
custObjects = custObjectDocs;
callback(err);
});
},
//load ordering
function(callback) {
self.loadSortOrdering(custObjType, function(err, ordering) {
sortOrder = ordering;
callback(err);
});
}
];
async.parallel(tasks, function(err) {
custObjects = CustomObjectService.applyOrder(custObjects, sortOrder);
cb(err, custObjects);
});
};
/**
* Coordinates the eager fetching of peer and child objects for the specified custom object.
* @method fetchChildren
* @param {Object} custObj The custom object to inspect
* @param {Object} options The options specified for the retrieval
* @param {Integer} options.fetch_depth The number of levels of peer and child
* objects to retrieve
* @param {Object|String} custObjType The custom object type for the specified
* custom object. This can also be the ID string value.
* @param {Function} cb A callback function that takes two parameters. The
* first is an Error, if occurred. The second is the specified custom object.
*/
CustomObjectService.prototype.fetchChildren = function(custObj, options, custObjType, cb) {
if (!util.isObject(custObj) || !util.isObject(options)) {
throw new Error('The custObj and options parameters must be an objects');
}
else if (util.isFunction(custObjType)) {
cb = custObjType;
custObjType = null;
}
else if (!pb.validation.isInt(options.fetch_depth, true, true) || options.fetch_depth <= 0) {
return cb(null, custObj);
}
//log what we are doing. this shit gets confusing
if (pb.log.isSilly()) {
pb.log.silly('CustomObjectService: Fetching children for [%s][%s] at depth:[%d]', custObj.type, custObj.name, options.fetch_depth);
}
//private function to retrieve the custom object type if not available
var self = this;
var getCustObjType = function(type, cb) {
if (util.isObject(type)) {
return cb(null, type);
}
else if (self.typesCache[type]) {
return cb(null, self.typesCache[type]);
}
else if (self.typesNametoId[type]) {
return cb(null, self.typesCache[self.typesNametoId[type]]);
}
else if (!type) {
type = custObj.type;
}
//build out the where clause
var where = null;
if (pb.validation.isIdStr(type, true)) {
where = pb.DAO.getIdWhere(type);
}
else {
where = {
name: type
};
}
self.loadTypeBy(where, function(err, custObjType) {
if (util.isObject(custObjType)) {
self.typesCache[type] = custObjType;
self.typesNametoId[custObjType.name] = custObjType[pb.DAO.getIdField()];
}
cb(err, custObjType);
});
};
//loads up a peer object
var loadPeerObject = function(id, objType, cb) {
if (!pb.validation.isIdStr(id, true)) {
return cb(null, null);
}
//we must determine if we are loading a regular object or a custom object
if (CustomObjectService.isCustomObjectType(objType)) {
getCustObjType(CustomObjectService.getCustTypeSimpleName(objType), function(err, type) {
if (util.isError(err)) {
return cb(err);
}
self.loadById(id, {fetch_depth: options.fetch_depth - 1}, cb);
});
}
else {
//we know that we are a system object so we resort to DAO
var dao = new pb.DAO();
dao.loadById(id, objType, cb);
}
};
//load up child objects
var loadChildObjects = function(ids, objType, cb) {
if (!util.isArray(ids) || ids === []) {
return cb(null, []);
}
//we must determine if we are loading custom objects or regular system objects
if (CustomObjectService.isCustomObjectType(objType)) {
getCustObjType(CustomObjectService.getCustTypeSimpleName(objType), function(err, type) {
if (util.isError(err)) {
return cb(err);
}
var opts = {
where: pb.DAO.getIdInWhere(ids),
fetch_depth: options.fetch_depth - 1
};
self.findByType(type, opts, cb);
});
}
else {
var opts = { where: pb.DAO.getIdInWhere(ids) };
var dao = new pb.DAO();
dao.q(objType, opts, cb);
}
};
//make sure we have the type for the object passed in
getCustObjType(custObjType, function(err, custObjType) {
if (util.isError(err)) {
return cb(err);
}
else if (util.isNullOrUndefined(custObjType)) {
return cb(new Error('An invalid custom object type: ' + custObjType + ' was found.'));
}
var tasks = util.getTasks(Object.keys(custObjType.fields), function(fieldNames, i) {
return function(callback) {
var field = custObjType.fields[fieldNames[i]];
if (!CustomObjectService.isReferenceFieldType(field.field_type)) {
return callback();
}
//load a peer object
if (field.field_type === PEER_OBJECT_TYPE) {
//load and set the peer object
loadPeerObject(custObj[fieldNames[i]], field.object_type, function(err, peerObj) {
if (util.isObject(peerObj)) {
custObj[fieldNames[i]] = peerObj;
}
callback(err);
});
}//load child objects
else if (field.field_type === CHILD_OBJECTS_TYPE) {
//load and set the child objects
loadChildObjects(custObj[fieldNames[i]], field.object_type, function(err, childObjs) {
if (util.isArray(childObjs)) {
//apply the original ordering to the fields. The
//DB does not promise to provide the correct ordering.
var sortOrder = {
sorted_objects: custObj[fieldNames[i]]
};
custObj[fieldNames[i]] = CustomObjectService.applyOrder(childObjs, sortOrder);
}
callback(err);
});
}
else {
callback(new Error('An invalid field type was provided: '+field.field_type));
}
};
});
async.series(tasks, function(err) {
cb(err, custObj);
});
});
};
/**
* Loads an ordering object for a specific custom object type.
* @method loadSortOrdering
* @param {Object|String} custObjType
* @param {Function} cb A callback that takes two parameters. The first is an
* error, if occurred. The second is the sort ordering object if found.
*/
CustomObjectService.prototype.loadSortOrdering = function(custObjType, cb) {
if (util.isObject(custObjType)) {
custObjType = custObjType[pb.DAO.getIdField()] + '';
}
else if (!util.isString(custObjType)) {
throw new Error('An invalid custom object type was provided: '+(typeof custObjType)+':'+custObjType);
}
var dao = new pb.DAO();
dao.loadByValue('custom_object_type', custObjType, CustomObjectService.CUST_OBJ_SORT_COLL, cb);
};
/**
* Finds custom objects by the specified type.
* @method findByType
* @param {Object|String} type The custom object type object or the ID of the
* object as a string
* @param {Object} [options] See DAO.q()
* @param {Function} cb A callback that takes two arguments. The first is an
* error, if occurred. The second is an array of custom objects that match the
* specified criteria.
*/
CustomObjectService.prototype.findByType = function(type, options, cb) {
if (util.isFunction(options)) {
cb = options;
options = {};
}
else if (!util.isObject(options)) {
options = {};
}
//ensure a where clause
if (!util.isObject(options.where)) {
options.where = {};
}
var typeStr = type;
if (util.isObject(type)) {
typeStr = type[pb.DAO.getIdField()] + '';
}
options.where.type = typeStr;
var self = this;
self.siteQueryService.q(CustomObjectService.CUST_OBJ_COLL, options, function(err, custObjs) {
if (util.isArray(custObjs)) {
var tasks = util.getTasks(custObjs, function(custObjs, i) {
return function(callback) {
self.fetchChildren(custObjs[i], options, type, callback);
};
});
async.series(tasks, cb);
return;
}
cb(err, custObjs);
});
};
/**
* Retrieves all of the custom object types in the system
* @method findTypes
* @param {Function} cb A callback that takes two parameters. The first is an
* error, if occurred. The second is an array of custom object type objects.
*/
CustomObjectService.prototype.findTypes = function(cb) {
var opts = {
where: pb.DAO.ANYWHERE,
select: pb.DAO.PROJECT_ALL,
order: [[NAME_FIELD, pb.DAO.ASC]]
};
this.siteQueryService.q(CustomObjectService.CUST_OBJ_TYPE_COLL, opts, function(err, custObjTypes) {
if (util.isArray(custObjTypes)) {
//currently, mongo cannot do case-insensitive sorts. We do it manually
//until a solution for https://jira.mongodb.org/browse/SERVER-90 is merged.
custObjTypes.sort(function(a, b) {
var x = a.name.toLowerCase();
var y = b.name.toLowerCase();
return ((x < y) ? -1 : ((x > y) ? 1 : 0));
});
}
cb(err, custObjTypes);
});
};
/**
* Retrieves a count based the specified criteria and type
* @method countByType
* @param {Object|String} type The custom object type object or ID string
* @param {Object} [where] The criteria for which objects to count
* @param {Function} cb A callback that takes two parameters. The first is an
* error, if occurred. The second is the number of objects that match the
* specified criteria.
*/
CustomObjectService.prototype.countByType = function(type, where, cb) {
if (util.isFunction(where)) {
cb = where;
where = {};
}
else if (!util.isObject(where)) {
where = {};
}
var typeStr = type;
if (util.isObject(type)) {
typeStr = type[pb.DAO.getIdField()] + '';
}
where.type = typeStr;
var dao = new pb.DAO();
dao.count(CustomObjectService.CUST_OBJ_COLL, where, cb);
};
/**
* Loads a custom object by ID
* @method loadById
* @param {ObjectID|String} id
* @param {Object} [options]
* @param {Function} cb
*/
CustomObjectService.prototype.loadById = function(id, options, cb) {
this.loadBy(undefined, pb.DAO.getIdWhere(id), options, cb);
};
/**
* Loads a custom object by name
* @method loadByName
* @param {String} type The ID string of the custom object type
* @param {String} name The unique name of the custom object
* @param {Object} [options]
* @param {Function} cb
*/
CustomObjectService.prototype.loadByName = function(type, name, options, cb) {
var where = {};
where[NAME_FIELD] = name;
this.loadBy(type, where, options, cb);
};
/**
* Loads a custom object by the specified where criteria
* @method loadBy
* @param {String|Null} type
* @param {Object} where
* @param {Object} [options]
* @param {Function} cb
*/
CustomObjectService.prototype.loadBy = function(type, where, options, cb) {
if (!pb.validation.isIdStr(type, false) || !pb.validation.isObj(where, true) || pb.validation.isEmpty(where)) {
throw new Error('The type, where must be provided in order to load a custom object');
}
else if (util.isFunction(options)) {
cb = options;
options = {};
}
else if (!util.isObject(options)) {
throw new Error('The options object must be an object');
}
if (type) {
var typeStr = type;
if (util.isObject(type)) {
typeStr = type[pb.DAO.getIdField()] + '';
}
where.type = typeStr;
}
var self = this;
self.siteQueryService.loadByValues(where, CustomObjectService.CUST_OBJ_COLL, function(err, custObj) {
if (util.isObject(custObj)) {
return self.fetchChildren(custObj, options, type, cb);
}
cb(err, custObj);
});
};
/**
* Loads a custom object type by ID
* @method loadTypeById
* @param {ObjectID|String} id
* @param {Function} cb
*/
CustomObjectService.prototype.loadTypeById = function(id, cb) {
this.loadTypeBy(pb.DAO.getIdWhere(id), cb);
};
/**
* Loads a custom object type by name
* @method loadTypeByName
* @param {String} name
* @param {Function} cb
*/
CustomObjectService.prototype.loadTypeByName = function(name, cb) {
name = CustomObjectService.getCustTypeSimpleName(name);
var where = {};
where[NAME_FIELD] = name;
this.loadTypeBy(where, cb);
};
/**
* Loads a custom object type by the specified where criteria
* @method loadTypeBy
* @param {Object} where
* @param {Function} cb
*/
CustomObjectService.prototype.loadTypeBy = function(where, cb) {
if (!pb.validation.isObj(where, true) || pb.validation.isEmpty(where)) {
return cb(Error("The where parameter must be provided in order to load a custom object type"));
}
this.siteQueryService.loadByValues(where, CustomObjectService.CUST_OBJ_TYPE_COLL, cb);
};
/**
* Validates a custom object
* @method validate
* @param {Object} custObj The object to validate
* @param {Object} custObjType The custom object type to validate against
* @param {Function} cb A callback that takes two parameters. The first is an
* error if occurred. The second is an array of validation error objects. If
* the array is empty it is safe to assume that the object is valid.
*/
CustomObjectService.prototype.validate = function(custObj, custObjType, cb) {
var self = this;
var errors = [];
var dao = new pb.DAO();
var tasks = [
//validate object type
function(callback) {
if (custObj.object_type !== CustomObjectService.CUST_OBJ_COLL) {
errors.push(CustomObjectService.err('type', "The object type must be: "+custObj.object_type));
}
callback(null);
},
//validate the type
function(callback) {
if (!pb.validation.isIdStr(custObj.type) || custObj.type !== custObjType[pb.DAO.getIdField()].toString()) {
errors.push(CustomObjectService.err('type', "The type must be an ID string and must match the describing custom object type's ID"));
}
callback(null);
},
//validate the name
function(callback) {
if (!pb.validation.isNonEmptyStr(custObj.name, true)) {
errors.push(CustomObjectService.err('name', 'The name cannot be empty'));
return callback(null);
}
//test for HTML
var sanitized = pb.BaseController.sanitize(custObj.name);
if (sanitized !== custObj.name) {
errors.push(CustomObjectService.err('name', 'The name cannot contain HTML'));
return callback(null);
}
//test uniqueness of name
var where = {
type: custObjType[pb.DAO.getIdField()].toString()
};
where[NAME_FIELD] = new RegExp('^'+util.escapeRegExp(custObj.name)+'$', 'i');
dao.unique(CustomObjectService.CUST_OBJ_COLL, where, custObj[pb.DAO.getIdField()], function(err, isUnique){
if (!isUnique) {
errors.push(CustomObjectService.err('name', 'The name '+custObj.name+' is not unique'));
}
callback(err);
});
},
//validate other fields
function(callback) {
self.validateCustObjFields(custObj, custObjType, function(err, fieldErrors) {
if (util.isArray(fieldErrors)) {
util.arrayPushAll(fieldErrors, errors);
}
callback(err);
});
}
];
async.series(tasks, function(err) {
cb(err, errors);
});
};
/**
* Validates the fields of a custom object
* @method validateCustObjFields
* @param {Object} custObj The object to validate
* @param {Object} custObjType The custom object type to validate against
* @param {Function} cb A callback that takes two parameters. The first is an
* error if occurred. The second is an array of validation error objects. If
* the array is empty it is safe to assume that the object is valid.
*/
CustomObjectService.prototype.validateCustObjFields = function(custObj, custObjType, cb) {
var errors = [];
var tasks = util.getTasks(Object.keys(custObjType.fields), function(keys, i) {
return function(callback) {
//check for exception
var fieldName = keys[i];
if (fieldName === NAME_FIELD) {
//validated independently in main validation function
return callback(null);
}
//get value
var val = custObj[fieldName];
//execute validation procedure
var field = custObjType.fields[fieldName];
var fieldType = field.field_type;
var isValid = AVAILABLE_FIELD_TYPES[fieldType];
if (!isValid(val, false)) {
errors.push(CustomObjectService.err(fieldName, 'An invalid value ['+val+'] was found.'));
}
callback(null);
};
});
async.series(tasks, function(err) {
cb(err, errors);
});
};
/**
* Validates a Custom Object Type.
* @method validateType
* @param {Object} custObjType The object to validate
* @param {Function} cb A callback function that provides two parameters: The
* first, an error, if exists. The second is an array of objects that represent
* validation errors. If the 2nd parameter is an empty array it is safe to
* assume that validation passed.
*/
CustomObjectService.prototype.validateType = function(custObjType, cb) {
if (!pb.validation.isObj(custObjType)) {
return cb(new Error('The type descriptor must be an object: '+(typeof custObjType)));
}
var self = this;
var errors = [];
var tasks = [
//validate the name
function(callback) {
if (!pb.validation.isNonEmptyStr(custObjType.name, true)) {
errors.push(CustomObjectService.err('name', 'The name cannot be empty'));
return callback(null);
}
//test for HTML
var sanitized = pb.BaseController.sanitize(custObjType.name);
if (sanitized !== custObjType.name) {
errors.push(CustomObjectService.err('name', 'The name cannot contain HTML'));
return callback(null);
}
//test uniqueness of name
var where = {};
where[NAME_FIELD] = new RegExp('^'+util.escapeRegExp(custObjType.name)+'$', 'i');
self.siteQueryService.unique(CustomObjectService.CUST_OBJ_TYPE_COLL, where, custObjType[pb.DAO.getIdField()], function(err, isUnique){
if(!isUnique) {
errors.push(CustomObjectService.err('name', 'The name '+custObjType.name+' is not unique'));
}
callback(err);
});
},
//validate the fields
function(callback) {
if (!pb.validation.isObj(custObjType.fields)) {
errors.push(CustomObjectService.err('fields', 'The fields property must be an object'));
return callback(null);
}
//get the supported object types
self.getReferenceTypes(function(err, types) {
if (util.isError(err)) {
return callback(err);
}
var typesHash = util.arrayToHash(types);
Object.keys(custObjType.fields).forEach(function(fieldName) {
if (!pb.validation.isNonEmptyStr(fieldName)) {
errors.push(CustomObjectService.err('fields.', 'The field name cannot be empty'));
}
else {
var fieldErrors = self.validateFieldDescriptor(custObjType.fields[fieldName], typesHash);
util.arrayPushAll(fieldErrors, errors);
}
});
callback(null);
});
}
];
async.series(tasks, function(err) {
cb(err, errors);
});
};
/**
* Validates that the field descriptor for a custom object type.
* @method validateFieldDescriptor
* @param {object} field
* @param {object} customTypes
* @return {Array} An array of objects that contain two properties: field and
* error
*/
CustomObjectService.prototype.validateFieldDescriptor = function(field, customTypes) {
var errors = [];
if (!pb.validation.isObj(field)) {
errors.push(CustomObjectService.err('', 'The field descriptor must be an object: '+(typeof field)));
}
else {
if (!AVAILABLE_FIELD_TYPES[field.field_type]) {
errors.push(CustomObjectService.err('field_type', 'An invalid field type was specified: '+field.field_type));
}
else if (field.field_type === PEER_OBJECT_TYPE || field.field_type === CHILD_OBJECTS_TYPE) {
if (!customTypes[field.object_type]) {
errors.push(CustomObjectService.err('object_type', 'An invalid object type was specified: '+field.object_type));
}
}
}
return errors;
};
/**
* Retrieves an array of all of the available object types that can be
* referenced as a child or peer object.
* @method getReferenceTypes
* @param {Function} cb A callback that takes two parameters: The first, an
* error, if occurs. The second is an array of all of the available object
* types that can be referenced as a peer or child object.
*/
CustomObjectService.prototype.getReferenceTypes = function(cb) {
var select = {};
select[NAME_FIELD] = 1;
select[pb.DAO.getIdField()] = 0;
var opts = {
where: pb.DAO.ANYWHERE,
select: select
};
this.siteQueryService.q(CustomObjectService.CUST_OBJ_TYPE_COLL, opts, function(err, types) {
if (util.isError(err)) {
return cb(err);
}
var allTypes = util.clone(AVAILABLE_REFERENCE_TYPES);
for (var i = 0; i < types.length; i++) {
allTypes.push('custom:'+types[i][NAME_FIELD]);
}
cb(null, allTypes);
});
};
/**
* Validates and persists a custom object
* @method save
* @param {Object} custObj The object to validate
* @param {Object} custObjType The custom object type to validate against
* @param {Function} cb A callback that takes two parameters. The first is an
* error if occurred. The second is an array of validation error objects or the
* result of the persistence operation.
*
*/
CustomObjectService.prototype.save = function(custObj, custObjType, cb) {
if (!pb.validation.isObj(custObj, true)) {
throw new Error('The custom object must be a valid object.');
}
var self = this;
custObj.object_type = CustomObjectService.CUST_OBJ_COLL;
this.validate(custObj, custObjType, function(err, errors) {
if (util.isError(err) || errors.length > 0) {
return cb(err, errors);
}
self.siteQueryService.save(custObj, cb);
});
};
/**
* Validates and persists a custom object type
* @method saveType
* @param {Object} custObjType The object to persist
* @param {Function} cb A callback that takes two parameters. The first is an
* error if occurred. The second is an array of validation error objects or the
* result of the persistence operation.
*/
CustomObjectService.prototype.saveType = function(custObjType, cb) {
if (!pb.validation.isObj(custObjType, true)) {
throw new Error('The custom object type must be a valid object.');
}
var self = this;
custObjType.object_type = CustomObjectService.CUST_OBJ_TYPE_COLL;
this.validateType(custObjType, function(err, errors) {
if(util.isError(err) || errors.length > 0) {
return cb(err, errors);
}
self.siteQueryService.save(custObjType, cb);
});
};
/**
* Deletes a custom object by ID
* @method deleteById
* @param {String} id
* @param {Function} cb (Error, *)
*/
CustomObjectService.prototype.deleteById = function(id, cb) {
var dao = new pb.DAO();
dao.deleteById(id, CustomObjectService.CUST_OBJ_COLL, cb);
};
/**
* Deletes a custom object type by id
* @method deleteTypeById
* @param {String|ObjectID} id
* @param {object} [options={}]
* @param {function} cb (Error, *)
*/
CustomObjectService.prototype.deleteTypeById = function(id, options, cb) {
if (util.isFunction(options)) {
cb = options;
options = {};
}
if (!pb.validation.isId(id, true)) {
return cb(new Error('INVALID_UID'));
}
var self = this;
var tasks = [
//remove object type
function(callback) {
var dao = new pb.DAO();
dao.deleteById(id, CustomObjectService.CUST_OBJ_TYPE_COLL, callback);
},
//remove those objects associated with the type
function(callback) {
self.deleteForType(id, callback);
}
];
async.series(tasks, cb);
};
/**
* Deletes all custom objects of a specified type
* @method deleteForType
* @param {string|object} custObjType A string ID of the custom object type or
* the custom object type itself.
* @param {Function} cb (Error, *)
*/
CustomObjectService.prototype.deleteForType = function(custObjType, cb) {
var typeId = custObjType;
if (!util.isString(custObjType)) {
typeId = custObjType[pb.DAO.getIdField()] + '';
}
var dao = new pb.DAO();
dao.delete({type: typeId}, CustomObjectService.CUST_OBJ_COLL, cb);
};
/**
* Determines if a custom object type with the specified name (case insensitive) exists
* @method typeExists
* @param {string} typeName
* @param {function} cb (Error, Boolean)
*/
CustomObjectService.prototype.typeExists = function(typeName, cb) {
var where = {
name: new RegExp('^'+util.escapeRegExp(typeName)+'$', 'ig')
};
this.siteQueryService.exists(CustomObjectService.CUST_OBJ_TYPE_COLL, where, cb);
};
/**
* Provides the various types of fields that are allowed for a custom object (number, boolean, string, etc).
* @static
* @method getFieldTypes
* @return {Array}
*/
CustomObjectService.getFieldTypes = function() {
return Object.keys(AVAILABLE_FIELD_TYPES);
};
/**
* Retrieves the objects types that can be referenced by custom objects
* @static
* @method getStaticReferenceTypes
* @return {Array}
*/
CustomObjectService.getStaticReferenceTypes = function() {
return util.clone(AVAILABLE_REFERENCE_TYPES);
};
/**
* Determines if a field type is reference to another object type
* @static
* @method isReferenceFieldType
* @param {String} fieldType
* @return {Boolean}
*/
CustomObjectService.isReferenceFieldType = function(fieldType) {
return fieldType === PEER_OBJECT_TYPE || fieldType === CHILD_OBJECTS_TYPE;
};
/**
* Determines if the field type is a custom object type or a system reference
* @static
* @method isCustomObjectType
* @param {String} objType
* @return {Boolean}
*/
CustomObjectService.isCustomObjectType = function(objType) {
return util.isString(objType) && objType.indexOf(CUST_OBJ_TYPE_PREFIX) === 0;
};
/**
* Gets the simple custom object name. The simple name is one that is not
* prefixed to indicate that it is custom
* @static
* @method getCustTypeSimpleName
* @param {String} name
* @return {String}
*/
CustomObjectService.getCustTypeSimpleName = function(name) {
if (util.isString(name)) {
name = name.replace(CUST_OBJ_TYPE_PREFIX, '');
}
return name;
};
/**
* Formats the object by ensuring that each field is in the correct data type.
* @static
* @method formatRawForType
* @param {Object} post The raw post object
* @param {Object} custObjType The custom object type describes the data in the
* post obj.
*/
CustomObjectService.formatRawForType = function(post, custObjType) {
//remove system fields if posted back
delete post[pb.DAO.getIdField()];
delete post.created;
delete post.last_modified;
//apply types to fields
Object.keys(custObjType.fields).forEach(function(key) {
if(custObjType.fields[key].field_type === 'number') {
if(util.isString(post[key])) {
post[key] = parseFloat(post[key]);
}
}
else if(custObjType.fields[key].field_type === 'date') {
if(util.isString(post[key])) {
post[key] = Date.parse(post[key]);
}
else if (!isNaN(post[key])) {
post[key] = new Date(post[key]);
}
}
else if (custObjType.fields[key].field_type === 'boolean') {
if (!util.isBoolean(post[key])) {
if (util.isString(post[key])) {
post[key] = "true" === post[key].toLowerCase();
}
else if (!isNaN(post[key])) {
post[key] = post[key] ? true : false;
}
}
}
else if (custObjType.fields[key].field_type === 'wysiwyg') {
//ensure not funky script tags or iframes
post[key] = pb.BaseController.sanitize(post[key], pb.BaseController.getContentSanitizationRules());
}
else if(custObjType.fields[key].field_type === CHILD_OBJECTS_TYPE) {
if(util.isString(post[key])) {
//strips out any non ID strings.
//TODO This should really move to validation.
post[key] = post[key].split(',');
for (var i = post[key].length - 1; i >= 0; i--) {
if (!pb.validation.isIdStr(post[key][i], true)) {
post[key].splice(i, 1);
}
}
}
}
else if (custObjType.fields[key].field_type === PEER_OBJECT_TYPE) {
//do nothing because it can only been a string ID. Validation
//should verify this before persistence.
}
else if (util.isString(post[key])){
//when nothing else matches and we just have a string. We should sanitize it
post[key] = pb.BaseController.sanitize(post[key]);
}
});
post.type = custObjType[pb.DAO.getIdField()].toString();
};
/**
* Formats the raw post data for a sort ordering
* @static
* @method formatRawSortOrdering
* @param {Object} post
* @param {Object} sortOrder the existing sort order object that the post data
* will be merged with
* @return {Object} The formatted sort ordering object
*/
CustomObjectService.formatRawSortOrdering = function(post, sortOrder) {
delete post.last_modified;
delete post.created;
delete post[pb.DAO.getIdField()];
var sortOrderDoc = pb.DocumentCreator.create('custom_object_sort', post, []);
if (!sortOrderDoc) {
return sortOrderDoc;
}
//merge the old and new
if (util.isObject(sortOrder)) {
util.merge(sortOrderDoc, sortOrder);
return sortOrder;
}
return sortOrderDoc;
};
/**
* Discovers the field types used for each entry in the provided array and sets
* the "fieldTypesUsed" property for the object.
* @static
* @method setFieldTypesUsed
* @param {Array} custObjTypes The array of custom object type objects to inspect
* @param {Localization} ls
*/
CustomObjectService.setFieldTypesUsed = function(custObjTypes, ls) {
if (!util.isArray(custObjTypes)) {
return;
}
var map = {};
map.text = ls.g('custom_objects.TEXT');
map.number = ls.g('custom_objects.NUMBER');
map.wysiwyg = ls.g('generic.WYSIWYG');
map.boolean = ls.g('custom_objects.BOOLEAN').toLowerCase();
map.date = ls.g('generic.DATE');
map[PEER_OBJECT_TYPE] = ls.g('custom_objects.PEER_OBJECT');
map[CHILD_OBJECTS_TYPE] = ls.g('custom_objects.CHILD_OBJECTS');
// Make the list of field types used in each custom object type, for display
for(var i = 0; i < custObjTypes.length; i++) {
var fieldTypesUsed = {};
for(var key in custObjTypes[i].fields) {
var fieldType = custObjTypes[i].fields[key].field_type;
fieldTypesUsed[map[fieldType]] = 1;
}
fieldTypesUsed = Object.keys(fieldTypesUsed);
custObjTypes[i].fieldTypesUsed = fieldTypesUsed.join(', ');
}
};
/**
* Orders the custom objects based on the provided sort order
* @static
* @method applyOrder
* @param {Array} custObjects The array of custom objects to be sorted
* @param {Object} sortOrder The object describing the ordering of the objects
* @return {Array} A refernce to the sorted array of custom objects. The
* reference is the same as provided to the function.
*/
CustomObjectService.applyOrder = function(custObjects, sortOrder) {
if (!util.isArray(custObjects)) {
throw new Error('The custObjects parameter must be an array');
}
//sort by name (case-insensitive)
if(!util.isObject(sortOrder) || !sortOrder.sorted_objects) {
//currently, mongo cannot do case-insensitive sorts. We do it manually
//until a solution for https://jira.mongodb.org/browse/SERVER-90 is merged.
custObjects.sort(function(a, b) {
var x = a.name.toLowerCase();
var y = b.name.toLowerCase();
return ((x < y) ? -1 : ((x > y) ? 1 : 0));
});
}
else {
var idField = pb.DAO.getIdField();
var customObjectSort = sortOrder.sorted_objects;
var sortedObjects = [];
for(var i = 0; i < customObjectSort.length; i++) {
for(var j = 0; j < custObjects.length; j++) {
if(custObjects[j][idField].equals(pb.DAO.getObjectId(customObjectSort[i]))) {
sortedObjects.push(custObjects[j]);
custObjects.splice(j, 1);
break;
}
}
}
custObjects = sortedObjects.concat(custObjects);
}
return custObjects;
};
/**
* Creates a validation error field
* @static
* @method err
* @param {String} field The field in the object that contains the error
* @param {String} err A string description of the error
* @return {Object} An object that describes the validation error
*/
CustomObjectService.err = function(field, err) {
return {
field: field,
msg: err
};
};
/**
* Creates an HTML formatted error string out of an array of error objects.
* @static
* @method createErrorStr
* @param {Array} errors An array of objects where each object has a "msg" and
* a "field" property
* @param {String} msg
* @return {String} HTML formatted string representing the errors
*/
CustomObjectService.createErrorStr = function(errors, msg) {
var errStr = '';
if (msg) {
errStr += msg + '\n';
}
errStr += '<ul>';
for(var i = 0; i < errors.length; i++) {
var err = errors[i];
errStr += '<li>';
if (err.field) {
errStr += err.field + ': ';
}
errStr += HtmlEncoder.htmlEncode(err.msg) + '</li>';
}
errStr += '</ul>';
return errStr;
};
return CustomObjectService;
};