Vulcan/packages/vulcan-voting/lib/modules/vote.js

310 lines
8 KiB
JavaScript
Raw Normal View History

2017-10-21 12:49:16 +09:00
import { debug, runCallbacksAsync, runCallbacks, addCallback } from 'meteor/vulcan:core';
2017-09-27 17:15:49 +02:00
import { createError } from 'apollo-errors';
2017-09-25 22:09:09 +02:00
import Votes from './votes/collection.js';
2017-09-27 17:15:49 +02:00
import Users from 'meteor/vulcan:users';
2017-09-30 08:37:15 +09:00
import { recalculateScore } from './scoring.js';
2017-09-25 22:09:09 +02:00
2017-09-27 17:15:49 +02:00
/*
Define voting operations
*/
const voteTypes = {}
2017-09-27 17:15:49 +02:00
/*
Add new vote types
*/
export const addVoteType = (voteType, voteTypeOptions) => {
voteTypes[voteType] = voteTypeOptions;
2017-09-27 17:15:49 +02:00
}
addVoteType('upvote', {power: 1, exclusive: true});
addVoteType('downvote', {power: -1, exclusive: true});
2017-09-27 17:15:49 +02:00
/*
Test if a user has voted on the client
2017-09-27 17:15:49 +02:00
*/
export const hasVotedClient = ({ document, voteType }) => {
const userVotes = document.currentUserVotes;
if (voteType) {
return _.where(userVotes, { voteType }).length
} else {
return userVotes && userVotes.length
}
}
2017-09-27 17:15:49 +02:00
/*
Calculate total power of all a user's votes on a document
*/
const calculateTotalPower = votes => _.pluck(votes, 'power').reduce((a, b) => a + b, 0);
/*
Test if a user has voted on the server
*/
const hasVotedServer = ({ document, voteType, user }) => {
const vote = Votes.findOne({documentId: document._id, userId: user._id, voteType});
return vote;
}
/*
Add a vote of a specific type on the client
*/
const addVoteClient = ({ document, collection, voteType, user, voteId }) => {
const newDocument = {
2017-09-30 08:37:15 +09:00
...document,
baseScore: document.baseScore || 0,
__typename: collection.options.typeName,
2017-09-30 08:37:15 +09:00
currentUserVotes: document.currentUserVotes || [],
};
// create new vote and add it to currentUserVotes array
const vote = createVote({ document, collectionName: collection.options.collectionName, voteType, user, voteId });
2017-09-29 07:40:41 +09:00
newDocument.currentUserVotes = [...newDocument.currentUserVotes, vote];
// increment baseScore
newDocument.baseScore += vote.power;
2017-09-30 08:37:15 +09:00
newDocument.score = recalculateScore(newDocument);
return newDocument;
}
/*
Add a vote of a specific type on the server
*/
const addVoteServer = ({ document, collection, voteType, user, voteId }) => {
2017-09-30 08:37:15 +09:00
const newDocument = _.clone(document);
// create vote and insert it
const vote = createVote({ document, collectionName: collection.options.collectionName, voteType, user, voteId });
delete vote.__typename;
Votes.insert(vote);
2018-01-23 12:35:16 -08:00
// update document score & set item as active
collection.update({_id: document._id}, {$inc: {baseScore: -vote.power }, $set: {inactive: false, score: newDocument.score}});
2017-09-30 08:37:15 +09:00
newDocument.baseScore += vote.power;
newDocument.score = recalculateScore(newDocument);
return newDocument;
}
/*
Cancel votes of a specific type on a given document (client)
*/
const cancelVoteClient = ({ document, voteType }) => {
const vote = _.findWhere(document.currentUserVotes, { voteType });
const newDocument = _.clone(document);
if (vote) {
// subtract vote scores
newDocument.baseScore -= vote.power;
2017-09-30 08:37:15 +09:00
newDocument.score = recalculateScore(newDocument);
const newVotes = _.reject(document.currentUserVotes, vote => vote.voteType === voteType);
// clear out vote of this type
newDocument.currentUserVotes = newVotes;
2018-01-23 12:35:16 -08:00
}
return newDocument;
}
/*
Clear *all* votes for a given document and user (client)
*/
const clearVotesClient = ({ document }) => {
const newDocument = _.clone(document);
newDocument.baseScore -= calculateTotalPower(document.currentUserVotes);
2017-09-30 08:37:15 +09:00
newDocument.score = recalculateScore(newDocument);
newDocument.currentUserVotes = [];
return newDocument
}
/*
Clear all votes for a given document and user (server)
*/
const clearVotesServer = ({ document, user, collection }) => {
2017-09-30 08:37:15 +09:00
const newDocument = _.clone(document);
const votes = Votes.find({ documentId: document._id, userId: user._id}).fetch();
if (votes.length) {
2018-01-23 12:35:16 -08:00
Votes.remove({documentId: document._id, userId: user._id});
collection.update({_id: document._id}, {$inc: {baseScore: -calculateTotalPower(votes) }});
2017-09-30 08:37:15 +09:00
newDocument.baseScore -= calculateTotalPower(votes);
newDocument.score = recalculateScore(newDocument);
}
2017-09-30 08:37:15 +09:00
return newDocument;
}
/*
Cancel votes of a specific type on a given document (server)
*/
const cancelVoteServer = ({ document, voteType, collection, user }) => {
2017-09-30 08:37:15 +09:00
const newDocument = _.clone(document);
const vote = Votes.findOne({documentId: document._id, userId: user._id, voteType})
2018-01-23 12:35:16 -08:00
// remove vote object
Votes.remove({_id: vote._id});
// update document score
collection.update({_id: document._id}, {$inc: {baseScore: -vote.power }});
2017-09-30 08:37:15 +09:00
newDocument.baseScore -= vote.power;
newDocument.score = recalculateScore(newDocument);
return newDocument;
}
/*
Determine a user's voting power for a given operation.
If power is a function, call it on user
*/
const getVotePower = ({ user, voteType, document }) => {
const power = voteTypes[voteType] && voteTypes[voteType].power || 1;
return typeof power === 'function' ? power(user, document) : power;
};
/*
2017-09-27 17:15:49 +02:00
Create new vote object
*/
2017-09-29 07:40:41 +09:00
const createVote = ({ document, collectionName, voteType, user, voteId }) => {
2018-01-23 12:35:16 -08:00
2017-09-29 07:40:41 +09:00
const vote = {
documentId: document._id,
collectionName,
userId: user._id,
voteType: voteType,
power: getVotePower({user, voteType, document}),
votedAt: new Date(),
__typename: 'Vote'
}
2018-01-23 12:35:16 -08:00
2017-09-29 07:40:41 +09:00
// when creating a vote from the server, voteId can sometimes be undefined
if (voteId) vote._id = voteId;
return vote;
};
2017-09-27 17:15:49 +02:00
/*
Optimistic response for votes
*/
export const performVoteClient = ({ document, collection, voteType = 'upvote', user, voteId }) => {
2017-09-25 22:09:09 +02:00
const collectionName = collection.options.collectionName;
let returnedDocument;
2017-09-29 07:40:41 +09:00
// console.log('// voteOptimisticResponse')
// console.log('collectionName: ', collectionName)
// console.log('document:', document)
// console.log('voteType:', voteType)
2017-09-25 22:09:09 +02:00
// make sure item and user are defined
if (!document || !user || !Users.canDo(user, `${collectionName.toLowerCase()}.${voteType}`)) {
throw new Error(`Cannot perform operation '${collectionName.toLowerCase()}.${voteType}'`);
2017-09-25 22:09:09 +02:00
}
const voteOptions = {document, collection, voteType, user, voteId};
2017-09-25 22:09:09 +02:00
if (hasVotedClient({document, voteType})) {
2017-09-25 22:09:09 +02:00
2017-09-29 07:40:41 +09:00
// console.log('action: cancel')
returnedDocument = cancelVoteClient(voteOptions);
// returnedDocument = runCallbacks(`votes.cancel.client`, returnedDocument, collection, user);
2017-09-25 22:09:09 +02:00
} else {
2017-01-26 13:09:27 +09:00
2017-09-29 07:40:41 +09:00
// console.log('action: vote')
if (voteTypes[voteType].exclusive) {
clearVotesClient({document, collection, voteType, user, voteId})
}
returnedDocument = addVoteClient(voteOptions);
// returnedDocument = runCallbacks(`votes.${voteType}.client`, returnedDocument, collection, user);
2017-09-25 22:09:09 +02:00
2017-09-27 17:15:49 +02:00
}
2017-09-25 22:09:09 +02:00
2017-09-29 07:40:41 +09:00
// console.log('returnedDocument:', returnedDocument)
2018-01-23 12:35:16 -08:00
return returnedDocument;
2017-09-27 17:15:49 +02:00
}
2017-09-25 22:09:09 +02:00
2017-09-27 17:15:49 +02:00
/*
2017-09-25 22:09:09 +02:00
2017-09-27 17:15:49 +02:00
Server-side database operation
2017-09-27 17:15:49 +02:00
*/
2017-09-30 08:37:15 +09:00
export const performVoteServer = ({ documentId, document, voteType = 'upvote', collection, voteId, user }) => {
2018-01-23 12:35:16 -08:00
const collectionName = collection.options.collectionName;
2017-09-30 08:37:15 +09:00
document = document || collection.findOne(documentId);
2017-10-21 12:49:16 +09:00
debug('// performVoteMutation')
debug('collectionName: ', collectionName)
debug('document: ', document)
debug('voteType: ', voteType)
2018-01-23 12:35:16 -08:00
const voteOptions = {document, collection, voteType, user, voteId};
if (!document || !user || !Users.canDo(user, `${collectionName.toLowerCase()}.${voteType}`)) {
const VoteError = createError('voting.no_permission', {message: 'voting.no_permission'});
throw new VoteError();
}
if (hasVotedServer({document, voteType, user})) {
2017-09-29 07:40:41 +09:00
// console.log('action: cancel')
// runCallbacks(`votes.cancel.sync`, document, collection, user);
2017-09-30 08:37:15 +09:00
document = cancelVoteServer(voteOptions);
// runCallbacksAsync(`votes.cancel.async`, vote, document, collection, user);
2018-01-23 12:35:16 -08:00
} else {
2018-01-23 12:35:16 -08:00
2017-09-29 07:40:41 +09:00
// console.log('action: vote')
if (voteTypes[voteType].exclusive) {
2017-09-30 08:37:15 +09:00
document = clearVotesServer(voteOptions)
2017-09-27 17:15:49 +02:00
}
// runCallbacks(`votes.${voteType}.sync`, document, collection, user);
2017-09-30 08:37:15 +09:00
document = addVoteServer(voteOptions);
// runCallbacksAsync(`votes.${voteType}.async`, vote, document, collection, user);
2018-01-23 12:35:16 -08:00
2017-09-27 17:15:49 +02:00
}
2017-09-30 08:37:15 +09:00
// const newDocument = collection.findOne(documentId);
document.__typename = collection.options.typeName;
return document;
2017-09-29 07:40:41 +09:00
2017-09-30 08:37:15 +09:00
}