Skip to content

Commit 42285a8

Browse files
committed
re-enable nova:subscribe
1 parent 7f73f5a commit 42285a8

File tree

8 files changed

+174
-115
lines changed

8 files changed

+174
-115
lines changed

packages/nova-subscribe/README.md

+10-10
Original file line numberDiff line numberDiff line change
@@ -25,35 +25,35 @@ categories.unsubscribe
2525

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

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

3030
```jsx
3131
// for example, in PostItem.jsx
32-
<Telescope.components.SubscribeTo document={post} documentType={"posts"} />
32+
<Components.SubscribeTo document={post} />
3333

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

3737
// for example, in Category.jsx
38-
<Telescope.components.SubscribeTo document={category} documentType={"categories"} />
38+
<Components.SubscribeTo document={category} />
3939
```
4040

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

4444
```js
4545
// in my custom package
46-
import subscribeMethodsGenerator from 'meteor/nova:subscribe';
46+
import subscribMutationsGenerator from 'meteor/nova:subscribe';
4747
import Movies from './collection.js';
4848

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

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

5959
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.

packages/nova-subscribe/lib/callbacks.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import Telescope from 'meteor/nova:lib';
1+
import Telescope from 'meteor/nova:lib'; // note: Telescope.notifications
22
import Users from 'meteor/nova:users';
33
import { addCallback } from 'meteor/nova:core';
44

Original file line numberDiff line numberDiff line change
@@ -1,75 +1,106 @@
11
import React, { PropTypes, Component } from 'react';
2-
import { intlShape } from 'react-intl';
3-
import { withCurrentUser, withMessages, registerComponent } from 'meteor/nova:core';
2+
import { intlShape, FormattedMessage } from 'react-intl';
3+
import { compose, graphql } from 'react-apollo';
4+
import gql from 'graphql-tag';
5+
import Users from 'meteor/nova:users';
6+
import { withCurrentUser, withMessages, registerComponent, Utils } from 'meteor/nova:core';
47

5-
class SubscribeTo extends Component {
8+
// boolean -> unsubscribe || subscribe
9+
const getSubscribeAction = subscribed => subscribed ? 'unsubscribe' : 'subscribe'
10+
11+
class SubscribeToActionHandler extends Component {
612

713
constructor(props, context) {
814
super(props, context);
915

1016
this.onSubscribe = this.onSubscribe.bind(this);
11-
this.isSubscribed = this.isSubscribed.bind(this);
12-
}
13-
14-
onSubscribe(e) {
15-
e.preventDefault();
16-
17-
const {document, documentType} = this.props;
18-
19-
const action = this.isSubscribed() ? `unsubscribe` : `subscribe`;
20-
21-
// method name will be for example posts.subscribe
22-
this.context.actions.call(`${documentType}.${action}`, document._id, (error, result) => {
23-
if (error) {
24-
this.props.flash(error.message, "error");
25-
}
26-
27-
if (result) {
28-
// success message will be for example posts.subscribed
29-
this.props.flash(this.context.intl.formatMessage(
30-
{id: `${documentType}.${action}d`},
31-
// handle usual name properties
32-
{name: document.name || document.title || document.displayName}
33-
), "success");
34-
this.context.events.track(action, {'_id': this.props.document._id});
35-
}
36-
});
17+
18+
this.state = {
19+
subscribed: !!Users.isSubscribedTo(props.currentUser, props.document, props.documentType),
20+
};
3721
}
3822

39-
isSubscribed() {
40-
const documentCheck = this.props.document;
41-
42-
return documentCheck && documentCheck.subscribers && documentCheck.subscribers.indexOf(this.context.currentUser._id) !== -1;
23+
async onSubscribe(e) {
24+
try {
25+
e.preventDefault();
26+
27+
const { document, documentType } = this.props;
28+
const action = getSubscribeAction(this.state.subscribed);
29+
30+
// todo: change the mutation to auto-update the user in the store
31+
await this.setState(prevState => ({subscribed: !prevState.subscribed}));
32+
33+
// mutation name will be for example postsSubscribe
34+
await this.props[`${documentType + Utils.capitalize(action)}`]({documentId: document._id});
35+
36+
// success message will be for example posts.subscribed
37+
this.props.flash(this.context.intl.formatMessage(
38+
{id: `${documentType}.${action}d`},
39+
// handle usual name properties
40+
{name: document.name || document.title || document.displayName}
41+
), "success");
42+
43+
44+
} catch(error) {
45+
this.props.flash(error.message, "error");
46+
}
4347
}
4448

4549
render() {
46-
const {currentUser, document, documentType} = this.props;
47-
50+
const { currentUser, document, documentType } = this.props;
51+
const { subscribed } = this.state;
52+
53+
const action = `${documentType}.${getSubscribeAction(subscribed)}`;
54+
4855
// can't subscribe to yourself or to own post (also validated on server side)
49-
if (!currentUser || !document || (documentType === 'posts' && document && document.author === currentUser.username) || (documentType === 'users' && document === currentUser)) {
56+
if (!currentUser || !document || (documentType === 'posts' && document.userId === currentUser._id) || (documentType === 'users' && document._id === currentUser._id)) {
5057
return null;
5158
}
5259

53-
const action = this.isSubscribed() ? `${documentType}.unsubscribe` : `${documentType}.subscribe`;
54-
5560
const className = this.props.className ? this.props.className : "";
56-
57-
return Users.canDo(currentUser, action) ? <a className={className} onClick={this.onSubscribe}>{this.context.intl.formatMessage({id: action})}</a> : null;
61+
62+
return Users.canDo(currentUser, action) ? <a className={className} onClick={this.onSubscribe}><FormattedMessage id={action} /></a> : null;
5863
}
5964

6065
}
6166

62-
SubscribeTo.propTypes = {
67+
SubscribeToActionHandler.propTypes = {
6368
document: React.PropTypes.object.isRequired,
64-
documentType: React.PropTypes.string.isRequired,
6569
className: React.PropTypes.string,
6670
currentUser: React.PropTypes.object,
6771
}
6872

69-
SubscribeTo.contextTypes = {
70-
actions: React.PropTypes.object,
71-
events: React.PropTypes.object,
73+
SubscribeToActionHandler.contextTypes = {
7274
intl: intlShape
7375
};
7476

75-
registerComponent('SubscribeTo', SubscribeTo, withCurrentUser, withMessages);
77+
const subscribeMutationContainer = ({documentType, actionName}) => graphql(gql`
78+
mutation ${documentType + actionName}($documentId: String) {
79+
${documentType + actionName}(documentId: $documentId) {
80+
_id
81+
subscribedItems
82+
}
83+
}
84+
`, {
85+
props: ({ownProps, mutate}) => ({
86+
[documentType + actionName]: vars => {
87+
return mutate({
88+
variables: vars,
89+
});
90+
},
91+
}),
92+
});
93+
94+
const SubscribeTo = props => {
95+
96+
const documentType = `${props.document.__typename.toLowerCase()}s`;
97+
98+
const withSubscribeMutations = ['Subscribe', 'Unsubscribe'].map(actionName => subscribeMutationContainer({documentType, actionName}));
99+
100+
const EnhancedHandler = compose(...withSubscribeMutations)(SubscribeToActionHandler);
101+
102+
return <EnhancedHandler {...props} documentType={documentType} />;
103+
}
104+
105+
106+
registerComponent('SubscribeTo', SubscribeTo, withCurrentUser, withMessages);

packages/nova-subscribe/lib/custom_fields.js

+1-5
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Users.addField([
88
optional: true,
99
blackbox: true,
1010
hidden: true, // never show this
11+
preload: true,
1112
}
1213
},
1314
{
@@ -16,11 +17,6 @@ Users.addField([
1617
type: [String],
1718
optional: true,
1819
hidden: true, // never show this,
19-
// publish: true,
20-
// join: {
21-
// joinAs: "subscribersArray",
22-
// collection: () => Users
23-
// }
2420
}
2521
},
2622
{
+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import Users from 'meteor/nova:users';
2+
3+
Users.isSubscribedTo = (user, document) => {
4+
// return user && document && document.subscribers && document.subscribers.indexOf(user._id) !== -1;
5+
if (!user || !document) {
6+
// should return an error
7+
return false;
8+
}
9+
10+
const { __typename, _id: itemId } = document;
11+
const documentType = __typename + 's';
12+
13+
if (user.subscribedItems && user.subscribedItems[documentType]) {
14+
return !!user.subscribedItems[documentType].find(subscribedItems => subscribedItems.itemId === itemId);
15+
} else {
16+
return false;
17+
}
18+
};
+3-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import './callbacks.js';
22
import './custom_fields.js';
3-
import subscribeMethodsGenerator from './methods.js';
3+
import './helpers.js';
4+
import subscribeMutationsGenerator from './mutations.js';
45
import './views.js';
56
import './permissions.js';
67

78
import './components/SubscribeTo.jsx';
89

9-
export default subscribeMethodsGenerator;
10+
export default subscribeMutationsGenerator;

packages/nova-subscribe/lib/methods.js packages/nova-subscribe/lib/mutations.js

+45-32
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Users from 'meteor/nova:users';
2+
import { Utils, GraphQLSchema } from 'meteor/nova:core';
23

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

3738
// assign the right fields depending on the collection
3839
const fields = {
39-
subscribers: collectionName === 'Users' ? 'subscribers' : 'subscribers',
40-
subscriberCount: collectionName === 'Users' ? 'subscriberCount' : 'subscriberCount',
40+
subscribers: 'subscribers',
41+
subscriberCount: 'subscriberCount',
4142
};
4243

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

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

125-
return true; // action completed! ✅
126+
const updatedUser = Users.findOne({_id: user._id}, {fields: {_id:1, subscribedItems: 1}});
127+
128+
return updatedUser;
126129
} else {
127-
return false; // xxx: should return exploitable error
130+
throw Error(Utils.encodeIntlError({id: 'app.something_bad_happened'}))
128131
}
129132
};
130133

131134
/**
132-
* @summary Generate methods 'collection.subscribe' & 'collection.unsubscribe' automatically
135+
* @summary Generate mutations 'collection.subscribe' & 'collection.unsubscribe' automatically
133136
* @params {Array[Collections]} collections
134137
*/
135-
let subscribeMethodsGenerator;
136-
export default subscribeMethodsGenerator = (collection) => {
138+
const subscribeMutationsGenerator = (collection) => {
137139

138-
// generic method function calling the performSubscriptionAction
139-
const genericMethodFunction = (col, action) => {
140+
// generic mutation function calling the performSubscriptionAction
141+
const genericMutationFunction = (collectionName, action) => {
140142
// return the method code
141-
return function(docId, userId) {
142-
check(docId, String);
143-
check(userId, Match.Maybe(String));
144-
145-
const currentUser = Users.findOne({_id: this.userId}); // this refers to Meteor thanks to previous fat arrows when this function-builder is used
146-
const user = typeof userId !== "undefined" ? Users.findOne({_id: userId }) : currentUser;
147-
148-
if (!Users.canDo(currentUser, `${col._name}.${action}`) || typeof userId !== "undefined" && !Users.canDo(currentUser, `${col._name}.${action}.all`)) {
149-
throw new Error(601, "You don't have the permission to do this");
143+
return function(root, { documentId }, context) {
144+
145+
// extract the current user & the relevant collection from the graphql server context
146+
const { currentUser, [Utils.capitalize(collectionName)]: collection } = context;
147+
148+
// permission check
149+
if (!Users.canDo(context.currentUser, `${collectionName}.${action}`) || !Users.canDo(currentUser, `${collectionName}.${action}.all`)) {
150+
throw new Error(Utils.encodeIntlError({id: "app.noPermission"}));
150151
}
151-
152-
return performSubscriptionAction(action, col, docId, user);
152+
153+
// do the actual subscription action
154+
return performSubscriptionAction(action, collection, documentId, currentUser);
153155
};
154156
};
155157

156158
const collectionName = collection._name;
157-
// return an object of the shape expected by Meteor.methods
158-
return {
159-
[`${collectionName}.subscribe`]: genericMethodFunction(collection, 'subscribe'),
160-
[`${collectionName}.unsubscribe`]: genericMethodFunction(collection, 'unsubscribe')
161-
};
159+
160+
// add mutations to the schema
161+
GraphQLSchema.addMutation(`${collectionName}Subscribe(documentId: String): User`),
162+
GraphQLSchema.addMutation(`${collectionName}Unsubscribe(documentId: String): User`);
163+
164+
// create an object of the shape expected by mutations resolvers
165+
GraphQLSchema.addResolvers({
166+
Mutation: {
167+
[`${collectionName}Subscribe`]: genericMutationFunction(collectionName, 'subscribe'),
168+
[`${collectionName}Unsubscribe`]: genericMutationFunction(collectionName, 'unsubscribe'),
169+
},
170+
});
171+
172+
162173
};
163174

164-
// Finally. Add the methods to the Meteor namespace 🖖
175+
// Finally. Add the mutations to the Meteor namespace 🖖
165176

166177
// nova:users is a dependency of this package, it is alreay imported
167-
Meteor.methods(subscribeMethodsGenerator(Users));
178+
subscribeMutationsGenerator(Users);
168179

169-
// check if nova:posts exists, if yes, add the methods to Posts
180+
// check if nova:posts exists, if yes, add the mutations to Posts
170181
if (typeof Package['nova:posts'] !== 'undefined') {
171182
import Posts from 'meteor/nova:posts';
172-
Meteor.methods(subscribeMethodsGenerator(Posts));
183+
subscribeMutationsGenerator(Posts);
173184
}
174185

175-
// check if nova:categories exists, if yes, add the methods to Categories
186+
// check if nova:categories exists, if yes, add the mutations to Categories
176187
if (typeof Package['nova:categories'] !== "undefined") {
177188
import Categories from 'meteor/nova:categories';
178-
Meteor.methods(subscribeMethodsGenerator(Categories));
189+
subscribeMutationsGenerator(Categories);
179190
}
191+
192+
export default subscribeMutationsGenerator;

0 commit comments

Comments
 (0)