Skip to content
This repository has been archived by the owner on May 31, 2024. It is now read-only.

Commit

Permalink
re-enable nova:subscribe
Browse files Browse the repository at this point in the history
  • Loading branch information
xavxyz committed Feb 7, 2017
1 parent 7f73f5a commit 42285a8
Show file tree
Hide file tree
Showing 8 changed files with 174 additions and 115 deletions.
20 changes: 10 additions & 10 deletions packages/nova-subscribe/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,35 +25,35 @@ categories.unsubscribe

This package also provides a reusable component called `SubscribeTo` to subscribe to an document of collection.

This component takes two props, `document` & `documentType`. It can trigger any method described below:
This component takes the `document` as a props. It can trigger any method described below:

```jsx
// for example, in PostItem.jsx
<Telescope.components.SubscribeTo document={post} documentType={"posts"} />
<Components.SubscribeTo document={post} />

// for example, in UsersProfile.jsx
<Telescope.components.SubscribeTo document={user} documentType={"users"} />
<Components.SubscribeTo document={user} />

// for example, in Category.jsx
<Telescope.components.SubscribeTo document={category} documentType={"categories"} />
<Components.SubscribeTo document={category} />
```

### Extend to other collections than Users, Posts, Categories
This package export a function called `subscribeMethodsGenerator` that takes a collection as an argument and create the associated methods code :
This package export a function called `subscribMutationsGenerator` that takes a collection as an argument and create the associated methods code :

```js
// in my custom package
import subscribeMethodsGenerator from 'meteor/nova:subscribe';
import subscribMutationsGenerator from 'meteor/nova:subscribe';
import Movies from './collection.js';

// the function creates the code, then you have to associate it to the Meteor namespace:
Meteor.methods(subscribeMethodsGenerator(Movies));
// the function creates the code and give it to the graphql server
subscribMutationsGenerator(Movies);
```

This will creates for you the methods `movies.subscribe` & `movies.unsubscribe` than can be used in the `SubscribeTo` component:
This will creates for you the mutations `moviesSubscribe` & `moviesUnsubscribe` than can be used in the `SubscribeTo` component:
```jsx
// in my custom component
<Telescope.components.SubscribeTo document={movie} documentType={"movies"} />
<Components.SubscribeTo document={movie} />
```

You'll also need to write the relevant callbacks, custom fields & permissions to run whenever a user is subscribed to your custom collection's item. See these files for inspiration.
Expand Down
2 changes: 1 addition & 1 deletion packages/nova-subscribe/lib/callbacks.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Telescope from 'meteor/nova:lib';
import Telescope from 'meteor/nova:lib'; // note: Telescope.notifications
import Users from 'meteor/nova:users';
import { addCallback } from 'meteor/nova:core';

Expand Down
123 changes: 77 additions & 46 deletions packages/nova-subscribe/lib/components/SubscribeTo.jsx
Original file line number Diff line number Diff line change
@@ -1,75 +1,106 @@
import React, { PropTypes, Component } from 'react';
import { intlShape } from 'react-intl';
import { withCurrentUser, withMessages, registerComponent } from 'meteor/nova:core';
import { intlShape, FormattedMessage } from 'react-intl';
import { compose, graphql } from 'react-apollo';
import gql from 'graphql-tag';
import Users from 'meteor/nova:users';
import { withCurrentUser, withMessages, registerComponent, Utils } from 'meteor/nova:core';

