Created
April 27, 2016 21:51
-
-
Save abernardobr/8c0ebad798a0b280f99715dc9901408f to your computer and use it in GitHub Desktop.
emprego.net - example code - back-end
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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