Skip to content

Instantly share code, notes, and snippets.

@abernardobr
Created April 27, 2016 21:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save abernardobr/8c0ebad798a0b280f99715dc9901408f to your computer and use it in GitHub Desktop.
Save abernardobr/8c0ebad798a0b280f99715dc9901408f to your computer and use it in GitHub Desktop.
emprego.net - example code - back-end
var _ = require('lodash');
var Mongoose = require('mongoose');
var Mongoosastic = require('mongoosastic')
var Behaviours = require("hd-behaviours");
var Decorators = require("hd-decorators");
var Async = require('async');
var HD = require('hd').utils;
var Domains = require('hd').domains;
var SanitizerPlugin = require('mongoose-sanitizer');
var HStatus = require('hd-status');
var itemStatus = HStatus.types;
var Schema = Mongoose.Schema;
var modelName = "activities";
var privacyBehaviour = { plugin: Behaviours.privacy, options: { modelName: modelName } };
var crudDecorator = { plugin: Decorators.crud ,
options: {
keepOnRemove: false,
crudOveride: { getQuery: function(options, cb) { model.getQueryOveride(options, cb); } }
}
};
var userPrivacy = {
ONLY_ME: 1,
MY_FRIENDS: 2,
USERS: 3,
PUBLIC: 4
};
var edgeType = Behaviours.edgeable.types;
var edgeStatus = Behaviours.edgeable.status;
// Behavior Plugins
// ** Behaviours.likeable: adds likeable behaviours to the Schema and the activity model
// ** privacyBehaviour: adds privacy check for security
// ** Behaviours.commentable: adds comments to activities (model activity)
var behaviours = [ Behaviours.likeable, privacyBehaviour, Behaviours.commentable ];
// Decorates the model
// ** crudDecorator: Adds all functions for CRUD (Create, Read, Update and Delete), aslo extra items like count. CRUD works with MongoDB and ElastichSearch
var decorators = [ crudDecorator ];
// MongoDB Schema
var LocalSchema = new Schema({
type: { type: Number, required: true },
shareCount: { type: Number, index: true, default: 0 },
node: { type: Schema.Types.ObjectId, required: true },
nodeModel: { type: String, required: true },
relatedNode: { type: Schema.Types.ObjectId },
relatedNodeModel: { type: String },
privacyLevel: { type: Number, required: true },
deleted: { type: Boolean, required: true, default: false },
shareOwnerId: { type: Schema.Types.ObjectId }, // The original owner of the shared activity
shareText: { type: String }
});
// ElasticSearch Mapping
var ESMapping = {
mappings: {
"properties": {
// Behavior and Decorators
"ownerId": {type: "string", "index": "not_analyzed"},
"privacyLevel": {type: "long"},
"created": {
"type": "date",
"format": "dateOptionalTime"
},
"modified": {
"type": "date",
"format": "dateOptionalTime"
},
"comments": {
"type" : "nested",
"include_in_parent": true,
"properties": {
"_id": {type: "string"},
"text": {type: "string"},
"userId": {type: "string", "index": "not_analyzed"},
"created": {
"type": "date",
"format": "dateOptionalTime"
}
}
},
"likes": {
"properties": {
"userId": {type: "string", "index": "not_analyzed"}
}
},
// Fields
"type": {
"type": "long"
},
"shareCount": {
"type": "long"
},
"node": {
"type": "string", "index": "not_analyzed"
},
"nodeModel": {
"type": "string", "index": "not_analyzed"
},
"relatedNode": {
"type": "string", "index": "not_analyzed"
},
"relatedNodeModel": {
"type": "string", "index": "not_analyzed"
},
"deleted": {
"type": "boolean"
},
"shareOwnerId": {
"type": "string", "index": "not_analyzed"
},
"shareText": {
"type": "string",
"analyzer": "portuguese"
}
}
}
};
// MongoDB Indexes
LocalSchema.plugin(Mongoosastic, _.assign(Domains.serverconfig().es, ESMapping));
// search
LocalSchema.index({ "ownerId" : 1, "privacyLevel" : 1 });
LocalSchema.index({ "ownerId" : 1, "privacyLevel" : 1, created: -1 });
LocalSchema.plugin(SanitizerPlugin, { include: ['shareText'] });
var selectFields = "";
// CRUD callback operations
LocalSchema.statics.before = function(type, options, data, cb) {
if(type === 'remove') {
// verify if the user owns this activity to be able to remove it
var user = HD.getUser(options.request);
model.findById(options.params.id).lean().exec(function(err, retData) {
if(err || retData == null)
cb(HD.errors.unauthorizedAction, options, data);
else {
if(retData.ownerId.toString() !== user._id.toString()) {
cb(HD.errors.unauthorizedAction, options, data);
} else {
cb(null, options, data);
}
}
});
} else {
cb(null, options, data);
}
}
// Local functions
LocalSchema.statics._prepareActivity = function(item, options, cb) {
var defineShareOwnerDetails = function(cbA){
if(!_.isUndefined(item.shareOwnerId)) {
model.descriptions.users(options, item.shareOwnerId, function(err, desc, avatar, postData, isValid) {
item.isValid = isValid;
if (_.isEmpty(postData))
postData = {};
postData.name = desc;
postData.avatar = avatar;
item.shareOwnerPostData = postData;
cbA(null);
});
} else
cbA(null);
};
var defineOwnerDetails = function(cbA){
model.descriptions.users(options, item.ownerId, function(err, desc, avatar, postData, isValid) {
item.isValid = isValid;
if(_.isEmpty(postData))
postData = {};
postData.name = desc;
postData.avatar = avatar;
item.ownerPostData = postData;
cbA(null);
});
};
var defineNodeDescription = function(cbA){
model.descriptions[item.nodeModel](options, item.node, function(err, desc, avatar, postData, isValid) {
item.isValid = isValid;
if(_.isEmpty(postData))
postData = {};
postData.commentsCount = item.comments ? item.comments.length : 0;
postData.description = desc;
postData.avatar = avatar;
postData.likes = item.likes ? item.likes : [];
postData.allowEdit = options.user._id.toString() === item.ownerId.toString();
item.nodePostData = postData;
if(item.comments && item.comments.length > 0) {
var aComments = _.sortBy(item.comments, function(itemSort) { return itemSort.created; }).reverse().splice(0, 3).reverse();
// get at most 3 latest comments
if(aComments.length === 0) {
cbA(null);
return;
}
var funcs = [];
_.each(aComments, function(item) {
funcs.push(function(next) {
var userId = item.userId.toString();
var _callNext = function(err, userData) {
if(!err) {
item.ownerName = userData.name;
item.ownerAvatar = userData.avatar;
item.allowEdit = userId === userData._id.toString();
}
next();
}
Domains.redis().getCache('activity', userId, function(err, retData) {
if(!err && retData != null) {
_callNext(null, retData);
} else {
Domains.users().getModelData(userId, function(err, retUser) {
_callNext(err, retUser);
});
}
});
});
});
Async.parallel(funcs, function(err) {
item.nodePostData.comments = aComments;
cbA(null);
});
} else {
item.nodePostData.comments = [];
cbA(null);
}
});
};
var defineRelatedNodeDescription = function(cbA){
if(!_.isUndefined(item.relatedNode) && !_.isNull(item.relatedNode))
model.descriptions[item.relatedNodeModel](options, item.relatedNode, function(err, desc, avatar, postData, isValid) {
item.isValid = isValid;
if(_.isEmpty(postData))
postData = {};
postData.description = desc;
postData.avatar = avatar;
item.relatedPostData = postData;
cbA(null);
});
else
cbA();
};
if (options.api !== "admin")
delete item.edges;
Async.parallel([defineShareOwnerDetails, defineOwnerDetails, defineNodeDescription, defineRelatedNodeDescription], function(err) {
cb(err, item);
});
};
LocalSchema.statics._prepareActivities = function(options, data, cb) {
if (data && data.length > 0) {
var _prepareActivityHelper = function(item, cbItem) {
model._prepareActivity(item, options, cbItem);
};
Async.map(data, _prepareActivityHelper, function(err, preparedItems){
data = preparedItems;
// remove invalid activities
var itemsToRemove = [];
data = _.filter(data, function(item) {
var isValid = item.isValid;
delete item.isValid;
if(!isValid)
Domains.redis().delCache('activity', item._id.toString());
return isValid;
});
Async.parallel(itemsToRemove, function(removeErr){
cb(err, data);
});
});
} else {
cb(null, data);
}
}
// Overwrite the CRUD search operation to search specifically on ElasticSearch for activities
// ES Search -- CANNOT use CRUD, since CRUD is completely different
LocalSchema.statics._prepareItemsES = function(items) {
var aItems = [];
_.each(items, function(item) {
if(item._source) {
item._source._id = item._id;
aItems.push(item._source);
}
});
return aItems;
}
LocalSchema.statics._searchES = function(options, searchQuery, cb) {
model.search(searchQuery, { directQuery: true }, function(err, resultSearch) {
var retData = { items: [], count: 0 };
if(!err && resultSearch) {
if(parseInt(resultSearch.took) > 2000 && resultSearch.hits.total > 0) {
console.log('ES Time took: ' + resultSearch.took);
console.log(JSON.stringify(searchQuery))
}
if (resultSearch.hits) {
retData.items = model._prepareItemsES(resultSearch.hits.hits);
retData.count = resultSearch.hits.total;
}
if (resultSearch.aggregations)
retData.aggregate = resultSearch.aggregations;
}
model._prepareActivities(options, retData.items, function(err, retData) {
cb(err, retData);
});
});
}
LocalSchema.statics._getPayloadValues = function(payload, mustAndFilters) {
_.each(mustAndFilters, function(item) {
var keyObj = item.term;
if(!_.isEmpty(keyObj)) {
var key = _.keys(keyObj)[0];
payload[key] = keyObj[key];
}
});
delete payload.mustAndFilters;
}
LocalSchema.statics.getQueryOverideES = function(options, cb) {
var searchQuery = { size: 10, "query": { "filtered": { "filter": { } } } };
var searchFilter = {};
var userReadLevel = userPrivacy.PUBLIC;
model._getPayloadValues(options.payload, options.payload.mustAndFilters);
var page = options.query.page ? parseInt(options.query.page) : 1;
var size = 10;
if(options.payload.limit)
size = options.payload.limit;
else if(options.query.perPage) {
size = options.query.perPage;
}
searchQuery.size = size;
searchQuery.from = (page - 1) * size;
searchQuery.sort = { "created": { "order": "desc" } };
var _search = function() {
searchQuery.query.filtered.filter = searchFilter;
var redisField = HD.redisKeyFromObj(searchQuery);
Domains.redis().getIndexCache('activitiess:activities', redisField, function(err, retData) {
if(!err && retData != null) {
HD.callCallback(cb, HD.checkMongoErr(err), retData);
} else {
model._searchES(options, searchQuery, function(err, retData) {
Domains.redis().setIndexCache('activitiess:activities', redisField, retData);
HD.callCallback(cb, HD.checkMongoErr(err), retData)
});
}
});
}
var _searchOpen = function() {
searchFilter.and = { filters: [] };
searchFilter.and.filters.push({
term: { "privacyLevel": userPrivacy.PUBLIC }
});
_search();
}
var _searchUserJustMine = function(userOwnerId, readLevel) {
searchFilter.and = { filters: [] };
searchFilter.and.filters.push({
term: { "ownerId": userOwnerId }
});
searchFilter.and.filters.push({
"range": {
"privacyLevel": {
"lte": userReadLevel
}
}
});
_search();
}
var _searchUserFriends = function(userOwnerId, readLevel, aFriendsIds) {
searchFilter = {
"or": {
"filters": [
{
"and": {
"filters": [
{
"term": {
"ownerId": userOwnerId,
"_cache": true
}
},
{
"range": {
"privacyLevel": {
"lte": userReadLevel
}
}
}
]
}
},
{
"and": {
"filters": [
{
"terms": {
"ownerId": aFriendsIds,
"execution" : "fielddata",
"_cache": true
}
},
{
"range": {
"privacyLevel": {
"gt": userPrivacy.ONLY_ME,
"lte": readLevel
}
}
}
]
}
}
]
}
};
_search();
}
var _searchOrgOrGroup = function(readLevel) {
searchFilter = {
"and": {
"filters": [
{
"range": {
"privacyLevel": {
"lte": readLevel
}
}
},
{
"or": {
"filters": [
{
"term": {
"node": options.payload.node
}
},
{
"term": {
"relatedNode": options.payload.node
}
}
]
}
}
]
}
};
_search();
}
if(!options.user || _.isNull(options.user)) {
_searchOpen();
} else {
var queryType = options.payload.queryType;
// data that users WANTS to see
var readLevel = userPrivacy.MY_FRIENDS;
if (HD.T.has(options.user.config, 'activities.privacy.read'))
readLevel = options.user.config['activities'].privacy.read;
if(queryType === 'user') {
var userOwner = options.user;
var userOwnerId = userOwner._id.toString();
// looks for friend data, but if we just want to see the users data, don't include friends data
if (_.isUndefined(options.payload.justMine))
justMine = false;
else
justMine = options.payload.justMine;
if(justMine) {
_searchUserJustMine(userOwnerId, readLevel);
} else {
if(readLevel >= userPrivacy.MY_FRIENDS) {
var userOwner = options.user;
var edges = userOwner.edges;
var aFriendsIds = [];
_.each(edges, function(e) {
if(e.edgeType === edgeType.FRIEND && e.status === edgeStatus.APPROVED) {
aFriendsIds.push(e.node);
}
});
if(aFriendsIds.length > 0)
_searchUserFriends(userOwnerId, readLevel, aFriendsIds);
else
_searchUserJustMine(userOwnerId, readLevel);
} else {
_searchUserJustMine(userOwnerId, readLevel);
}
}
} else if(queryType === 'group' || queryType === 'organization') {
_searchOrgOrGroup(readLevel);
} else {
HD.callCallback(cb, null, { items: [], count: 0 });
}
}
}
LocalSchema.statics.getQueryOveride = function(options, cb) {
if(options.esSearch) {
model.getQueryOverideES(options, cb);
} else {
model.getQueryOverideMongo(options, cb);
}
}
// Activite Description Functions
LocalSchema.statics.descriptions = {};
LocalSchema.statics.descriptions.users = function(options, userId, cb) {
var _retData = function(err, data) {
var postData = {};
if(data) {
postData = {
occupation: data.occupation,
gender: data.gender
};
}
cb(err, (data ? data.name : null),
(data ? data.avatar : null),
postData,
(data ? true : false));
}
Domains.redis().getCache('activity', userId, function(err, retData) {
if(!err && retData != null)
_retData(null, retData);
else {
Domains.users().getModelData(userId, '_id occupation gender name avatar', function(err, data) {
if(!err && data != null)
Domains.redis().setCache('activity', userId, data);
_retData(err, data);
});
}
});
};
LocalSchema.statics.descriptions.groups = function(options, groupId, cb) {
var _retData = function(err, data) {
var postData = {};
if(!err && data != null) {
postData = {
allowEdit: data.allowEdit,
category: data.category,
connectionsCount: data.connectionsCount,
description: data.description,
isUserFollower: data.isUserFollower,
isUserMember: data.isUserMember,
isUserPending: data.isUserPending,
isUserRejected: data.isUserRejected,
allowFollow: !data.isUserMember && !data.isUserFollower,
allowUnfollow: !data.isUserMember && data.isUserFollower,
allowJoinGroup: !data.isUserMember && !data.isUserPending && !data.isUserRejected,
allowLeaveGroup: data.isUserMember && !data.allowEdit, // if user is owner, cannot leave
allowCancelJoinRequest: data.isUserPending,
userEdgeFollowerId: data.userEdgeFollowerId,
showFollowing: !data.isUserMember && !data.isUserPending && data.isUserFollower,
tags: data.tags,
topicsCount: data.topicsCount
};
}
cb(err, (data ? data.name : null),
(data ? data.avatar : null),
postData,
(data && data.status === itemStatus.APPROVED ? true : false));
}
Domains.redis().getCache('activity', groupId, function(err, retData) {
if(!err && retData != null)
_retData(null, retData);
else {
Domains.groups().get({user: options.user, params: {id: groupId}}, function(err, data) {
if(!err && data != null)
Domains.redis().setCache('activity', groupId, data);
_retData(err, data);
});
}
});
};
LocalSchema.statics.descriptions.jobs = function(options, jobId, cb) {
var _retData = function(err, data) {
var postData = {};
if(!err && data != null) {
postData = {
workPlaces: data.workPlaces,
country: data.country,
type: data.type,
responsibilities: data.responsibilities
};
}
cb(err, (data ? data.description : null),
null,
postData,
(data ? true : false));
}
Domains.redis().getCache('activity', jobId, function(err, retData) {
if(!err && retData != null)
_retData(null, retData);
else {
Domains.jobs().get({user: options.user, params: {id: jobId}}, function(err, data) {
if(!err && data != null)
Domains.redis().setCache('activity', jobId, data);
_retData(err, data);
});
}
});
}
LocalSchema.statics.descriptions.classifieds = function(options, classifiedId, cb) {
var _retData = function(err, data) {
var postData = {};
if(!err && data != null) {
postData = {
cityState: data.cityState,
country: data.country,
tags: data.tags,
title: data.title,
text: data.text,
zipcode: data.zipcode,
comments: data.comments ? data.comments : [],
allowEdit: data.allowEdit
};
}
cb(err, (data ? data.title : null),
null,
postData,
(data ? true : false));
}
Domains.redis().getCache('activity', classifiedId, function(err, retData) {
if(!err && retData != null)
_retData(null, retData);
else {
Domains.classifieds().get({user: options.user, params: {id: classifiedId}}, function(err, data) {
if(!err && data != null)
Domains.redis().setCache('activity', classifiedId, data);
_retData(err, data);
});
}
});
}
LocalSchema.statics.descriptions.topics = function(options, topicId, cb) {
var _retData = function(err, data) {
if(!err && data != null)
data.shareCount = 0;
cb(err, (data ? data.title : null),
null,
(data ? data : {}),
(data ? true : false));
}
Domains.redis().getCache('activity', topicId, function(err, retData) {
if(!err && retData != null)
_retData(null, retData);
else {
Domains.topics().get({user: options.user, params: {id: topicId}}, function(err, data) {
if(!err && data != null)
Domains.redis().setCache('activity', topicId, data);
_retData(err, data);
});
}
});
}
LocalSchema.statics.descriptions.recommendations = function(options, recommendationId, cb) {
var _retData = function(err, data) {
cb(err, (data ? data.description : null),
null,
(data ? data : {}),
(data ? true : false));
}
Domains.redis().getCache('activity', recommendationId, function(err, retData) {
if(!err && retData != null)
_retData(null, retData);
else {
Domains.recommendations().get({user: options.user, params: {id: recommendationId}}, function(err, data) {
if(!err && data != null)
Domains.redis().setCache('activity', recommendationId, data);
_retData(err, data);
});
}
});
}
LocalSchema.statics.descriptions.organizationpages = function(options, organizationPageid, cb) {
var _retData = function(err, data) {
var postData = {};
if(!err && data != null) {
postData = {
allowEdit: data.allowEdit,
category: data.category,
connectionsCount: data.connectionsCount,
description: data.description,
isUserFollower: data.isUserFollower,
isUserMember: data.isUserMember,
isUserPending: data.isUserPending,
isUserRejected: data.isUserRejected,
allowFollow: !data.isUserMember && !data.isUserFollower,
allowUnfollow: !data.isUserMember && data.isUserFollower,
allowJoinGroup: !data.isUserMember && !data.isUserPending && !data.isUserRejected,
allowLeaveGroup: data.isUserMember && !data.allowEdit, // if user is owner, cannot leave
allowCancelJoinRequest: data.isUserPending,
userEdgeFollowerId: data.userEdgeFollowerId,
showFollowing: !data.isUserMember && !data.isUserPending && data.isUserFollower,
tags: data.tags,
topicsCount: data.topicsCount
};
}
cb(err, (data ? data.name : null),
(data ? data.avatar : null),
postData,
(data && data.status === itemStatus.APPROVED ? true : false));
}
Domains.redis().getCache('activity', organizationPageid, function(err, retData) {
if(!err && retData != null)
_retData(null, retData);
else {
Domains.organizationpages().get({user: options.user, params: {id: organizationPageid}}, function(err, data) {
Domains.redis().setCache('activity', organizationPageid, data);
_retData(err, data);
});
}
});
}
// Share Functions
LocalSchema.statics.share = function(options, cb) {
var funcs = [];
var activityId = options.params.activityId;
var activityData = {};
var user = options.user;
var retActivity = {};
// get the activity
funcs.push(function(next) {
model.findById(activityId).lean().exec(function(err, retData) {
if(!err && retData != null)
activityData = retData;
next(err);
});
});
// add the new activity
funcs.push(function(next) {
var privacyLevel = options.payload.privacyLevel;
var shareOwnerId = HD.ObjectID(activityData.shareOwnerId ? activityData.shareOwnerId : activityData.ownerId);
delete activityData._id;
delete activityData.created;
activityData.ownerId = HD.ObjectID(user._id);
activityData.privacyLevel = privacyLevel;
activityData.shareOwnerId = shareOwnerId;
if(!_.isUndefined(options.payload.shareText) && options.payload.shareText !== '')
activityData.shareText = options.payload.shareText;
activityData.comments = [];
activityData.likes = [];
model.findOne({ shareOwnerId: activityData.shareOwnerId, node: activityData.node}).lean().exec(function(err, retData) {
if(err || retData != null) {
next(null);
} else {
model.create(activityData, function(err, newActivity) {
if(!err && newActivity != null)
retActivity = newActivity;
next(err);
});
}
});
});
Async.series(funcs, function(err) {
cb(err, retActivity);
});
}
LocalSchema.statics.shareGroup = function(options, cb) {
var funcs = [];
var groupId = options.params.groupId;
var groupData = {};
var user = options.user;
var retActivity = {};
// get the group
funcs.push(function(next) {
Domains.groups().dcrud.findById(groupId).lean().exec(function(err, retData) {
if(!err && retData != null)
groupData = retData;
next(err);
});
});
// add the new activity
funcs.push(function(next) {
model.findOne({ type: Behaviours.activities.types.GROUP_SHARE, node: groupId, relatedNode: user._id }).lean().exec(function(err, retData) {
if(err || retData != null) {
next(null);
} else {
var activityData = {
type: Behaviours.activities.types.GROUP_SHARE,
node: groupData._id,
nodeModel: 'groups',
relatedNode: user._id,
relatedNodeModel: 'users',
privacyLevel: options.payload.privacyLevel,
ownerId: user._id,
comments: [],
likes: []
};
model.create(activityData, function(err, newActivity) {
if(!err && newActivity != null)
retActivity = newActivity;
next(err);
});
}
});
});
Async.series(funcs, function(err) {
cb(err, retActivity);
});
}
LocalSchema.statics.shareOrg = function(options, cb) {
var funcs = [];
var orgId = options.params.orgId;
var orgData = {};
var user = options.user;
var retActivity = {};
// get the group
funcs.push(function(next) {
Domains.organizationpages().dcrud.findById(orgId).lean().exec(function(err, retData) {
if(!err && retData != null)
orgData = retData;
next(err);
});
});
// add the new activity
funcs.push(function(next) {
model.findOne({ type: Behaviours.activities.types.ORGPAGE_SHARE, node: orgId, relatedNode: user._id }).lean().exec(function(err, retData) {
if(err || retData != null) {
next(null);
} else {
var activityData = {
type: Behaviours.activities.types.ORGPAGE_SHARE,
node: orgData._id,
nodeModel: 'organizationpages',
relatedNode: user._id,
relatedNodeModel: 'users',
privacyLevel: options.payload.privacyLevel,
ownerId: user._id,
comments: [],
likes: []
};
model.create(activityData, function(err, newActivity) {
if(!err && newActivity != null)
retActivity = newActivity;
next(err);
});
}
});
});
Async.series(funcs, function(err) {
cb(err, retActivity);
});
}
LocalSchema.statics.findAndRemove = function(conditions, cb) {
model.update(conditions, {deleted: true}, {multi: true}, function(err, updCount) {
cb(err, updCount);
})
}
// Add Behaviours and Decorators to the model
Behaviours.api.addPlugins(LocalSchema, behaviours);
Decorators.api.addPlugins(LocalSchema, decorators);
// Creates the model and indexes on ElasticsSearch
var model = Mongoose.model(modelName, LocalSchema);
var elastic = new HD.elastic(model, LocalSchema);
elastic.createMapping();
// Public domain functions on the model
var domain = {
elastic: elastic,
types: Behaviours.activities.types,
selectFields: selectFields,
findAndRemove: model.findAndRemove,
share: model.share,
shareGroup: model.shareGroup,
shareOrg: model.shareOrg
};
Behaviours.api.prepareModel(domain, model, behaviours);
Decorators.api.prepareModel(domain, model, decorators);
// Export the functions to be used
module.exports = domain;
var _ = require('lodash');
var HD = require('hd').utils;
var Domains = require('hd').domains;
var HP = require('hd').plugin;
var Types = require('joi');
var Mongoose = require('mongoose');
var Async = require('async');
var Schema = Mongoose.Schema;
// Declare plugin fields
var fields = [
{
ownerId: { type: Schema.Types.ObjectId, index: true },
created: { type: Date, required: true, default: Date.now },
modified: { type: Date, fulltext: false }
}
];
// PLUGIN CONSTRUCTION
function initPlugin(schema) {
HP.initPlugin(schema, fields);
}
function CRUD(model, options) {
var self = this;
self.keepOnRemove = _.isUndefined(options.keepOnRemove) ? false : options.keepOnRemove;
self.putPayload = _.isUndefined(options.putPayload) ? "parse" : options.putPayload;
self.cache = _.isUndefined(options.cache) ? false : options.cache;
self.crudOveride = _.assign(
{
getQuery: false
},
options.crudOveride
);
self.model = model;
self.domain = {};
self.exposeRoutes = _.assign(options.exposeRoutes ? options.exposeRoutes : {},
{
getQuery: { admin: true, api: true },
get: { admin: true, api: true },
remove: { admin: true, api: true },
add: { admin: true, api: true },
update: { admin: true, api: true },
exist: { admin: true, api: true },
count: { admin: true, api: true }
});
self.selectFields = function() {
var fields = "";
if(!_.isEmpty(self.domain) && self.domain.selectFields)
fields = self.domain.selectFields;
if(fields.indexOf('searchIdx') === -1)
fields += ' -searchIdx';
if(fields.indexOf('aggregateIdx') === -1)
fields += ' -aggregateIdx';
if(fields.indexOf('privacyLevel') === -1)
fields += ' -privacyLevel';
return fields.trim();
};
self.routes = { admin: [], api: [] };
if(self.exposeRoutes.getQuery.admin) {
self.routes.admin.push({
method: 'POST',
path: '/admin/{model}',
config: {
auth: true,
handler: self.handlerAdmin.getQuery.bind(self),
payload: 'parse'
}
});
}
if(self.exposeRoutes.get.admin) {
self.routes.admin.push({
method: 'GET',
path: '/admin/{model}/{id}',
config: {
auth: true,
handler: self.handlerAdmin.get.bind(self)
}
});
}
if(self.exposeRoutes.remove.admin) {
self.routes.admin.push({
method: 'DELETE',
path: '/admin/{model}/{id}',
config: {
auth: true,
handler: self.handlerAdmin.remove.bind(self)
}
});
}
if(self.exposeRoutes.add.admin) {
self.routes.admin.push({
method: 'PUT',
path: '/admin/{model}',
config: {
auth: true,
handler: self.handlerAdmin.add.bind(self),
payload: 'parse'
}
});
}
if(self.exposeRoutes.update.admin) {
self.routes.admin.push({
method: 'PUT',
path: '/admin/{model}/{id}',
config: {
auth: true,
handler: self.handlerAdmin.update.bind(self),
payload: 'parse'
}
});
}
if(self.exposeRoutes.exist.admin) {
self.routes.admin.push({
method: 'PUT',
path: '/admin/{model}/exist',
config: {
auth: true,
handler: self.handlerAdmin.exist.bind(self),
payload: self.putPayload
}
});
}
if(self.exposeRoutes.count.admin) {
self.routes.admin.push({
method: 'POST',
path: '/admin/{model}/count',
config: {
auth: true,
handler: self.handlerAdmin.count.bind(self),
payload: 'parse'
}
});
}
if(self.exposeRoutes.getQuery.api) {
self.routes.api.push({
method: 'POST',
path: '/{model}',
config: {
auth: 'try',
handler: self.handler.getQuery.bind(self),
payload: 'parse',
notes: ['Does not requires authentication'],
tags: ['CRUD']
}
});
}
if(self.exposeRoutes.get.api) {
self.routes.api.push({
method: 'GET',
path: '/{model}/{id}',
config: {
auth: 'try',
handler: self.handler.get.bind(self),
description: 'Returns the domain by its id',
notes: ['Does not requires authentication'],
tags: ['CRUD']
}
});
}
if(self.exposeRoutes.remove.api) {
self.routes.api.push({
method: 'DELETE',
path: '/{model}/{id}',
config: {
auth: true,
handler: self.handler.remove.bind(self),
description: 'Removes the domain by its id',
notes: ['Requires authentication', 'Requires owner permissions'],
tags: ['CRUD']
}
});
}
if(self.exposeRoutes.add.api) {
self.routes.api.push({
method: 'PUT',
path: '/{model}',
config: {
auth: true,
handler: self.handler.add.bind(self),
payload: self.putPayload,
timeout: {
socket: 13 * 60 * 1000,
server: 12 * 60 * 1000
},
description: 'Adds a new item to domain ',
notes: ['Requires authentication', 'Requires owner permissions'],
tags: ['CRUD']
}
});
}
if(self.exposeRoutes.update.api) {
self.routes.api.push({
method: 'PUT',
path: '/{model}/{id}',
config: {
auth: 'try',
handler: self.handler.update.bind(self),
payload: 'parse',
description: 'Updates the domain by its id',
notes: ['Requires authentication', 'Requires owner permissions'],
tags: ['CRUD']
}
});
}
if(self.exposeRoutes.exist.api) {
self.routes.api.push({
method: 'PUT',
path: '/{model}/exist',
config: {
auth: false,
handler: self.handler.exist.bind(self),
payload: 'parse',
description: 'Checks if the document item exists',
notes: ['Does not require authentication'],
tags: ['CRUD']
}
});
}
if(self.exposeRoutes.count.api) {
self.routes.api.push({
method: 'POST',
path: '/{model}/count',
config: {
auth: false,
handler: self.handler.count.bind(self),
payload: 'parse',
notes: ['Does not requires authentication'],
tags: ['CRUD']
}
});
}
self.exports = {
get: self.get.bind(self),
getQuery: self.getQuery.bind(self),
update: self.update.bind(self),
add: self.add.bind(self),
remove: self.remove.bind(self),
removeAll: self.removeAll.bind(self),
exist: self.exist.bind(self),
count: self.count.bind(self),
dcrud: model
}
}
// ****************
// Internal Methods
// ****************
CRUD.prototype._get = function(options, cb) {
var self = this;
var doCache = self.cache && self.cache.actions.indexOf('get') !== -1;
var _get = function() {
self.model.findById(options.params.id, self.selectFields()).lean().exec(function(err, doc) {
HD.callCallback(cb, err, doc);
});
}
if(doCache) {
Domains.redis().getCache(self.cache.id, options.params.id.toString(), function(err, retData) {
if(!err && retData != null) {
HD.callCallback(cb, null, retData);
} else {
_get();
}
});
} else {
_get();
}
}
// 'There is always a limit of 100.',
// 'To get all other docs, use pagination by passing on the query the following paramenters.',
// 'page: the page number you want',
// 'perPage: total records per page',
// 'To get the organization by the username, pass the username on the query. But if you use this parameter, paging will be disabled.'
CRUD.prototype._getQueryMongo = function(options, cb) {
var self = this;
var query = _.isUndefined(options.query) ? {} : options.query;
var payload = _.isUndefined(options.payload) ? {} : options.payload;
var aggregate = [];
var noLimit = options.noLimit || false;
var page = 0;
var perPage = 0;
var fields = HD.createFields(self.model.schema.paths, options.selectFields && options.selectFields !== "" ? options.selectFields : self.selectFields());
var sort = {};
var geoNear = false;
var cacheCountLimit = 100;
var dataLimit = 100;
// work with query
if(!_.isUndefined(query.page))
page = parseInt(query["page"]);
if(!_.isUndefined(query.perPage))
perPage = parseInt(query["perPage"]);
// Work with the payload, it has to respect this order (and has to be at the end)
if(!_.isUndefined(payload["sort"])){
sort = payload["sort"];
delete payload["sort"];
}
if(!_.isUndefined(payload["geoNear"])) {
var geoNear = payload["geoNear"];
var distance = parseInt(geoNear.distance) * 1000;
var lng = parseFloat(geoNear.coordinates[0]);
var ltd = parseFloat(geoNear.coordinates[1]);
geoNear = { $geoNear: {
near: { type: "Point", coordinates: [lng, ltd] },
distanceField: "distCalculated",
maxDistance: distance,
spherical: true,
uniqueDocs: true,
distanceMultiplier: 6378.1 } };
delete payload["geoNear"];
}
var match = HD.query.prepare(options.payload);
// start aggregation structure
if(geoNear) {
// has to come first on the aggreation pipiline
aggregate.push(geoNear);
fields['distCalculated'] = 1;
sort = _.assign({ distCalculated: 1 }, sort);
}
if(!_.isEmpty(match))
aggregate.push({ $match: match });
var timer = new HD.timer();
timer.init();
if(page !== 0 && perPage !== 0) {
// get the count
aggregate.push({ $group: { _id: null, total: { $sum: 1 } } } );
var _return = function(aggregate, err, retData) {
timer.markIf(aggregate, 1);
timer.log('GQ');
HD.callCallback(cb, HD.checkMongoErr(err), retData);
}
var _postCount = function(err, docs) {
timer.mark('A crudquerycount');
if(err || docs == null || docs.length === 0 || docs[0].total === 0) {
_return(aggregate, err, {count: 0, items: []});
} else {
var count = docs[0].total;
if(perPage > count)
perPage = count;
aggregate.pop(); // remove the count
var _getDocs = function(aggregateResult) {
var skip = (page - 1) * perPage;
if(skip > 0)
aggregate.push({ $skip: skip });
if(!_.isEmpty(sort))
aggregate.push({ $sort: sort });
aggregate.push({ $limit: perPage });
if(!_.isEmpty(fields))
aggregate.push({ $project: fields });
// return payload with internal search structure
options.payload = match;
timer.mark('B _getDocs');
self.model.aggregate(aggregate).allowDiskUse(true).exec(function(err, docs) {
timer.mark('A _getDocs');
var resultSearch = {count: 0, items: []};
if (!err && docs != null && docs.length > 0) {
resultSearch.count = count;
resultSearch.items = docs;
if(aggregateResult)
resultSearch.aggregate = aggregateResult ? aggregateResult : {};
}
_return(aggregate, err, resultSearch);
});
};
if(options && options.query && options.query.aggregate) {
var aggregateOptions = [];
if(geoNear)
aggregateOptions.push(geoNear);
aggregateOptions.push({ $match: match });
aggregateOptions.push({ $project: { 'aggregateIdx.type': 1, 'aggregateIdx.value': 1 } });
aggregateOptions.push({ $limit: 10000 });
aggregateOptions.push({ $unwind: '$aggregateIdx' });
aggregateOptions.push({ $group: { _id: { type: '$aggregateIdx.type', value: '$aggregateIdx.value' }, total: { $sum: 1 } } });
var aggKey = count + ': ' + HD.redisKeyFromObj(aggregateOptions);
timer.mark('B crudqueryagg');
Domains.redis().getCache('crudqueryagg', aggKey, function(err, retAggCache) {
timer.mark('A crudqueryagg');
if(!err && retAggCache != null) {
_getDocs(retAggCache);
} else {
self.model.aggregate(aggregateOptions).allowDiskUse(true).exec(function(err, aggregateResult) {
if(err)
HD.callCallback(cb, HD.checkMongoErr(err), []);
else {
if(count >= cacheCountLimit)
Domains.redis().setCache('crudqueryagg', aggKey, aggregateResult);
_getDocs(aggregateResult);
}
});
}
});
} else {
_getDocs();
}
}
}
var countKey = HD.redisKeyFromObj(aggregate);
timer.mark('B crudquerycount');
Domains.redis().getCache('crudquerycount', countKey, function(err, retCountCache) {
if(!err && retCountCache != null) {
_postCount(null, retCountCache);
} else {
self.model.aggregate(aggregate).allowDiskUse(true).exec(function(err, docs) {
if(!err && docs != null && docs.length > 0 && docs[0].total >= cacheCountLimit)
Domains.redis().setCache('crudquerycount', countKey, docs);
_postCount(err, docs);
});
}
});
} else {
var q = self.model.find(match);
if(!_.isEmpty(sort))
q.sort(sort);
if(!noLimit) {
q.limit(options.limit ? parseInt(options.limit) : dataLimit);
}
if(!_.isEmpty(fields))
q.select(fields);
// return payload with internal search structure
options.payload = match;
timer.mark('Before find');
q.lean().exec(function(err, docs) {
timer.mark('After find');
timer.markIf({ match: match, sort: sort, limit: options.limit ? parseInt(options.limit) : dataLimit }, 1);
timer.log('GQ - FIND');
HD.callCallback(cb, HD.checkMongoErr(err), docs);
});
}
}
CRUD.prototype._prepareItemsES = function(items) {
var aItems = [];
_.each(items, function(item) {
if(item._source) {
item._source._id = item._id;
aItems.push(item._source);
}
});
return aItems;
}
CRUD.prototype._getQueryES = function(options, cb) {
var self = this;
var query = _.isUndefined(options.query) ? {} : options.query;
var payload = _.isUndefined(options.payload) ? {} : options.payload;
var searchQuery = {};
var noLimit = options.noLimit || false;
var page = 0;
var perPage = 0;
var limit = (noLimit ? 0 : (options.limit ? options.limit : 100));
// Query (full text) - expect any formatted ES query
if(options.payload.esQuery)
searchQuery = {query: {filtered: {query: options.payload.esQuery}}};
// Full text - expect any well formatted ES filter
if(options.payload.mustAndFilters && options.payload.mustAndFilters.length > 0) {
if(_.isEmpty(searchQuery))
searchQuery = {query: {filtered: {filter: { bool: { must: { and: { filters: [] } } } } } } };
else
searchQuery.query.filtered.filter = { bool: { must: { and: { filters: [] } } } };
searchQuery.query.filtered.filter.bool.must.and.filters = options.payload.mustAndFilters;
} else if(options.payload.esFilter) {
if(_.isEmpty(searchQuery))
searchQuery = {query: {filtered: {filter: {} }}};
searchQuery.query.filtered.filter = options.payload.esFilter;
}
// Work with the payload, it has to respect this order (and has to be at the end)
if(options.payload.esSort && _.isEmpty(options.payload.esQuery))
searchQuery.sort = payload.esSort;
else
searchQuery.sort = ['_score'];
if(!_.isUndefined(query.perPage)) {
searchQuery.size = parseInt(query["perPage"]);
} else {
if(noLimit)
searchQuery.size = 0;
else
searchQuery.size = limit;
}
if(!_.isUndefined(query.page)) {
var page = parseInt(query["page"])
var skip = (page - 1) * searchQuery.size;
if(skip > 0)
searchQuery.from = skip;
}
if(!_.isUndefined(payload["esAggs"]))
searchQuery.aggs = payload["esAggs"];
self.model.search(searchQuery, { directQuery: true }, function(err, resultSearch) {
var retData = { items: [], count: 0 };
if(!err && resultSearch) {
if(parseInt(resultSearch.took) > 2000 && resultSearch.hits.total > 0) {
console.log('ES Time took: ' + resultSearch.took);
console.log(JSON.stringify(searchQuery))
}
if (resultSearch.hits) {
retData.items = self._prepareItemsES(resultSearch.hits.hits);
retData.count = resultSearch.hits.total;
}
if (resultSearch.aggregations)
retData.aggregate = resultSearch.aggregations;
}
HD.callCallback(cb, HD.checkMongoErr(err), retData);
});
}
CRUD.prototype._getQuery = function(options, cb) {
var self = this;
if(self.crudOveride && self.crudOveride.getQuery) {
self.crudOveride.getQuery(options, cb);
} else {
if (options.esSearch) {
self._getQueryES(options, cb);
} else {
self._getQueryMongo(options, cb);
}
}
}
CRUD.prototype._add = function(options, cb) {
var self = this;
var data = options.payload;
data.ownerId = HD.ObjectID(options.ownerId);
//console.log('CRUD Before _add');
self.model.create(data, function(err, doc) {
//console.log('CRUD After _add');
var localErr = HD.checkMongoErr(err);
var localDoc = data;
if(localErr)
if(localErr.code === 11000) {
localErr = HD.errors.duplicateDocument;
} else
localErr = err;
else if(_.isUndefined(doc))
localErr = HD.errors.invalidDocument;
else {
localDoc = HD.prepareReturnData(self.model, doc);
}
if(localErr) {
console.log("CRUD ERROR _add");
console.dir(localErr);
}
HD.callCallback(cb, localErr, localDoc);
});
}
CRUD.prototype._remove = function(options, cb) {
var self = this;
var id = options.params.id;
var doCache = self.cache && self.cache.actions.indexOf('remove') !== -1;
var loggedUser = options.user ? options.user : HD.getUser(options.request);
if(self.keepOnRemove) {
self.model.findByIdAndUpdate(id, { deleted: true }, { new: true }).lean().exec(function(err, doc) {
HD.callCallback(cb, HD.checkMongoErr(err), doc);
});
} else {
_removeEdges = function(doc, removeCB) {
var funcs = [];
_.each(doc.edges, function(edge) {
var localOptions = {params: {id: doc._id.toString(), edgeId: edge._id.toString()}, user: loggedUser};
funcs.push(function(next) {
self.domain.edges.remove(localOptions, function(err, model) {
next(err);
});
});
});
Async.parallel(funcs, function(err) {
removeCB();
});
}
_remove = function() {
self.model.findByIdAndRemove(id, self.selectFields(), function(err, doc) {
var id = doc._id.toString();
if(doCache)
Domains.redis().delCache(self.cache.id, id);
if(!err && doc != null && doc.remove)
doc.remove();
HD.callCallback(cb, HD.checkMongoErr(err), doc);
});
}
self.model.findById(options.params.id, self.selectFields()).lean().exec(function(err, doc) {
if (doc.edges && _.isArray(doc.edges) && doc.edges.length > 0) {
_removeEdges(doc, function() {
_remove();
});
} else
_remove();
});
}
}
CRUD.prototype._removeAll = function(options, cb) {
var self = this;
var q = self.model.find();
q.remove().exec(function(err, docs) {
HD.callCallback(cb, HD.checkMongoErr(err), docs);
});
}
CRUD.prototype._update = function(options, cb) {
var self = this;
var params = options.params;
var id = params.id;
var data = options.payload;
var retNew = _.isUndefined(options.retNew) ? true : options.retNew;
var doCache = self.cache && self.cache.actions.indexOf('update') !== -1;
data.modified = new Date();
self.model.findByIdAndUpdate(id, data, { select: self.selectFields(), new: retNew }).lean().exec(function(err, doc) {
HD.callCallback(cb, HD.checkMongoErr(err), doc);
});
}
CRUD.prototype._exist = function(options, cb) {
var self = this;
var id = options.payload.id;
delete options.payload.id;
self._getQuery(options, function(err, docs) {
var data = { exist: false };
if(docs && docs.length > 0) {
if(_.isUndefined(id) || id === "")
data = { exist: true };
else if(docs[0]._id.id !== HD.ObjectID(id).id)
data = { exist: true };
}
HD.callCallback(cb, HD.checkMongoErr(err), data);
});
}
CRUD.prototype._count = function(options, cb) {
var self = this;
var payload = _.isUndefined(options.payload) ? {} : options.payload;
var aggregate = [];
var geoNear = false;
delete payload["sort"];
// Work with the payload, it has to respect this order (and has to be at the end)
if(!_.isUndefined(payload["geoNear"])) {
var geoNear = payload["geoNear"];
var distance = parseInt(geoNear.distance) * 1000;
var lng = parseFloat(geoNear.coordinates[0]);
var ltd = parseFloat(geoNear.coordinates[1]);
geoNear = { $geoNear: {
near: { type: "Point", coordinates: [lng, ltd] },
distanceField: "distCalculated",
maxDistance: distance,
spherical: true,
uniqueDocs: true,
distanceMultiplier: 6378.1 } };
delete payload["geoNear"];
}
var match = HD.query.prepare(options.payload);
// start aggregation structure
if(geoNear) {
// has to come first on the aggreation pipiline
aggregate.push(geoNear);
fields['distCalculated'] = 1;
sort = _.assign({ distCalculated: 1 }, sort);
}
if(!_.isEmpty(match))
aggregate.push({ $match: match });
// get the count
aggregate.push({ $group: { _id: null, total: { $sum: 1 } } } );
self.model.aggregate(aggregate, function(err, docs) {
if(err || docs == null || docs.length === 0 || docs[0].total === 0)
HD.callCallback(cb, HD.checkMongoErr(err), 0);
else
HD.callCallback(cb, HD.checkMongoErr(err), docs[0].total);
});
}
// ****************
// Exposed Methods
// ****************
CRUD.prototype.createAsyncCalls = function(funcs, funcArg) {
var self = this;
if(_.isFunction(funcArg)) {
var func = funcArg;
funcs.push(function(type, options, data, asyncCB) {
func(options, function(err, retData) {
asyncCB(err, type, options, retData);
});
});
} else if(funcArg === 'cache') {
// add the caching func
if(self.cache) {
funcs.push(function (type, options, data, asyncCB) {
self.executeCache(type, options, data, function(err, retOptions, retData) {
asyncCB(err, type, retOptions, retData);
});
});
}
} else {
var funcName = funcArg;
// add the plugin CRUD callbacks
var addPluginFuncs = function(pluginType) {
if(self.model[pluginType]) {
var plugins = self.model[pluginType];
_.each(plugins, function(plugin) {
if(plugin.exports && plugin.exports.crud && plugin.exports.crud[funcName]) {
var func = plugin.exports.crud[funcName];
funcs.push(function(type, options, data, asyncCB) {
func(type, options, data, function(err, retOptions, retData) {
asyncCB(err, type, retOptions, retData);
});
});
}
});
}
}
addPluginFuncs("Decorators");
addPluginFuncs("Behaviours");
// add the model CRUD callback
if(!_.isUndefined(self.model[funcName])) {
funcs.push(function(type, options, data, asyncCB) {
self.model[funcName](type, options, data, function(err, retOptions, retData) {
asyncCB(err, type, retOptions, retData);
});
});
}
}
}
CRUD.prototype.executeCache = function(type, options, data, cb) {
var self = this;
var doCache = self.cache && self.cache.actions.indexOf(type) !== -1;
if(doCache && data && data._id)
Domains.redis().setCache(self.cache.id, data._id.toString(), data);
cb(null, options, data);
}
CRUD.prototype.execute = function(func, type, options, cb) {
var self = this;
var self = this;
var aFuncs = [];
var init = function(asyncCB) {
options.cb = cb;
asyncCB(null, type, options, options.payload);
};
var finish = function(err, type, options, data) {
if(_.isFunction(options.cb))
options.cb(HD.checkMongoErr(err), HD.prepareReturnData(self.model, data, options.request, type, options.dataKey));
};
aFuncs.push(init);
self.createAsyncCalls(aFuncs, "validate");
self.createAsyncCalls(aFuncs, "before");
self.createAsyncCalls(aFuncs, func);
self.createAsyncCalls(aFuncs, "after");
self.createAsyncCalls(aFuncs, "cache");
Async.waterfall(aFuncs, finish);
}
CRUD.prototype.get = function(options, cb) {
var self = this;
self.execute(self._get.bind(self), "get", options, cb);
}
CRUD.prototype.getQuery = function(options, cb) {
var self = this;
if(options.payload && options.payload.esSearch) {
options.esSearch = options.payload.esSearch;
delete options.payload.esSearch;
} else {
options.payload = HD.query.payload.prepare(options.payload);
}
self.execute(self._getQuery.bind(self), "getQuery", options, cb);
}
CRUD.prototype.add = function(options, cb) {
var self = this;
self.execute(self._add.bind(self), "add", options, cb);
}
CRUD.prototype.update = function(options, cb) {
var self = this;
self.execute(self._update.bind(self), "update", options, cb);
}
CRUD.prototype.remove = function(options, cb) {
var self = this;
self.execute(self._remove.bind(self), "remove", options, cb);
}
CRUD.prototype.removeAll = function(options, cb) {
var self = this;
self.execute(self._removeAll.bind(self), "removeAll", options, cb);
}
CRUD.prototype.exist = function(options, cb) {
var self = this;
self.execute(self._exist.bind(self), "exist", options, cb);
}
CRUD.prototype.count = function(options, cb) {
var self = this;
options.payload = HD.query.payload.prepare(options.payload);
self.execute(self._count.bind(self), "count", options, cb);
}
// ***********
// HANDLERS
// ***********
CRUD.prototype.handler = {};
CRUD.prototype.handler.get = function(request, reply) {
var self = this;
var options = {
request: request,
user: HD.getUser(request),
params: request.params,
api: "site"
};
self.get(options, function(err, data) {
HD.respond(reply, err, data);
});
}
CRUD.prototype.handler.getQuery = function(request, reply) {
var self = this;
var options = {
request: request,
user: HD.getUser(request),
query: request.query,
payload: request.payload,
api: "site"
};
self.getQuery(options, function(err, data) {
HD.respond(reply, err, data);
});
}
CRUD.prototype.handler.update = function(request, reply) {
var self = this;
var options = {
request: request,
user: HD.getUser(request),
params: request.params,
payload: request.payload,
api: "site"
};
self.update(options, function(err, data) {
HD.respond(reply, err, data);
});
}
CRUD.prototype.handler.remove = function(request, reply) {
var self = this;
var options = {
request: request,
user: HD.getUser(request),
params: request.params,
api: "site"
};
self.remove(options, function(err, data) {
HD.respond(reply, err, data);
});
}
CRUD.prototype.handler.add = function(request, reply) {
var self = this;
var ownerId = null;
var user = HD.getUser(request);
if(user)
ownerId = HD.ObjectID(user._id);
var options = {
request: request,
user: user,
payload: request.payload,
ownerId: ownerId,
api: "site"
};
self.add(options, function(err, data) {
HD.respond(reply, err, data);
});
}
CRUD.prototype.handler.exist = function(request, reply) {
var self = this;
var options = {
request: request,
user: HD.getUser(request),
payload: request.payload,
api: "site"
};
self.exist(options, function(err, data) {
HD.respond(reply, err, data);
});
}
CRUD.prototype.handler.count = function(request, reply) {
var self = this;
var options = {
request: request,
user: HD.getUser(request),
query: request.query,
payload: request.payload,
api: "site"
};
self.count(options, function(err, data) {
HD.respond(reply, err, data);
});
}
// ***************
// HANDLERS ADMIN
// **************
CRUD.prototype.handlerAdmin = {};
CRUD.prototype.handlerAdmin.get = function(request, reply) {
var self = this;
var options = {
request: request,
user: HD.getUser(request),
params: request.params,
api: "admin"
};
self.get(options, function(err, data) {
HD.respond(reply, err, data);
});
}
CRUD.prototype.handlerAdmin.getQuery = function(request, reply) {
var self = this;
var options = {
request: request,
user: HD.getUser(request),
query: request.query,
payload: request.payload,
api: "admin"
};
self.getQuery(options, function(err, data) {
HD.respond(reply, err, data);
});
}
CRUD.prototype.handlerAdmin.update = function(request, reply) {
var self = this;
var options = {
request: request,
user: HD.getUser(request),
params: request.params,
payload: request.payload,
api: "admin"
};
self.update(options, function(err, data) {
HD.respond(reply, err, data);
});
}
CRUD.prototype.handlerAdmin.remove = function(request, reply) {
var self = this;
var options = {
request: request,
user: HD.getUser(request),
params: request.params,
api: "admin"
};
self.remove(options, function(err, data) {
HD.respond(reply, err, data);
});
}
CRUD.prototype.handlerAdmin.add = function(request, reply) {
var self = this;
var ownerId = null;
var user = HD.getUser(request);
if(user)
ownerId = HD.ObjectID(user._id);
var options = {
request: request,
user: user,
payload: request.payload,
ownerId: ownerId,
api: "admin"
};
self.add(options, function(err, data) {
HD.respond(reply, err, data);
});
}
CRUD.prototype.handlerAdmin.exist = function(request, reply) {
var self = this;
var options = {
request: request,
user: HD.getUser(request),
payload: request.payload,
api: "admin"
};
self.exist(options, function(err, data) {
HD.respond(reply, err, data);
});
}
CRUD.prototype.handlerAdmin.count = function(request, reply) {
var self = this;
var options = {
request: request,
user: HD.getUser(request),
query: request.query,
payload: request.payload,
api: "admin"
};
self.count(options, function(err, data) {
HD.respond(reply, err, data);
});
}
module.exports = {
plugin: initPlugin,
newInstance: function(model, options) {
return new CRUD(model, options);
}
};
var _ = require('lodash');
var HD = require('hd').utils;
var HP = require('hd').plugin;
var Types = require('joi');
var Activities = require('./activities');
var Domains = require('hd').domains;
// Declare plugin fields
var fields = [
{ likes: [ {userId: String} ]}
];
// PLUGIN CONSTRUCTION
function initPlugin(schema) {
HP.initPlugin(schema, fields);
}
function Likeable(model) {
var self = this;
this.model = model;
this.routes = {
admin: [
{
method: 'PUT',
path: '/admin/{model}/{id}/likes',
config: {
auth: true,
handler: self.handler.add.bind(self),
payload: 'parse'
}
},
{
method: 'DELETE',
path: '/admin/{model}/{id}/likes',
config: {
auth: true,
handler: self.handler.remove.bind(self)
}
},
{
method: 'POST',
path: '/admin/{model}/{id}/likes',
config: {
auth: true,
handler: self.handler.get.bind(self)
}
}
],
api: [
{
method: 'PUT',
path: '/{model}/{id}/likes',
config: {
auth: true,
handler: self.handler.add.bind(self),
payload: 'parse',
description: 'Adds a "like" to a {model}.',
notes: ['Requires authentication', 'The id can ONLY be the id of the owner.'],
tags: ['like', 'social']
}
},
{
method: 'DELETE',
path: '/{model}/{id}/likes',
config: {
auth: true,
handler: self.handler.remove.bind(self),
description: 'Removes a existing "like" from a {model}.',
notes: ['Requires authentication', 'The id can ONLY be the id of the owner.'],
tags: ['like', 'social']
}
},
{
method: 'POST',
path: '/{model}/{id}/likes',
config: {
auth: true,
handler: self.handler.get.bind(self),
description: 'Gets a list of all users who liked a {model}',
notes: ['Requires authentication', 'The id can ONLY be the id of the owner.'],
tags: ['like', 'social']
}
}
]
};
this.exports = {
likes: {
get: self.get.bind(self),
add: self.add.bind(self),
remove: self.remove.bind(self)
}
}
}
// INTERNAL METHODS
Likeable.prototype.add = function(options, cb) {
var self = this;
var modelId = options.params.id;
var data = options.payload;
var activityId = data.activityId;
// validates model id
if(_.isUndefined(modelId)) {
HD.callCallback(cb, HD.errors.invalidModelId, data);
return;
}
// validate that the userId is the logged user
if(data.userId !== options.user._id.toString()) {
HD.callCallback(cb, HD.errors.unauthorizedAction, data);
return;
}
// validates user existence
Domains.users().get({params : { id: data.userId }}, function(err, userData) {
if(err) {
HD.callCallback(cb, HD.errors.invalidLikeUserId, data);
return;
}
if(_.isNull(userData)) {
HD.callCallback(cb, HD.errors.invalidLikeUserId, data);
return;
}
// validates if user has already liked this model
var id = modelId;
var model = self.model;
if(activityId && activityId !== '') {
id = HD.ObjectID(activityId);
model = Domains.activities().dcrud;
}
model.findOne({_id : id, 'likes.userId' : data.userId}).lean().exec(function(err, likeData) {
if(err || !_.isNull(likeData)) {
HD.callCallback(cb, HD.errors.invalidLike, data);
return;
}
// adds like to model
model.findByIdAndUpdate(id,
{$addToSet: {likes: {userId: data.userId}}},
{safe: true, upsert: true, new: true}).lean().exec(
function(err, model) {
// adds activity
if(model.type === Activities.types.LIKE) {
HD.callCallback(cb, err, HD.prepareReturnData(self.model, model));
} else {
Activities.like(options.user._id.toString(), modelId, self.model.modelName, data.relatedModelId, data.relatedModelName, undefined, function () {
Domains.redis().delCache('activity', modelId);
HD.callCallback(cb, err, HD.prepareReturnData(self.model, model));
});
}
}
);
});
});
};
Likeable.prototype.remove = function(options, cb) {
var self = this;
var modelId = options.params.id;
var userId = options.user._id.toString();
// validates model id
if(_.isUndefined(modelId)) {
HD.callCallback(cb, HD.errors.invalidModelId, data);
return;
}
// validates model id
if(_.isUndefined(userId)) {
HD.callCallback(cb, HD.errors.invalidLikeUserId, data);
return;
}
//remove like form model
self.model.findByIdAndUpdate(modelId,
{$pull: { likes: { userId: userId } }},
{safe: true, upsert: true, new: true}).lean().exec(
function(err, model) {
// try to remove the activity if exists one for like
// We need
// * the userId as the ownerId (has to be owner)
// * the modelId is the node, it refers to what was liked
// * the activityTypes.LIKE
var activities = Domains.activities();
var conditions = {
ownerId: HD.ObjectID(userId),
type: activities.types.LIKE,
node: HD.ObjectID(modelId)
};
activities.findAndRemove(conditions, function(err) {
Domains.redis().delCache('activity', modelId);
HD.callCallback(cb, err, HD.prepareReturnData(self.model, model));
});
}
);
};
Likeable.prototype.get = function(options, cb) {
var self = this;
var page = (!_.isUndefined(options.query["page"])) ? parseInt(options.query["page"]) : 0;
var perPage = (!_.isUndefined(options.query["perPage"])) ? parseInt(options.query["perPage"]) : 0;
// creates aggregate base pipeline
var aggregateBase = [
{ $match : { _id: HD.ObjectID(options.params.id) }},
{ $unwind: '$likes'}
];
// retrieves item count
var count = aggregateBase.concat({ $group: { _id: '$_id', count: { $sum: 1} }});
self.model.aggregate(count, function(e, resultCount) {
if(!e) {
// check pagination parameters
if(perPage !== 0 && page !== 0){
aggregateBase.push({ $skip: ((page-1) * perPage)});
aggregateBase.push({ $limit: perPage});
}
aggregateBase.push({ $group: { _id: '$_id', items: { $push: '$likes.userId'}}});
self.model.aggregate(aggregateBase, function(e, result) {
if(!e) {
// populates items
if(_.isArray(result) && result.length > 0) {
result[0].count = resultCount[0].count;
}
HD.callCallback(cb, e, result);
}
else
HD.callCallback(cb, e);
});
}
else
HD.callCallback(cb, e);
});
};
// HANDLERS
Likeable.prototype.handler = {};
Likeable.prototype.handler.add = function(request, reply) {
var self = this;
var user = HD.getUser(request);
var options = {
params: request.params,
payload: request.payload,
user: user
};
var cb = function(err, retData) {
HD.respond(reply, err, retData);
};
self.add(options, cb);
};
Likeable.prototype.handler.remove = function(request, reply) {
var self = this;
var user = HD.getUser(request);
var options = {
params: request.params,
user: user
};
var cb = function(err, retData) {
HD.respond(reply, err, retData);
};
self.remove(options, cb);
};
Likeable.prototype.handler.get = function(request, reply) {
var self = this;
var user = HD.getUser(request);
var options = {
params: request.params,
query: request.query,
payload: request.payload,
user: user
};
var cb = function(err, retData) {
HD.respond(reply, err, retData);
};
self.get(options, cb);
};
module.exports = {
plugin: initPlugin,
newInstance: function(model) {
return new Likeable(model);
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment