diff --git a/packages/nova-subscribe/README.md b/packages/nova-subscribe/README.md
index 60dd50386a..5bcaef9a5f 100644
--- a/packages/nova-subscribe/README.md
+++ b/packages/nova-subscribe/README.md
@@ -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
-
+
// for example, in UsersProfile.jsx
-
+
// for example, in Category.jsx
-
+
```
### 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
-
+
```
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.
diff --git a/packages/nova-subscribe/lib/callbacks.js b/packages/nova-subscribe/lib/callbacks.js
index b399c64ce6..eabb08e08c 100644
--- a/packages/nova-subscribe/lib/callbacks.js
+++ b/packages/nova-subscribe/lib/callbacks.js
@@ -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';
diff --git a/packages/nova-subscribe/lib/components/SubscribeTo.jsx b/packages/nova-subscribe/lib/components/SubscribeTo.jsx
index 98c2032f9b..66712c5e3f 100644
--- a/packages/nova-subscribe/lib/components/SubscribeTo.jsx
+++ b/packages/nova-subscribe/lib/components/SubscribeTo.jsx
@@ -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) ? {this.context.intl.formatMessage({id: action})} : null;
+
+ return Users.canDo(currentUser, action) ? : 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);
\ No newline at end of file
+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 ;
+}
+
+
+registerComponent('SubscribeTo', SubscribeTo, withCurrentUser, withMessages);
diff --git a/packages/nova-subscribe/lib/custom_fields.js b/packages/nova-subscribe/lib/custom_fields.js
index 21bc96f6d9..3c58a8e173 100644
--- a/packages/nova-subscribe/lib/custom_fields.js
+++ b/packages/nova-subscribe/lib/custom_fields.js
@@ -8,6 +8,7 @@ Users.addField([
optional: true,
blackbox: true,
hidden: true, // never show this
+ preload: true,
}
},
{
@@ -16,11 +17,6 @@ Users.addField([
type: [String],
optional: true,
hidden: true, // never show this,
- // publish: true,
- // join: {
- // joinAs: "subscribersArray",
- // collection: () => Users
- // }
}
},
{
diff --git a/packages/nova-subscribe/lib/helpers.js b/packages/nova-subscribe/lib/helpers.js
new file mode 100644
index 0000000000..e1c4de0469
--- /dev/null
+++ b/packages/nova-subscribe/lib/helpers.js
@@ -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;
+ }
+};
diff --git a/packages/nova-subscribe/lib/modules.js b/packages/nova-subscribe/lib/modules.js
index bb6d992f1b..6f2dfb4842 100644
--- a/packages/nova-subscribe/lib/modules.js
+++ b/packages/nova-subscribe/lib/modules.js
@@ -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;
\ No newline at end of file
+export default subscribeMutationsGenerator;
diff --git a/packages/nova-subscribe/lib/methods.js b/packages/nova-subscribe/lib/mutations.js
similarity index 67%
rename from packages/nova-subscribe/lib/methods.js
rename to packages/nova-subscribe/lib/mutations.js
index 8dd90ce351..a7b159f968 100644
--- a/packages/nova-subscribe/lib/methods.js
+++ b/packages/nova-subscribe/lib/mutations.js
@@ -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
@@ -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
@@ -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
@@ -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;
diff --git a/packages/nova-subscribe/lib/views.js b/packages/nova-subscribe/lib/views.js
index 7e4cd1747c..e8198d8ba2 100644
--- a/packages/nova-subscribe/lib/views.js
+++ b/packages/nova-subscribe/lib/views.js
@@ -1,19 +1,19 @@
-import Users from 'meteor/nova:users';
-
-if (typeof Package['nova:posts'] !== "undefined") {
- import Posts from "meteor/nova:posts";
-
- Posts.views.add("userSubscribedPosts", function (terms) {
- var user = Users.findOne(terms.userId),
- postsIds = [];
-
- if (user && user.subscribedItems && user.subscribedItems.Posts) {
- postsIds = _.pluck(user.subscribedItems.Posts, "itemId");
- }
-
- return {
- selector: {_id: {$in: postsIds}},
- options: {limit: 5, sort: {postedAt: -1}}
- };
- });
-}
+// import Users from 'meteor/nova:users';
+//
+// if (typeof Package['nova:posts'] !== "undefined") {
+// import Posts from "meteor/nova:posts";
+//
+// Posts.views.add("userSubscribedPosts", function (terms) {
+// var user = Users.findOne(terms.userId),
+// postsIds = [];
+//
+// if (user && user.subscribedItems && user.subscribedItems.Posts) {
+// postsIds = _.pluck(user.subscribedItems.Posts, "itemId");
+// }
+//
+// return {
+// selector: {_id: {$in: postsIds}},
+// options: {limit: 5, sort: {postedAt: -1}}
+// };
+// });
+// }