mirror of
https://github.com/vale981/Vulcan
synced 2025-03-06 01:51:40 -05:00
Another voting refactor (hopefully the last one…)
This commit is contained in:
parent
15ed3c3923
commit
4640dad0b7
6 changed files with 227 additions and 129 deletions
|
@ -1,14 +1,14 @@
|
|||
import React, { PropTypes, Component } from 'react';
|
||||
import { graphql } from 'react-apollo';
|
||||
import gql from 'graphql-tag';
|
||||
import { voteOptimisticResponse } from '../modules/vote.js';
|
||||
import { performVoteClient } from '../modules/vote.js';
|
||||
import { VoteableCollections } from '../modules/make_voteable.js';
|
||||
|
||||
export const withVote = component => {
|
||||
|
||||
return graphql(gql`
|
||||
mutation vote($documentId: String, $operationType: String, $collectionName: String, $voteId: String) {
|
||||
vote(documentId: $documentId, operationType: $operationType, collectionName: $collectionName, voteId: $voteId) {
|
||||
mutation vote($documentId: String, $voteType: String, $collectionName: String, $voteId: String) {
|
||||
vote(documentId: $documentId, voteType: $voteType, collectionName: $collectionName, voteId: $voteId) {
|
||||
${VoteableCollections.map(collection => `
|
||||
... on ${collection.typeName} {
|
||||
__typename
|
||||
|
@ -25,15 +25,14 @@ export const withVote = component => {
|
|||
}
|
||||
`, {
|
||||
props: ({ownProps, mutate}) => ({
|
||||
vote: ({document, operationType, collection, currentUser}) => {
|
||||
vote: ({document, voteType, collection, currentUser, voteId = Random.id()}) => {
|
||||
|
||||
const voteId = Random.id();
|
||||
const newDocument = voteOptimisticResponse({collection, document, user: currentUser, operationType, voteId});
|
||||
const newDocument = performVoteClient({collection, document, user: currentUser, voteType, voteId});
|
||||
|
||||
return mutate({
|
||||
variables: {
|
||||
documentId: document._id,
|
||||
operationType,
|
||||
voteType,
|
||||
collectionName: collection.options.collectionName,
|
||||
voteId,
|
||||
},
|
||||
|
|
|
@ -18,7 +18,7 @@ export const makeVoteable = collection => {
|
|||
type: '[Vote]',
|
||||
resolver: async (document, args, { Users, Votes, currentUser }) => {
|
||||
if (!currentUser) return [];
|
||||
const votes = Votes.find({userId: currentUser._id, itemId: document._id}).fetch();
|
||||
const votes = Votes.find({userId: currentUser._id, documentId: document._id}).fetch();
|
||||
if (!votes.length) return [];
|
||||
return votes;
|
||||
// return Users.restrictViewableFields(currentUser, Votes, votes);
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
import Users from 'meteor/vulcan:users';
|
||||
|
||||
const membersActions = [
|
||||
'posts.upvote',
|
||||
'posts.cancelUpvote',
|
||||
'posts.downvote',
|
||||
'posts.cancelDownvote',
|
||||
'comments.upvote',
|
||||
'comments.cancelUpvote',
|
||||
'comments.downvote',
|
||||
'comments.cancelDownvote'
|
||||
];
|
||||
Users.groups.members.can(membersActions);
|
|
@ -1,68 +1,191 @@
|
|||
import { registerSetting, getSetting } from 'meteor/vulcan:core';
|
||||
import { runCallbacksAsync, runCallbacks, addCallback } from 'meteor/vulcan:core';
|
||||
import { createError } from 'apollo-errors';
|
||||
import Votes from './votes/collection.js';
|
||||
import Users from 'meteor/vulcan:users';
|
||||
|
||||
registerSetting('voting.maxVotes', 1, 'How many times a user can vote on the same document');
|
||||
|
||||
/*
|
||||
|
||||
Define voting operations
|
||||
|
||||
*/
|
||||
export const voteOperations = {
|
||||
'upvote': {
|
||||
power: 1,
|
||||
// TODO: refactor voteOptimisticResponse and performVoteOperation code
|
||||
// into extensible, action-specific objects
|
||||
// clientOperation: () => {
|
||||
const voteTypes = {}
|
||||
|
||||
// },
|
||||
// serverOperation: () => {
|
||||
/*
|
||||
|
||||
// }
|
||||
},
|
||||
'downvote': {
|
||||
power: -1
|
||||
},
|
||||
'adminUpvote': {
|
||||
power: user => Users.isAdmin(user) ? 5 : 1
|
||||
},
|
||||
'cancelVote': {
|
||||
Add new vote types
|
||||
|
||||
*/
|
||||
export const addVoteType = (voteType, voteTypeOptions) => {
|
||||
voteTypes[voteType] = voteTypeOptions;
|
||||
}
|
||||
|
||||
addVoteType('upvote', {power: 1, exclusive: true});
|
||||
addVoteType('downvote', {power: -1, exclusive: true});
|
||||
|
||||
addVoteType('angry', {power: -1, exclusive: true});
|
||||
addVoteType('sad', {power: -1, exclusive: true});
|
||||
addVoteType('happy', {power: 1, exclusive: true});
|
||||
addVoteType('laughing', {power: 1, exclusive: true});
|
||||
|
||||
/*
|
||||
|
||||
Test if a user has voted on the client
|
||||
|
||||
*/
|
||||
export const hasVotedClient = ({ document, voteType }) => {
|
||||
const userVotes = document.currentUserVotes;
|
||||
if (voteType) {
|
||||
return _.where(userVotes, { voteType }).length
|
||||
} else {
|
||||
return userVotes && userVotes.length
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
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 = {
|
||||
_id: document._id,
|
||||
baseScore: document.baseScore || 0,
|
||||
__typename: collection.options.typeName,
|
||||
};
|
||||
|
||||
// create new vote and add it to currentUserVotes array
|
||||
const vote = createVote({ document, collectionName: collection.options.collectionName, voteType, user, voteId });
|
||||
newDocument.currentUserVotes = [...document.currentUserVotes, vote];
|
||||
|
||||
// increment baseScore
|
||||
newDocument.baseScore += vote.power;
|
||||
|
||||
return newDocument;
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
Add a vote of a specific type on the server
|
||||
|
||||
*/
|
||||
const addVoteServer = ({ document, collection, voteType, user, voteId }) => {
|
||||
|
||||
// create vote and insert it
|
||||
const vote = createVote({ document, collectionName: collection.options.collectionName, voteType, user, voteId });
|
||||
delete vote.__typename;
|
||||
Votes.insert(vote);
|
||||
|
||||
// update document score
|
||||
collection.update({_id: document._id}, {$inc: {baseScore: vote.power }});
|
||||
|
||||
return vote;
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
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;
|
||||
|
||||
const newVotes = _.reject(document.currentUserVotes, vote => vote.voteType === voteType);
|
||||
|
||||
// clear out vote of this type
|
||||
newDocument.currentUserVotes = newVotes;
|
||||
|
||||
}
|
||||
return newDocument;
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
Clear *all* votes for a given document and user (client)
|
||||
|
||||
*/
|
||||
const clearVotesClient = ({ document }) => {
|
||||
const newDocument = _.clone(document);
|
||||
newDocument.baseScore -= calculateTotalPower(document.currentUserVotes);
|
||||
newDocument.currentUserVotes = [];
|
||||
return newDocument
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
Clear all votes for a given document and user (server)
|
||||
|
||||
*/
|
||||
const clearVotesServer = ({ document, user, collection }) => {
|
||||
const votes = Votes.find({ documentId: document._id, userId: user._id}).fetch();
|
||||
if (votes.length) {
|
||||
Votes.remove({documentId: document._id});
|
||||
collection.update({_id: document._id}, {$inc: {baseScore: -calculateTotalPower(votes) }});
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
Cancel votes of a specific type on a given document (server)
|
||||
|
||||
*/
|
||||
const cancelVoteServer = ({ document, voteType, collection, user }) => {
|
||||
|
||||
const vote = Votes.findOne({documentId: document._id, userId: user._id, voteType})
|
||||
|
||||
// remove vote object
|
||||
Votes.remove({_id: vote._id});
|
||||
|
||||
// update document score
|
||||
collection.update({_id: document._id}, {$inc: {baseScore: -vote.power }});
|
||||
|
||||
return vote;
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
Determine a user's voting power for a given operation.
|
||||
If power is a function, call it on user
|
||||
|
||||
*/
|
||||
export const getVotePower = (user, operationType) => {
|
||||
const power = voteOperations[operationType].power;
|
||||
return typeof power === 'function' ? power(user) : power;
|
||||
const getVotePower = ({ user, voteType, document }) => {
|
||||
const power = voteTypes[voteType] && voteTypes[voteType].power || 1;
|
||||
return typeof power === 'function' ? power(user, document) : power;
|
||||
};
|
||||
|
||||
/*
|
||||
|
||||
Calculate total power of all a user's votes on a document
|
||||
|
||||
*/
|
||||
export const calculateTotalPower = userVotes => _.pluck(userVotes, 'power').reduce((a, b) => a + b, 0);
|
||||
|
||||
/*
|
||||
|
||||
Create new vote object
|
||||
|
||||
*/
|
||||
export const createVote = ({ documentId, collectionName, operationType, user, voteId }) => ({
|
||||
const createVote = ({ document, collectionName, voteType, user, voteId }) => ({
|
||||
_id: voteId,
|
||||
itemId: documentId,
|
||||
documentId: document._id,
|
||||
collectionName,
|
||||
userId: user._id,
|
||||
voteType: operationType,
|
||||
power: getVotePower(user, operationType),
|
||||
voteType: voteType,
|
||||
power: getVotePower({user, voteType, document}),
|
||||
votedAt: new Date(),
|
||||
__typename: 'Vote'
|
||||
});
|
||||
|
@ -72,49 +195,45 @@ export const createVote = ({ documentId, collectionName, operationType, user, vo
|
|||
Optimistic response for votes
|
||||
|
||||
*/
|
||||
export const voteOptimisticResponse = ({collection, document, user, operationType = 'upvote', voteId}) => {
|
||||
export const performVoteClient = ({ document, collection, voteType = 'upvote', user, voteId }) => {
|
||||
|
||||
const collectionName = collection.options.collectionName;
|
||||
let returnedDocument;
|
||||
|
||||
console.log('// voteOptimisticResponse')
|
||||
console.log('collectionName: ', collectionName)
|
||||
console.log('document:', document)
|
||||
console.log('voteType:', voteType)
|
||||
|
||||
// make sure item and user are defined
|
||||
if (!document || !user) {
|
||||
throw new Error(`Cannot perform operation '${collectionName}.${operationType}'`);
|
||||
if (!document || !user || !Users.canDo(user, `${collectionName.toLowerCase()}.${voteType}`)) {
|
||||
throw new Error(`Cannot perform operation '${collectionName.toLowerCase()}.${voteType}'`);
|
||||
}
|
||||
|
||||
// console.log('// voteOptimisticResponse')
|
||||
// console.log('collectionName: ', collectionName)
|
||||
// console.log('document:', document)
|
||||
// console.log('operationType:', operationType)
|
||||
const voteOptions = {document, collection, voteType, user, voteId};
|
||||
|
||||
// create a "lite" version of the document that only contains relevant fields
|
||||
// we do not want to affect the original item directly
|
||||
const newDocument = {
|
||||
_id: document._id,
|
||||
baseScore: document.baseScore || 0,
|
||||
__typename: collection.options.typeName,
|
||||
};
|
||||
if (hasVotedClient({document, voteType})) {
|
||||
|
||||
if (operationType === 'cancelVote') {
|
||||
|
||||
// subtract vote scores
|
||||
newDocument.baseScore -= calculateTotalPower(document.currentUserVotes);
|
||||
|
||||
// clear out all votes
|
||||
newDocument.currentUserVotes = [];
|
||||
console.log('action: cancel')
|
||||
returnedDocument = cancelVoteClient(voteOptions);
|
||||
// returnedDocument = runCallbacks(`votes.cancel.client`, returnedDocument, collection, user);
|
||||
|
||||
} else {
|
||||
|
||||
// create new vote and add it to currentUserVotes array
|
||||
const vote = createVote({ documentId: document._id, collectionName, operationType, user, voteId });
|
||||
newDocument.currentUserVotes = [...document.currentUserVotes, vote];
|
||||
console.log('action: vote')
|
||||
|
||||
// increment baseScore
|
||||
const power = getVotePower(user, operationType);
|
||||
newDocument.baseScore += power;
|
||||
if (voteTypes[voteType].exclusive) {
|
||||
clearVotesClient({document, collection, voteType, user, voteId})
|
||||
}
|
||||
|
||||
returnedDocument = addVoteClient(voteOptions);
|
||||
// returnedDocument = runCallbacks(`votes.${voteType}.client`, returnedDocument, collection, user);
|
||||
|
||||
}
|
||||
|
||||
return newDocument;
|
||||
console.log('returnedDocument:', returnedDocument)
|
||||
|
||||
return returnedDocument;
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -122,42 +241,47 @@ export const voteOptimisticResponse = ({collection, document, user, operationTyp
|
|||
Server-side database operation
|
||||
|
||||
*/
|
||||
export const performVoteOperation = ({documentId, operationType, collection, voteId, currentUser}) => {
|
||||
// console.log('// performVoteMutation')
|
||||
// console.log('operationType: ', operationType)
|
||||
// console.log('collectionName: ', collectionName)
|
||||
// console.log('// document: ', collection.findOne(documentId))
|
||||
export const performVoteServer = ({ documentId, voteType = 'upvote', collection, voteId, user }) => {
|
||||
|
||||
const power = getVotePower(currentUser, operationType);
|
||||
const userVotes = Votes.find({itemId: documentId, userId: currentUser._id}).fetch();
|
||||
const collectionName = collection.options.collectionName;
|
||||
const document = collection.findOne(documentId);
|
||||
|
||||
if (operationType === 'cancelVote') {
|
||||
|
||||
// if a vote has been cancelled, delete all votes and subtract their power from base score
|
||||
const scoreTotal = calculateTotalPower(userVotes);
|
||||
console.log('// performVoteMutation')
|
||||
console.log('collectionName: ', collectionName)
|
||||
console.log('document: ', collection.findOne(documentId))
|
||||
console.log('voteType: ', voteType)
|
||||
|
||||
const voteOptions = {document, collection, voteType, user, voteId};
|
||||
|
||||
// remove vote object
|
||||
Votes.remove({itemId: documentId, userId: currentUser._id});
|
||||
if (!document || !user || !Users.canDo(user, `${collectionName.toLowerCase()}.${voteType}`)) {
|
||||
const VoteError = createError('voting.no_permission', {message: 'voting.no_permission'});
|
||||
throw new VoteError();
|
||||
}
|
||||
|
||||
// update document score
|
||||
collection.update({_id: documentId}, {$inc: {baseScore: -scoreTotal }});
|
||||
if (hasVotedServer({document, voteType, user})) {
|
||||
|
||||
console.log('action: cancel')
|
||||
|
||||
// runCallbacks(`votes.cancel.sync`, document, collection, user);
|
||||
cancelVoteServer(voteOptions);
|
||||
// runCallbacksAsync(`votes.cancel.async`, vote, document, collection, user);
|
||||
|
||||
} else {
|
||||
|
||||
console.log('action: vote')
|
||||
|
||||
if (userVotes.length < getSetting('voting.maxVotes')) {
|
||||
|
||||
// create vote and insert it
|
||||
const vote = createVote({ documentId, collectionName: collection.options.collectionName, operationType, user: currentUser, voteId });
|
||||
delete vote.__typename;
|
||||
Votes.insert(vote);
|
||||
|
||||
// update document score
|
||||
collection.update({_id: documentId}, {$inc: {baseScore: power }});
|
||||
|
||||
} else {
|
||||
const VoteError = createError('voting.maximum_votes_reached', {message: 'voting.maximum_votes_reached'});
|
||||
throw new VoteError();
|
||||
if (voteTypes[voteType].exclusive) {
|
||||
clearVotesServer(voteOptions)
|
||||
}
|
||||
|
||||
// runCallbacks(`votes.${voteType}.sync`, document, collection, user);
|
||||
addVoteServer(voteOptions);
|
||||
// runCallbacksAsync(`votes.${voteType}.async`, vote, document, collection, user);
|
||||
|
||||
}
|
||||
|
||||
const newDocument = collection.findOne(documentId);
|
||||
newDocument.__typename = collection.options.typeName;
|
||||
return newDocument;
|
||||
|
||||
}
|
|
@ -8,7 +8,7 @@ const schema = {
|
|||
/**
|
||||
The id of the document that was voted on
|
||||
*/
|
||||
itemId: {
|
||||
documentId: {
|
||||
type: String
|
||||
},
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { addCallback, addGraphQLSchema, addGraphQLResolvers, addGraphQLMutation, Utils, registerSetting, getSetting } from 'meteor/vulcan:core';
|
||||
import { performVoteOperation } from '../modules/vote.js';
|
||||
import { performVoteServer } from '../modules/vote.js';
|
||||
import { VoteableCollections } from '../modules/make_voteable.js';
|
||||
import { createError } from 'apollo-errors';
|
||||
|
||||
function CreateVoteableUnionType() {
|
||||
const voteableSchema = VoteableCollections.length ? `union Voteable = ${VoteableCollections.map(collection => collection.typeName).join(' | ')}` : '';
|
||||
|
@ -20,29 +19,18 @@ const resolverMap = {
|
|||
|
||||
addGraphQLResolvers(resolverMap);
|
||||
|
||||
addGraphQLMutation('vote(documentId: String, operationType: String, collectionName: String, voteId: String) : Voteable');
|
||||
addGraphQLMutation('vote(documentId: String, voteType: String, collectionName: String, voteId: String) : Voteable');
|
||||
|
||||
const voteResolver = {
|
||||
Mutation: {
|
||||
async vote(root, {documentId, operationType, collectionName, voteId}, context) {
|
||||
async vote(root, {documentId, voteType, collectionName, voteId}, context) {
|
||||
|
||||
const { currentUser } = context;
|
||||
const collection = context[collectionName];
|
||||
|
||||
if (context.Users.canDo(currentUser, `${collectionName.toLowerCase()}.${operationType}`)) {
|
||||
|
||||
performVoteOperation({documentId, operationType, collection, voteId, currentUser});
|
||||
const document = performVoteServer({documentId, voteType, collection, voteId, user: currentUser});
|
||||
return document;
|
||||
|
||||
const document = collection.findOne(documentId);
|
||||
document.__typename = collection.options.typeName;
|
||||
return document;
|
||||
|
||||
} else {
|
||||
|
||||
const VoteError = createError('voting.cannot_vote', {message: 'voting.cannot_vote'});
|
||||
throw new VoteError();
|
||||
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
Loading…
Add table
Reference in a new issue