class SubscribeTo extends Component {
// boolean -> unsubscribe || subscribe
const getSubscribeAction = subscribed => subscribed ? 'unsubscribe' : 'subscribe'

class SubscribeToActionHandler extends Component {

constructor(props, context) {
super(props, context);

this.onSubscribe = this.onSubscribe.bind(this);
this.isSubscribed = this.isSubscribed.bind(this);
}

onSubscribe(e) {
e.preventDefault();

const {document, documentType} = this.props;

const action = this.isSubscribed() ? `unsubscribe` : `subscribe`;

// method name will be for example posts.subscribe
this.context.actions.call(`${documentType}.${action}`, document._id, (error, result) => {
if (error) {
this.props.flash(error.message, "error");
}

if (result) {
// success message will be for example posts.subscribed
this.props.flash(this.context.intl.formatMessage(
{id: `${documentType}.${action}d`},
// handle usual name properties
{name: document.name || document.title || document.displayName}
), "success");
this.context.events.track(action, {'_id': this.props.document._id});
}
});

this.state = {
subscribed: !!Users.isSubscribedTo(props.currentUser, props.document, props.documentType),
};
}

isSubscribed() {
const documentCheck = this.props.document;

return documentCheck && documentCheck.subscribers && documentCheck.subscribers.indexOf(this.context.currentUser._id) !== -1;
async onSubscribe(e) {
try {
e.preventDefault();

const { document, documentType } = this.props;
const action = getSubscribeAction(this.state.subscribed);

// todo: change the mutation to auto-update the user in the store
await this.setState(prevState => ({subscribed: !prevState.subscribed}));

// mutation name will be for example postsSubscribe
await this.props[`${documentType + Utils.capitalize(action)}`]({documentId: document._id});

// success message will be for example posts.subscribed
this.props.flash(this.context.intl.formatMessage(
{id: `${documentType}.${action}d`},
// handle usual name properties
{name: document.name || document.title || document.displayName}
), "success");


} catch(error) {
this.props.flash(error.message, "error");
}
}

render() {
const {currentUser, document, documentType} = this.props;

const { currentUser, document, documentType } = this.props;
const { subscribed } = this.state;

const action = `${documentType}.${getSubscribeAction(subscribed)}`;

// can't subscribe to yourself or to own post (also validated on server side)
if (!currentUser || !document || (documentType === 'posts' && document && document.author === currentUser.username) || (documentType === 'users' && document === currentUser)) {
if (!currentUser || !document || (documentType === 'posts' && document.userId === currentUser._id) || (documentType === 'users' && document._id === currentUser._id)) {
return null;
}

const action = this.isSubscribed() ? `${documentType}.unsubscribe` : `${documentType}.subscribe`;

const className = this.props.className ? this.props.className : "";

return Users.canDo(currentUser, action) ? <a className={className} onClick={this.onSubscribe}>{this.context.intl.formatMessage({id: action})}</a> : null;
return Users.canDo(currentUser, action) ? <a className={className} onClick={this.onSubscribe}><FormattedMessage id={action} /></a> : null;
}

}

SubscribeTo.propTypes = {
SubscribeToActionHandler.propTypes = {
document: React.PropTypes.object.isRequired,
documentType: React.PropTypes.string.isRequired,
className: React.PropTypes.string,
currentUser: React.PropTypes.object,
}

SubscribeTo.contextTypes = {
actions: React.PropTypes.object,
events: React.PropTypes.object,
SubscribeToActionHandler.contextTypes = {
intl: intlShape
};

registerComponent('SubscribeTo', SubscribeTo, withCurrentUser, withMessages);
const subscribeMutationContainer = ({documentType, actionName}) => graphql(gql`
mutation ${documentType + actionName}($documentId: String) {
${documentType + actionName}(documentId: $documentId) {
_id
subscribedItems
}
}
`, {
props: ({ownProps, mutate}) => ({
[documentType + actionName]: vars => {
return mutate({
variables: vars,
});
},
}),
});

const SubscribeTo = props => {

const documentType = `${props.document.__typename.toLowerCase()}s`;

const withSubscribeMutations = ['Subscribe', 'Unsubscribe'].map(actionName => subscribeMutationContainer({documentType, actionName}));

const EnhancedHandler = compose(...withSubscribeMutations)(SubscribeToActionHandler);

return <EnhancedHandler {...props} documentType={documentType} />;
}


registerComponent('SubscribeTo', SubscribeTo, withCurrentUser, withMessages);
6 changes: 1 addition & 5 deletions packages/nova-subscribe/lib/custom_fields.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Users.addField([
optional: true,
blackbox: true,
hidden: true, // never show this
preload: true,
}
},
{
Expand All @@ -16,11 +17,6 @@ Users.addField([
type: [String],
optional: true,
hidden: true, // never show this,
// publish: true,
// join: {
// joinAs: "subscribersArray",
// collection: () => Users
// }
}
},
{
Expand Down
18 changes: 18 additions & 0 deletions packages/nova-subscribe/lib/helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import Users from 'meteor/nova:users';

Users.isSubscribedTo = (user, document) => {
// return user && document && document.subscribers && document.subscribers.indexOf(user._id) !== -1;
if (!user || !document) {
// should return an error
return false;
}

const { __typename, _id: itemId } = document;
const documentType = __typename + 's';

if (user.subscribedItems && user.subscribedItems[documentType]) {
return !!user.subscribedItems[documentType].find(subscribedItems => subscribedItems.itemId === itemId);
} else {
return false;
}
};
5 changes: 3 additions & 2 deletions packages/nova-subscribe/lib/modules.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import './callbacks.js';
import './custom_fields.js';
import subscribeMethodsGenerator from './methods.js';
import './helpers.js';
import subscribeMutationsGenerator from './mutations.js';
import './views.js';
import './permissions.js';

import './components/SubscribeTo.jsx';

export default subscribeMethodsGenerator;
export default subscribeMutationsGenerator;
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Users from 'meteor/nova:users';
import { Utils, GraphQLSchema } from 'meteor/nova:core';

/**
* @summary Verify that the un/subscription can be performed
Expand Down Expand Up @@ -36,8 +37,8 @@ const prepareSubscription = (action, collection, itemId, user) => {

// assign the right fields depending on the collection
const fields = {
subscribers: collectionName === 'Users' ? 'subscribers' : 'subscribers',
subscriberCount: collectionName === 'Users' ? 'subscriberCount' : 'subscriberCount',
subscribers: 'subscribers',
subscriberCount: 'subscriberCount',
};

// return true if the item has the subscriber's id in its fields
Expand Down Expand Up @@ -82,7 +83,7 @@ const performSubscriptionAction = (action, collection, itemId, user) => {
// - the action is subscribe but the user has already subscribed to this item
// - the action is unsubscribe but the user hasn't subscribed to this item
if (!subscription || (action === 'subscribe' && subscription.hasSubscribedItem) || (action === 'unsubscribe' && !subscription.hasSubscribedItem)) {
return false; // xxx: should return exploitable error
throw Error({id: 'app.mutation_not_allowed', value: 'Already subscribed'})
}

// shorthand for useful variables
Expand Down Expand Up @@ -122,58 +123,70 @@ const performSubscriptionAction = (action, collection, itemId, user) => {
[updateOperator]: { [`subscribedItems.${collectionName}`]: loggedItem }
});

return true; // action completed! ✅
const updatedUser = Users.findOne({_id: user._id}, {fields: {_id:1, subscribedItems: 1}});

return updatedUser;
} else {
return false; // xxx: should return exploitable error
throw Error(Utils.encodeIntlError({id: 'app.something_bad_happened'}))
}
};

/**
* @summary Generate methods 'collection.subscribe' & 'collection.unsubscribe' automatically
* @summary Generate mutations 'collection.subscribe' & 'collection.unsubscribe' automatically
* @params {Array[Collections]} collections
*/
let subscribeMethodsGenerator;
export default subscribeMethodsGenerator = (collection) => {
const subscribeMutationsGenerator = (collection) => {

// generic method function calling the performSubscriptionAction
const genericMethodFunction = (col, action) => {
// generic mutation function calling the performSubscriptionAction
const genericMutationFunction = (collectionName, action) => {
// return the method code
return function(docId, userId) {
check(docId, String);
check(userId, Match.Maybe(String));

const currentUser = Users.findOne({_id: this.userId}); // this refers to Meteor thanks to previous fat arrows when this function-builder is used
const user = typeof userId !== "undefined" ? Users.findOne({_id: userId }) : currentUser;

if (!Users.canDo(currentUser, `${col._name}.${action}`) || typeof userId !== "undefined" && !Users.canDo(currentUser, `${col._name}.${action}.all`)) {
throw new Error(601, "You don't have the permission to do this");
return function(root, { documentId }, context) {

// extract the current user & the relevant collection from the graphql server context
const { currentUser, [Utils.capitalize(collectionName)]: collection } = context;

// permission check
if (!Users.canDo(context.currentUser, `${collectionName}.${action}`) || !Users.canDo(currentUser, `${collectionName}.${action}.all`)) {
throw new Error(Utils.encodeIntlError({id: "app.noPermission"}));
}

return performSubscriptionAction(action, col, docId, user);

// do the actual subscription action
return performSubscriptionAction(action, collection, documentId, currentUser);
};
};

const collectionName = collection._name;
// return an object of the shape expected by Meteor.methods
return {
[`${collectionName}.subscribe`]: genericMethodFunction(collection, 'subscribe'),
[`${collectionName}.unsubscribe`]: genericMethodFunction(collection, 'unsubscribe')
};

// add mutations to the schema
GraphQLSchema.addMutation(`${collectionName}Subscribe(documentId: String): User`),
GraphQLSchema.addMutation(`${collectionName}Unsubscribe(documentId: String): User`);

// create an object of the shape expected by mutations resolvers
GraphQLSchema.addResolvers({
Mutation: {
[`${collectionName}Subscribe`]: genericMutationFunction(collectionName, 'subscribe'),
[`${collectionName}Unsubscribe`]: genericMutationFunction(collectionName, 'unsubscribe'),
},
});


};

// Finally. Add the methods to the Meteor namespace 🖖
// Finally. Add the mutations to the Meteor namespace 🖖

// nova:users is a dependency of this package, it is alreay imported
Meteor.methods(subscribeMethodsGenerator(Users));
subscribeMutationsGenerator(Users);

// check if nova:posts exists, if yes, add the methods to Posts
// check if nova:posts exists, if yes, add the mutations to Posts
if (typeof Package['nova:posts'] !== 'undefined') {
import Posts from 'meteor/nova:posts';
Meteor.methods(subscribeMethodsGenerator(Posts));
subscribeMutationsGenerator(Posts);
}

// check if nova:categories exists, if yes, add the methods to Categories
// check if nova:categories exists, if yes, add the mutations to Categories
if (typeof Package['nova:categories'] !== "undefined") {
import Categories from 'meteor/nova:categories';
Meteor.methods(subscribeMethodsGenerator(Categories));
subscribeMutationsGenerator(Categories);
}

export default subscribeMutationsGenerator;
Loading

0 comments on commit 42285a8

Please sign in to comment.