mirror of
https://github.com/wassname/talk.git
synced 2026-06-30 17:25:31 +08:00
@@ -94,6 +94,11 @@ if (app.get('env') !== 'production') {
|
||||
endpointURL: '/api/v1/graph/ql'
|
||||
}));
|
||||
|
||||
// GraphQL documention.
|
||||
app.get('/admin/docs', (req, res) => {
|
||||
res.render('admin/docs');
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import {GraphQLDocs} from 'graphql-docs';
|
||||
|
||||
import fetcher from './services/fetcher';
|
||||
|
||||
// Render the application into the DOM
|
||||
ReactDOM.render(<GraphQLDocs fetcher={fetcher} />, document.querySelector('#root'));
|
||||
@@ -0,0 +1,10 @@
|
||||
export default function fetcher(query) {
|
||||
return fetch(`${window.location.host}/api/v1/graph/ql`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({query}),
|
||||
}).then((res) => res.json());
|
||||
}
|
||||
@@ -18,7 +18,8 @@ import LikeButton from 'coral-plugin-likes/LikeButton';
|
||||
|
||||
import styles from './Comment.css';
|
||||
|
||||
const getAction = (type, comment) => comment.actions.filter((a) => a.type === type)[0];
|
||||
const getActionSummary = (type, comment) => comment.action_summaries
|
||||
.filter((a) => a.__typename === type)[0];
|
||||
|
||||
class Comment extends React.Component {
|
||||
|
||||
@@ -35,7 +36,8 @@ class Comment extends React.Component {
|
||||
setActiveReplyBox: PropTypes.func.isRequired,
|
||||
refetch: PropTypes.func.isRequired,
|
||||
showSignInDialog: PropTypes.func.isRequired,
|
||||
postAction: PropTypes.func.isRequired,
|
||||
postFlag: PropTypes.func.isRequired,
|
||||
postLike: PropTypes.func.isRequired,
|
||||
deleteAction: PropTypes.func.isRequired,
|
||||
parentId: PropTypes.string,
|
||||
addNotification: PropTypes.func.isRequired,
|
||||
@@ -51,7 +53,7 @@ class Comment extends React.Component {
|
||||
}),
|
||||
comment: PropTypes.shape({
|
||||
depth: PropTypes.number,
|
||||
actions: PropTypes.array.isRequired,
|
||||
action_summaries: PropTypes.array.isRequired,
|
||||
body: PropTypes.string.isRequired,
|
||||
id: PropTypes.string.isRequired,
|
||||
replies: PropTypes.arrayOf(
|
||||
@@ -78,14 +80,15 @@ class Comment extends React.Component {
|
||||
refetch,
|
||||
addNotification,
|
||||
showSignInDialog,
|
||||
postAction,
|
||||
postLike,
|
||||
postFlag,
|
||||
setActiveReplyBox,
|
||||
activeReplyBox,
|
||||
deleteAction
|
||||
} = this.props;
|
||||
|
||||
const like = getAction('LIKE', comment);
|
||||
const flag = getAction('FLAG', comment);
|
||||
const like = getActionSummary('LikeActionSummary', comment);
|
||||
const flag = getActionSummary('FlagActionSummary', comment);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -101,7 +104,7 @@ class Comment extends React.Component {
|
||||
<LikeButton
|
||||
like={like}
|
||||
id={comment.id}
|
||||
postAction={postAction}
|
||||
postLike={postLike}
|
||||
deleteAction={deleteAction}
|
||||
showSignInDialog={showSignInDialog}
|
||||
currentUser={currentUser} />
|
||||
@@ -117,7 +120,7 @@ class Comment extends React.Component {
|
||||
flag={flag}
|
||||
id={comment.id}
|
||||
author_id={comment.user.id}
|
||||
postAction={postAction}
|
||||
postFlag={postFlag}
|
||||
deleteAction={deleteAction}
|
||||
showSignInDialog={showSignInDialog}
|
||||
currentUser={currentUser} />
|
||||
@@ -150,7 +153,8 @@ class Comment extends React.Component {
|
||||
depth={depth + 1}
|
||||
asset={asset}
|
||||
currentUser={currentUser}
|
||||
postAction={postAction}
|
||||
postLike={postLike}
|
||||
postFlag={postFlag}
|
||||
deleteAction={deleteAction}
|
||||
showSignInDialog={showSignInDialog}
|
||||
reactKey={reply.id}
|
||||
@@ -158,7 +162,6 @@ class Comment extends React.Component {
|
||||
comment={reply} />;
|
||||
})
|
||||
}
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ const {addNotification, clearNotification} = notificationActions;
|
||||
const {fetchAssetSuccess} = assetActions;
|
||||
|
||||
import {queryStream} from 'coral-framework/graphql/queries';
|
||||
import {postComment, postAction, deleteAction} from 'coral-framework/graphql/mutations';
|
||||
import {postComment, postFlag, postLike, deleteAction} from 'coral-framework/graphql/mutations';
|
||||
import {editName} from 'coral-framework/actions/user';
|
||||
import {Notification, notificationActions, authActions, assetActions, pym} from 'coral-framework';
|
||||
|
||||
@@ -91,7 +91,7 @@ class Embed extends Component {
|
||||
minHeight: document.body.scrollHeight + 200
|
||||
} : {};
|
||||
|
||||
if (loading) {
|
||||
if (loading || !asset) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
@@ -146,7 +146,8 @@ class Embed extends Component {
|
||||
postItem={this.props.postItem}
|
||||
asset={asset}
|
||||
currentUser={user}
|
||||
postAction={this.props.postAction}
|
||||
postLike={this.props.postLike}
|
||||
postFlag={this.props.postFlag}
|
||||
deleteAction={this.props.deleteAction}
|
||||
showSignInDialog={this.props.showSignInDialog}
|
||||
comments={asset.comments} />
|
||||
@@ -209,7 +210,8 @@ const mapDispatchToProps = dispatch => ({
|
||||
export default compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
postComment,
|
||||
postAction,
|
||||
postFlag,
|
||||
postLike,
|
||||
deleteAction,
|
||||
queryStream
|
||||
)(Embed);
|
||||
|
||||
@@ -37,7 +37,8 @@ class Stream extends React.Component {
|
||||
asset,
|
||||
postItem,
|
||||
addNotification,
|
||||
postAction,
|
||||
postFlag,
|
||||
postLike,
|
||||
deleteAction,
|
||||
showSignInDialog,
|
||||
refetch
|
||||
@@ -56,7 +57,8 @@ class Stream extends React.Component {
|
||||
postItem={postItem}
|
||||
asset={asset}
|
||||
currentUser={currentUser}
|
||||
postAction={postAction}
|
||||
postLike={postLike}
|
||||
postFlag={postFlag}
|
||||
deleteAction={deleteAction}
|
||||
showSignInDialog={showSignInDialog}
|
||||
key={comment.id}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
fragment actionSummaryView on ActionSummary {
|
||||
__typename
|
||||
count
|
||||
current_user {
|
||||
id
|
||||
created_at
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
#import "../fragments/actionSummaryView.graphql"
|
||||
|
||||
fragment commentView on Comment {
|
||||
id
|
||||
body
|
||||
@@ -7,12 +9,7 @@ fragment commentView on Comment {
|
||||
id
|
||||
name: displayName
|
||||
}
|
||||
actions {
|
||||
type: action_type
|
||||
count
|
||||
current: current_user {
|
||||
id
|
||||
created_at
|
||||
}
|
||||
action_summaries {
|
||||
...actionSummaryView
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
mutation deleteAction ($id: ID!) {
|
||||
deleteAction(id:$id)
|
||||
deleteAction(id:$id) {
|
||||
errors {
|
||||
translation_key
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {graphql} from 'react-apollo';
|
||||
import POST_COMMENT from './postComment.graphql';
|
||||
import POST_ACTION from './postAction.graphql';
|
||||
import POST_FLAG from './postFlag.graphql';
|
||||
import POST_LIKE from './postLike.graphql';
|
||||
import DELETE_ACTION from './deleteAction.graphql';
|
||||
|
||||
import commentView from '../fragments/commentView.graphql';
|
||||
@@ -21,12 +22,23 @@ export const postComment = graphql(POST_COMMENT, {
|
||||
}}),
|
||||
});
|
||||
|
||||
export const postAction = graphql(POST_ACTION, {
|
||||
export const postLike = graphql(POST_LIKE, {
|
||||
props: ({mutate}) => ({
|
||||
postAction: (action) => {
|
||||
postLike: (like) => {
|
||||
return mutate({
|
||||
variables: {
|
||||
action
|
||||
like
|
||||
}
|
||||
});
|
||||
}}),
|
||||
});
|
||||
|
||||
export const postFlag = graphql(POST_FLAG, {
|
||||
props: ({mutate}) => ({
|
||||
postFlag: (flag) => {
|
||||
return mutate({
|
||||
variables: {
|
||||
flag
|
||||
}
|
||||
});
|
||||
}}),
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
mutation CreateAction ($action: CreateActionInput!) {
|
||||
createAction(action:$action) {
|
||||
id
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,11 @@
|
||||
|
||||
mutation CreateComment ($asset_id: ID!, $parent_id: ID, $body: String!) {
|
||||
createComment(asset_id:$asset_id, parent_id:$parent_id, body:$body) {
|
||||
...commentView
|
||||
comment {
|
||||
...commentView
|
||||
}
|
||||
errors {
|
||||
translation_key
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
mutation CreateFlag($flag: CreateFlagInput!) {
|
||||
createFlag(flag:$flag) {
|
||||
flag {
|
||||
id
|
||||
}
|
||||
errors {
|
||||
translation_key
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
mutation CreateLike ($like: CreateLikeInput!) {
|
||||
createLike(like:$like) {
|
||||
like {
|
||||
id
|
||||
}
|
||||
errors {
|
||||
translation_key
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ function getQueryVariable(variable) {
|
||||
}
|
||||
|
||||
// If no query is included, return a default string for development
|
||||
return 'http://dev.default.stream';
|
||||
return 'http://localhost/default/stream';
|
||||
}
|
||||
|
||||
export const queryStream = graphql(STREAM_QUERY, {
|
||||
|
||||
@@ -8,9 +8,6 @@ const name = 'coral-plugin-commentbox';
|
||||
class CommentBox extends Component {
|
||||
|
||||
static propTypes = {
|
||||
|
||||
// updateItem: PropTypes.func,
|
||||
// comments: PropTypes.array,
|
||||
commentPostedHandler: PropTypes.func,
|
||||
postItem: PropTypes.func.isRequired,
|
||||
cancelButtonClicked: PropTypes.func,
|
||||
@@ -29,10 +26,6 @@ class CommentBox extends Component {
|
||||
|
||||
postComment = () => {
|
||||
const {
|
||||
|
||||
// child_id,
|
||||
// updateItem,
|
||||
// appendItemArray,
|
||||
commentPostedHandler,
|
||||
postItem,
|
||||
assetId,
|
||||
@@ -48,28 +41,13 @@ class CommentBox extends Component {
|
||||
parent_id: parentId
|
||||
};
|
||||
|
||||
// let related;
|
||||
// let parent_type;
|
||||
// if (parent_id) {
|
||||
// comment.parent_id = parent_id;
|
||||
// related = 'children';
|
||||
// parent_type = 'comments';
|
||||
// } else {
|
||||
// related = 'comments';
|
||||
// parent_type = 'assets';
|
||||
// }
|
||||
// if (child_id || parent_id) {
|
||||
// updateItem(child_id || parent_id, 'showReply', false, 'comments');
|
||||
// }
|
||||
|
||||
if (this.props.charCount && this.state.body.length > this.props.charCount) {
|
||||
return;
|
||||
}
|
||||
postItem(comment, 'comments')
|
||||
.then(({data}) => {
|
||||
const postedComment = data.createComment;
|
||||
const postedComment = data.createComment.comment;
|
||||
|
||||
// const commentId = postedComment.id;
|
||||
if (postedComment.status === 'REJECTED') {
|
||||
addNotification('error', lang.t('comment-post-banned-word'));
|
||||
} else if (postedComment.status === 'PREMOD') {
|
||||
|
||||
@@ -12,7 +12,7 @@ class FlagButton extends Component {
|
||||
showMenu: false,
|
||||
itemType: '',
|
||||
reason: '',
|
||||
note: '',
|
||||
message: '',
|
||||
step: 0,
|
||||
posted: false,
|
||||
localPost: null,
|
||||
@@ -23,7 +23,7 @@ class FlagButton extends Component {
|
||||
onReportClick = () => {
|
||||
const {currentUser, flag, deleteAction} = this.props;
|
||||
const {localPost, localDelete} = this.state;
|
||||
const flagged = (flag && flag.current && !localDelete) || localPost;
|
||||
const flagged = (flag && flag.current_user && !localDelete) || localPost;
|
||||
if (!currentUser) {
|
||||
const offset = document.getElementById(`c_${this.props.id}`).getBoundingClientRect().top - 75;
|
||||
this.props.showSignInDialog(offset);
|
||||
@@ -31,15 +31,15 @@ class FlagButton extends Component {
|
||||
}
|
||||
if (flagged) {
|
||||
this.setState((prev) => prev.localPost ? {...prev, localPost: null, step: 0} : {...prev, localDelete: true});
|
||||
deleteAction(localPost || flag.current.id);
|
||||
deleteAction(localPost || flag.current_user.id);
|
||||
} else {
|
||||
this.setState({showMenu: !this.state.showMenu});
|
||||
}
|
||||
}
|
||||
|
||||
onPopupContinue = () => {
|
||||
const {postAction, id, author_id} = this.props;
|
||||
const {itemType, reason, step, posted} = this.state;
|
||||
const {postFlag, id, author_id} = this.props;
|
||||
const {itemType, reason, step, posted, message} = this.state;
|
||||
|
||||
// Proceed to the next step or close the menu if we've reached the end
|
||||
if (step + 1 >= this.props.getPopupMenu.length) {
|
||||
@@ -67,13 +67,14 @@ class FlagButton extends Component {
|
||||
if (itemType === 'COMMENTS') {
|
||||
this.setState({localPost: 'temp'});
|
||||
}
|
||||
postAction({
|
||||
postFlag({
|
||||
item_id,
|
||||
item_type: itemType,
|
||||
action_type: 'FLAG'
|
||||
reason,
|
||||
message
|
||||
}).then(({data}) => {
|
||||
if (itemType === 'COMMENTS') {
|
||||
this.setState({localPost: data.createAction.id});
|
||||
this.setState({localPost: data.createFlag.flag.id});
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -99,7 +100,7 @@ class FlagButton extends Component {
|
||||
}
|
||||
|
||||
onNoteTextChange = (e) => {
|
||||
this.setState({note: e.target.value});
|
||||
this.setState({message: e.target.value});
|
||||
}
|
||||
|
||||
handleClickOutside () {
|
||||
@@ -109,7 +110,7 @@ class FlagButton extends Component {
|
||||
render () {
|
||||
const {flag, getPopupMenu} = this.props;
|
||||
const {localPost, localDelete} = this.state;
|
||||
const flagged = (flag && flag.current && !localDelete) || localPost;
|
||||
const flagged = (flag && flag.current_user && !localDelete) || localPost;
|
||||
const popupMenu = getPopupMenu[this.state.step](this.state.itemType);
|
||||
|
||||
return <div className={`${name}-container`}>
|
||||
@@ -150,15 +151,15 @@ class FlagButton extends Component {
|
||||
}
|
||||
{
|
||||
this.state.reason && <div>
|
||||
<label htmlFor={'note'} className={`${name}-popup-radio-label`}>
|
||||
<label htmlFor={'message'} className={`${name}-popup-radio-label`}>
|
||||
{lang.t('flag-reason')}
|
||||
</label><br/>
|
||||
<textarea
|
||||
className={`${name}-reason-text`}
|
||||
id="note"
|
||||
id="message"
|
||||
rows={4}
|
||||
onChange={this.onNoteTextChange}
|
||||
value={this.state.note}/>
|
||||
value={this.state.message}/>
|
||||
</div>
|
||||
}
|
||||
</form>
|
||||
|
||||
@@ -12,7 +12,7 @@ class LikeButton extends Component {
|
||||
count: PropTypes.number
|
||||
}),
|
||||
id: PropTypes.string,
|
||||
postAction: PropTypes.func.isRequired,
|
||||
postLike: PropTypes.func.isRequired,
|
||||
deleteAction: PropTypes.func.isRequired,
|
||||
showSignInDialog: PropTypes.func.isRequired,
|
||||
currentUser: PropTypes.shape({
|
||||
@@ -26,9 +26,9 @@ class LikeButton extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const {like, id, postAction, deleteAction, showSignInDialog, currentUser} = this.props;
|
||||
const {like, id, postLike, deleteAction, showSignInDialog, currentUser} = this.props;
|
||||
const {localPost, localDelete} = this.state;
|
||||
const liked = (like && like.current && !localDelete) || localPost;
|
||||
const liked = (like && like.current_user && !localDelete) || localPost;
|
||||
let count = like ? like.count : 0;
|
||||
if (localPost) {count += 1;}
|
||||
if (localDelete) {count -= 1;}
|
||||
@@ -44,16 +44,15 @@ class LikeButton extends Component {
|
||||
}
|
||||
if (!liked) {
|
||||
this.setState({localPost: 'temp', localDelete: false});
|
||||
postAction({
|
||||
postLike({
|
||||
item_id: id,
|
||||
item_type: 'COMMENTS',
|
||||
action_type: 'LIKE'
|
||||
item_type: 'COMMENTS'
|
||||
}).then(({data}) => {
|
||||
this.setState({localPost: data.createAction.id});
|
||||
this.setState({localPost: data.createLike.like.id});
|
||||
});
|
||||
} else {
|
||||
this.setState((prev) => prev.localPost ? {...prev, localPost: null} : {...prev, localDelete: true});
|
||||
deleteAction(localPost || like.current.id);
|
||||
deleteAction(localPost || like.current_user.id);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -121,6 +121,7 @@ const ErrInvalidAssetURL = new APIError('asset_url is invalid', {
|
||||
// ErrNotAuthorized is an error that is returned in the event an operation is
|
||||
// deemed not authorized.
|
||||
const ErrNotAuthorized = new APIError('not authorized', {
|
||||
translation_key: 'NOT_AUTHORIZED',
|
||||
status: 401
|
||||
});
|
||||
|
||||
|
||||
@@ -5,6 +5,15 @@ const util = require('./util');
|
||||
const ActionsService = require('../../services/actions');
|
||||
const ActionModel = require('../../models/action');
|
||||
|
||||
/**
|
||||
* Gets actions based on their item id's.
|
||||
*/
|
||||
const genActionsByItemID = (_, item_ids) => {
|
||||
return ActionsService
|
||||
.findByItemIdArray(item_ids)
|
||||
.then(util.arrayJoinBy(item_ids, 'item_id'));
|
||||
};
|
||||
|
||||
/**
|
||||
* Looks up actions based on the requested id's all bounded by the user.
|
||||
* @param {Object} context the context of the request
|
||||
@@ -35,6 +44,7 @@ const getItemIdsByActionTypeAndItemType = (_, action_type, item_type) => {
|
||||
*/
|
||||
module.exports = (context) => ({
|
||||
Actions: {
|
||||
getByID: new DataLoader((ids) => genActionsByItemID(context, ids)),
|
||||
getSummariesByItemID: new DataLoader((ids) => genActionSummariessByItemID(context, ids)),
|
||||
getByTypes: ({action_type, item_type}) => getItemIdsByActionTypeAndItemType(context, action_type, item_type)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const ActionModel = require('../../models/action');
|
||||
const ActionsService = require('../../services/actions');
|
||||
const UsersService = require('../../services/users');
|
||||
const errors = require('../../errors');
|
||||
|
||||
/**
|
||||
* Creates an action on a item. If the item is a user flag, sets the user's status to
|
||||
@@ -11,11 +12,12 @@ const UsersService = require('../../services/users');
|
||||
* @param {String} action_type type of the action
|
||||
* @return {Promise} resolves to the action created
|
||||
*/
|
||||
const createAction = ({user = {}}, {item_id, item_type, action_type, metadata = {}}) => {
|
||||
const createAction = ({user = {}}, {item_id, item_type, action_type, group_id, metadata = {}}) => {
|
||||
return ActionsService.insertUserAction({
|
||||
item_id,
|
||||
item_type,
|
||||
user_id: user.id,
|
||||
group_id,
|
||||
action_type,
|
||||
metadata
|
||||
}).then((action) => {
|
||||
@@ -58,8 +60,8 @@ module.exports = (context) => {
|
||||
|
||||
return {
|
||||
Action: {
|
||||
create: () => {},
|
||||
delete: () => {}
|
||||
create: () => Promise.reject(errors.ErrNotAuthorized),
|
||||
delete: () => Promise.reject(errors.ErrNotAuthorized)
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -171,7 +171,7 @@ module.exports = (context) => {
|
||||
|
||||
return {
|
||||
Comment: {
|
||||
create: () => {}
|
||||
create: () => Promise.reject(errors.ErrNotAuthorized)
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
const Action = {
|
||||
__resolveType({action_type}) {
|
||||
switch (action_type) {
|
||||
case 'FLAG':
|
||||
return 'FlagAction';
|
||||
case 'LIKE':
|
||||
return 'LikeAction';
|
||||
}
|
||||
},
|
||||
|
||||
// This will load the user for the specific action. We'll limit this to the
|
||||
// admin users only or the current logged in user.
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
const ActionSummary = {};
|
||||
const ActionSummary = {
|
||||
__resolveType({action_type}) {
|
||||
switch (action_type) {
|
||||
case 'FLAG':
|
||||
return 'FlagActionSummary';
|
||||
case 'LIKE':
|
||||
return 'LikeActionSummary';
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = ActionSummary;
|
||||
|
||||
@@ -16,7 +16,16 @@ const Comment = {
|
||||
replyCount({id}, _, {loaders: {Comments}}) {
|
||||
return Comments.countByParentID.load(id);
|
||||
},
|
||||
actions({id}, _, {loaders: {Actions}}) {
|
||||
actions({id}, _, {user, loaders: {Actions}}) {
|
||||
|
||||
// Only return the actions if the user is not an admin.
|
||||
if (user && user.hasRoles('ADMIN')) {
|
||||
return Actions.getByID.load(id);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
action_summaries({id}, _, {loaders: {Actions}}) {
|
||||
return Actions.getSummariesByItemID.load(id);
|
||||
},
|
||||
asset({asset_id}, _, {loaders: {Assets}}) {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
const FlagAction = {
|
||||
|
||||
// Stored in the metadata, extract and return.
|
||||
reason({metadata: {reason}}) {
|
||||
return reason;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = FlagAction;
|
||||
@@ -0,0 +1,7 @@
|
||||
const FlagActionSummary = {
|
||||
reason({group_id}) {
|
||||
return group_id;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = FlagActionSummary;
|
||||
@@ -0,0 +1,3 @@
|
||||
const GenericUserError = {};
|
||||
|
||||
module.exports = GenericUserError;
|
||||
@@ -1,21 +1,33 @@
|
||||
const Action = require('./action');
|
||||
const ActionSummary = require('./action_summary');
|
||||
const Action = require('./action');
|
||||
const Asset = require('./asset');
|
||||
const Comment = require('./comment');
|
||||
const Date = require('./date');
|
||||
const FlagActionSummary = require('./flag_action_summary');
|
||||
const FlagAction = require('./flag_action');
|
||||
const GenericUserError = require('./generic_user_error');
|
||||
const LikeAction = require('./like_action');
|
||||
const RootMutation = require('./root_mutation');
|
||||
const RootQuery = require('./root_query');
|
||||
const Settings = require('./settings');
|
||||
const UserError = require('./user_error');
|
||||
const User = require('./user');
|
||||
const ValidationUserError = require('./validation_user_error');
|
||||
|
||||
module.exports = {
|
||||
Action,
|
||||
ActionSummary,
|
||||
Action,
|
||||
Asset,
|
||||
Comment,
|
||||
Date,
|
||||
FlagActionSummary,
|
||||
FlagAction,
|
||||
GenericUserError,
|
||||
LikeAction,
|
||||
RootMutation,
|
||||
RootQuery,
|
||||
Settings,
|
||||
User
|
||||
UserError,
|
||||
User,
|
||||
ValidationUserError,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
const LikeAction = {
|
||||
|
||||
};
|
||||
|
||||
module.exports = LikeAction;
|
||||
@@ -1,12 +1,31 @@
|
||||
/**
|
||||
* Wraps up a promise to return an object with the resolution of the promise
|
||||
* keyed at `key` or an error caught at `errors`.
|
||||
*/
|
||||
const wrapResponse = (key) => (promise) => {
|
||||
return promise.then((value) => {
|
||||
let res = {};
|
||||
if (key) {
|
||||
res[key] = value;
|
||||
}
|
||||
return res;
|
||||
}).catch((err) => ({
|
||||
errors: [err]
|
||||
}));
|
||||
};
|
||||
|
||||
const RootMutation = {
|
||||
createComment(_, {asset_id, parent_id, body}, {mutators: {Comment}}) {
|
||||
return Comment.create({asset_id, parent_id, body});
|
||||
return wrapResponse('comment')(Comment.create({asset_id, parent_id, body}));
|
||||
},
|
||||
createAction(_, {action}, {mutators: {Action}}) {
|
||||
return Action.create(action);
|
||||
createLike(_, {like: {item_id, item_type}}, {mutators: {Action}}) {
|
||||
return wrapResponse('like')(Action.create({item_id, item_type, action_type: 'LIKE'}));
|
||||
},
|
||||
createFlag(_, {flag: {item_id, item_type, reason, message}}, {mutators: {Action}}) {
|
||||
return wrapResponse('flag')(Action.create({item_id, item_type, action_type: 'FLAG', group_id: reason, metadata: {message}}));
|
||||
},
|
||||
deleteAction(_, {id}, {mutators: {Action}}) {
|
||||
return Action.delete({id});
|
||||
return wrapResponse(null)(Action.delete({id}));
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
const User = {
|
||||
actions({id}, _, {loaders: {Actions}}) {
|
||||
action_summaries({id}, _, {loaders: {Actions}}) {
|
||||
return Actions.getSummariesByItemID.load(id);
|
||||
},
|
||||
actions({id}, _, {user, loaders: {Actions}}) {
|
||||
|
||||
// Only return the actions if the user is not an admin.
|
||||
if (user && user.hasRoles('ADMIN')) {
|
||||
return Actions.getByID.load(id);
|
||||
}
|
||||
|
||||
},
|
||||
comments({id}, _, {loaders: {Comments}, user}) {
|
||||
|
||||
// If the user is not an admin, only return comment list for the owner of
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
const UserError = {
|
||||
__resolveType({field_name}) {
|
||||
if (field_name) {
|
||||
return 'ValidationUserError';
|
||||
}
|
||||
|
||||
return 'GenericUserError';
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = UserError;
|
||||
@@ -0,0 +1,3 @@
|
||||
const ValidationUserError = {};
|
||||
|
||||
module.exports = ValidationUserError;
|
||||
@@ -1,8 +1,15 @@
|
||||
const tools = require('graphql-tools');
|
||||
const maskErrors = require('graphql-errors').maskErrors;
|
||||
|
||||
const resolvers = require('./resolvers');
|
||||
const typeDefs = require('./typeDefs');
|
||||
|
||||
const schema = tools.makeExecutableSchema({typeDefs, resolvers});
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
|
||||
// Mask errors that are thrown if we are in a production environment.
|
||||
maskErrors(schema);
|
||||
}
|
||||
|
||||
module.exports = schema;
|
||||
|
||||
+334
-84
@@ -1,16 +1,82 @@
|
||||
# Establishes the ordering of the content by their created_at time stamp.
|
||||
enum SORT_ORDER {
|
||||
# newest to oldest order.
|
||||
REVERSE_CHRONOLOGICAL
|
||||
|
||||
# oldest to newer order.
|
||||
CHRONOLOGICAL
|
||||
}
|
||||
################################################################################
|
||||
## Custom Scalar Types
|
||||
################################################################################
|
||||
|
||||
# Date represented as an ISO8601 string.
|
||||
scalar Date
|
||||
|
||||
################################################################################
|
||||
## Users
|
||||
################################################################################
|
||||
|
||||
# Roles that a user can have, these can be combined.
|
||||
enum USER_ROLES {
|
||||
|
||||
# an administrator of the site
|
||||
ADMIN
|
||||
|
||||
# a moderator of the site
|
||||
MODERATOR
|
||||
}
|
||||
|
||||
# Any person who can author comments, create actions, and view comments on a
|
||||
# stream.
|
||||
type User {
|
||||
|
||||
# The ID of the User.
|
||||
id: ID!
|
||||
|
||||
# display name of a user.
|
||||
displayName: String!
|
||||
|
||||
# Action summaries against the user.
|
||||
action_summaries: [ActionSummary]
|
||||
|
||||
# Actions completed on the parent.
|
||||
actions: [Action]
|
||||
|
||||
# the current roles of the user.
|
||||
roles: [USER_ROLES]
|
||||
|
||||
# determines whether the user can edit their username
|
||||
canEditName: Boolean
|
||||
|
||||
# returns all comments based on a query.
|
||||
comments(query: CommentsQuery): [Comment]
|
||||
}
|
||||
|
||||
################################################################################
|
||||
## Comments
|
||||
################################################################################
|
||||
|
||||
# The statuses that a comment may have.
|
||||
enum COMMENT_STATUS {
|
||||
|
||||
# The comment has been accepted by a moderator.
|
||||
ACCEPTED
|
||||
|
||||
# The comment has been rejected by a moderator.
|
||||
REJECTED
|
||||
|
||||
# The comment was created while the asset's premoderation option was on, and
|
||||
# new comments that haven't been moderated yet are referred to as
|
||||
# "premoderated" or "premod" comments.
|
||||
PREMOD
|
||||
}
|
||||
|
||||
# The types of action there are as enum's.
|
||||
enum ACTION_TYPE {
|
||||
|
||||
# Represents a LikeAction.
|
||||
LIKE
|
||||
|
||||
# Represents a FlagAction.
|
||||
FLAG
|
||||
}
|
||||
|
||||
# CommentsQuery allows the ability to query comments by a specific methods.
|
||||
input CommentsQuery {
|
||||
|
||||
# current status of a comment.
|
||||
statuses: [COMMENT_STATUS]
|
||||
|
||||
@@ -34,42 +100,16 @@ input CommentsQuery {
|
||||
sort: SORT_ORDER = REVERSE_CHRONOLOGICAL
|
||||
}
|
||||
|
||||
enum USER_ROLES {
|
||||
# an administrator of the site
|
||||
ADMIN
|
||||
|
||||
# a moderator of the site
|
||||
MODERATOR
|
||||
}
|
||||
|
||||
# Any person who can author comments, create actions, and view comments on a
|
||||
# stream.
|
||||
type User {
|
||||
id: ID!
|
||||
|
||||
# display name of a user.
|
||||
displayName: String!
|
||||
|
||||
# actions against a specific user.
|
||||
actions: [ActionSummary]
|
||||
|
||||
# the current roles of the user.
|
||||
roles: [USER_ROLES]
|
||||
|
||||
# determines whether the user can edit their username
|
||||
canEditName: Boolean
|
||||
|
||||
# returns all comments based on a query.
|
||||
comments(query: CommentsQuery): [Comment]
|
||||
}
|
||||
|
||||
# Comment is the base representation of user interaction in Talk.
|
||||
type Comment {
|
||||
|
||||
# The ID of the comment.
|
||||
id: ID!
|
||||
|
||||
# the actual comment data.
|
||||
# The actual comment data.
|
||||
body: String!
|
||||
|
||||
# the user who authored the comment.
|
||||
# The user who authored the comment.
|
||||
user: User
|
||||
|
||||
# the recent replies made against this comment.
|
||||
@@ -78,68 +118,153 @@ type Comment {
|
||||
# the replies that were made to the comment.
|
||||
replies(sort: SORT_ORDER = CHRONOLOGICAL, limit: Int = 3): [Comment]
|
||||
|
||||
# the count of replies on a comment
|
||||
# The count of replies on a comment.
|
||||
replyCount: Int
|
||||
|
||||
# the actions made against a comment.
|
||||
actions: [ActionSummary]
|
||||
# Actions completed on the parent.
|
||||
actions: [Action]
|
||||
|
||||
# the asset that a comment was made on.
|
||||
# Action summaries against a comment.
|
||||
action_summaries: [ActionSummary]
|
||||
|
||||
# The asset that a comment was made on.
|
||||
asset: Asset
|
||||
|
||||
# the current status of a comment.
|
||||
# The current status of a comment.
|
||||
status: COMMENT_STATUS
|
||||
|
||||
# the time when the comment was created
|
||||
# The time when the comment was created
|
||||
created_at: Date!
|
||||
}
|
||||
|
||||
enum ITEM_TYPE {
|
||||
ASSETS
|
||||
COMMENTS
|
||||
USERS
|
||||
}
|
||||
################################################################################
|
||||
## Actions
|
||||
################################################################################
|
||||
|
||||
enum ACTION_TYPE {
|
||||
LIKE
|
||||
FLAG
|
||||
}
|
||||
# An action rendered against a parent enity item.
|
||||
interface Action {
|
||||
|
||||
type Action {
|
||||
# The ID of the action.
|
||||
id: ID!
|
||||
action_type: ACTION_TYPE!
|
||||
|
||||
item_id: ID!
|
||||
item_type: ITEM_TYPE!
|
||||
# The author of the action.
|
||||
user: User
|
||||
|
||||
user: User!
|
||||
# The time when the Action was updated.
|
||||
updated_at: Date
|
||||
|
||||
# The time when the Action was created.
|
||||
created_at: Date
|
||||
}
|
||||
|
||||
type ActionSummary {
|
||||
action_type: ACTION_TYPE!
|
||||
item_type: ITEM_TYPE!
|
||||
# A summary of actions based on the specific grouping of the group_id.
|
||||
interface ActionSummary {
|
||||
|
||||
# The count of actions with this group.
|
||||
count: Int
|
||||
|
||||
# The current user's action.
|
||||
current_user: Action
|
||||
}
|
||||
|
||||
# LikeAction is used by users who "like" a specific entity.
|
||||
type LikeAction implements Action {
|
||||
|
||||
# The ID of the action.
|
||||
id: ID!
|
||||
|
||||
# The author of the action.
|
||||
user: User
|
||||
|
||||
# The time when the Action was updated.
|
||||
updated_at: Date
|
||||
|
||||
# The time when the Action was created.
|
||||
created_at: Date
|
||||
}
|
||||
|
||||
# LikeActionSummary is counts the amount of "likes" that a specific entity has.
|
||||
type LikeActionSummary implements ActionSummary {
|
||||
|
||||
# The count of likes against the parent entity.
|
||||
count: Int!
|
||||
|
||||
current_user: LikeAction
|
||||
}
|
||||
|
||||
# A FLAG action that contains flag metadata.
|
||||
type FlagAction implements Action {
|
||||
|
||||
# The ID of the Flag Action.
|
||||
id: ID!
|
||||
|
||||
# The reason for which the Flag Action was created.
|
||||
reason: String
|
||||
|
||||
# An optional message sent with the flagging action by the user.
|
||||
message: String
|
||||
|
||||
# The user who created the action.
|
||||
user: User
|
||||
|
||||
# The time when the Flag Action was updated.
|
||||
updated_at: Date
|
||||
|
||||
# The time when the Flag Action was created.
|
||||
created_at: Date
|
||||
}
|
||||
|
||||
# Summary for Flag Action with a a unique reason.
|
||||
type FlagActionSummary implements ActionSummary {
|
||||
|
||||
# The total count of flags with this reason.
|
||||
count: Int!
|
||||
|
||||
# The reason for which the Flag Action was created.
|
||||
reason: String
|
||||
|
||||
# The flag by the current user against the parent entity with this reason.
|
||||
current_user: FlagAction
|
||||
}
|
||||
|
||||
################################################################################
|
||||
## Settings
|
||||
################################################################################
|
||||
|
||||
# The moderation mode of the site.
|
||||
enum MODERATION_MODE {
|
||||
|
||||
# Comments posted while in `PRE` mode will be labeled with a `PREMOD`
|
||||
# status and will require a moderator decision before being visible.
|
||||
PRE
|
||||
|
||||
# Comments posted while in `POST` will be visible immediately.
|
||||
POST
|
||||
}
|
||||
|
||||
# Site wide global settings.
|
||||
type Settings {
|
||||
|
||||
# Moderation mode for the site.
|
||||
moderation: MODERATION_MODE!
|
||||
|
||||
# Enables a requirement for email confirmation before a user can login.
|
||||
requireEmailConfirmation: Boolean
|
||||
|
||||
infoBoxEnable: Boolean
|
||||
infoBoxContent: String
|
||||
closeTimeout: Int
|
||||
closedMessage: String
|
||||
charCountEnable: Boolean
|
||||
charCount: Int
|
||||
requireEmailConfirmation: Boolean
|
||||
|
||||
}
|
||||
|
||||
################################################################################
|
||||
## Assets
|
||||
################################################################################
|
||||
|
||||
# Where comments are made on.
|
||||
type Asset {
|
||||
|
||||
# The current ID of the asset.
|
||||
@@ -171,53 +296,178 @@ type Asset {
|
||||
created_at: Date
|
||||
}
|
||||
|
||||
enum COMMENT_STATUS {
|
||||
ACCEPTED
|
||||
REJECTED
|
||||
PREMOD
|
||||
################################################################################
|
||||
## Errors
|
||||
################################################################################
|
||||
|
||||
# Any error rendered due to the user's input.
|
||||
interface UserError {
|
||||
|
||||
# Translation key relating to a translatable string containing details to be
|
||||
# displayed to the end user.
|
||||
translation_key: String!
|
||||
}
|
||||
|
||||
# A generic error not related to validation reasons.
|
||||
type GenericUserError implements UserError {
|
||||
|
||||
# Translation key relating to a translatable string containing details to be
|
||||
# displayed to the end user.
|
||||
translation_key: String!
|
||||
}
|
||||
|
||||
# A validation error that affects the input.
|
||||
type ValidationUserError implements UserError {
|
||||
|
||||
# Translation key relating to a translatable string containing details to be
|
||||
# displayed to the end user.
|
||||
translation_key: String!
|
||||
|
||||
# The field in question that caused the error.
|
||||
field_name: String!
|
||||
}
|
||||
|
||||
################################################################################
|
||||
## Queries
|
||||
################################################################################
|
||||
|
||||
# Establishes the ordering of the content by their created_at time stamp.
|
||||
enum SORT_ORDER {
|
||||
|
||||
# newest to oldest order.
|
||||
REVERSE_CHRONOLOGICAL
|
||||
|
||||
# oldest to newer order.
|
||||
CHRONOLOGICAL
|
||||
}
|
||||
|
||||
# All queries that can be executed.
|
||||
type RootQuery {
|
||||
|
||||
# retrieves site wide settings and defaults.
|
||||
# Site wide settings and defaults.
|
||||
settings: Settings
|
||||
|
||||
# retrieves all assets.
|
||||
# All assets.
|
||||
assets: [Asset]
|
||||
|
||||
# retrieves a specific asset.
|
||||
# Find or create an asset by url, or just find with the ID.
|
||||
asset(id: ID, url: String): Asset
|
||||
|
||||
# retrieves comments based on the input query.
|
||||
# Comments returned based on a query.
|
||||
comments(query: CommentsQuery!): [Comment]
|
||||
|
||||
# retrieves the current logged in user.
|
||||
# The currently logged in user based on the request.
|
||||
me: User
|
||||
}
|
||||
|
||||
input CreateActionInput {
|
||||
# the type of action.
|
||||
action_type: ACTION_TYPE!
|
||||
################################################################################
|
||||
## Mutations
|
||||
################################################################################
|
||||
|
||||
# the type of the item.
|
||||
item_type: ITEM_TYPE!
|
||||
# Response defines what can be expected from any response to a mutation action.
|
||||
interface Response {
|
||||
|
||||
# the id of the item that is related to the action.
|
||||
# An array of errors relating to the mutation that occured.
|
||||
errors: [UserError]
|
||||
}
|
||||
|
||||
# CreateCommentResponse is returned with the comment that was created and any
|
||||
# errors that may have occured in the attempt to create it.
|
||||
type CreateCommentResponse implements Response {
|
||||
|
||||
# The comment that was created.
|
||||
comment: Comment
|
||||
|
||||
# An array of errors relating to the mutation that occured.
|
||||
errors: [UserError]
|
||||
}
|
||||
|
||||
# Used to represent the item type for an action.
|
||||
enum ACTION_ITEM_TYPE {
|
||||
|
||||
# The action references a entity of type Asset.
|
||||
ASSETS
|
||||
|
||||
# The action references a entity of type Comment.
|
||||
COMMENTS
|
||||
|
||||
# The action references a entity of type User.
|
||||
USERS
|
||||
}
|
||||
|
||||
input CreateLikeInput {
|
||||
|
||||
# The item's id for which we are to create a like.
|
||||
item_id: ID!
|
||||
|
||||
# The type of the item for which we are to create the like.
|
||||
item_type: ACTION_ITEM_TYPE!
|
||||
}
|
||||
|
||||
type CreateLikeResponse implements Response {
|
||||
|
||||
# The like that was created.
|
||||
like: LikeAction
|
||||
|
||||
# An array of errors relating to the mutation that occured.
|
||||
errors: [UserError]
|
||||
}
|
||||
|
||||
input CreateFlagInput {
|
||||
|
||||
# The item's id for which we are to create a flag.
|
||||
item_id: ID!
|
||||
|
||||
# The type of the item for which we are to create the flag.
|
||||
item_type: ACTION_ITEM_TYPE!
|
||||
|
||||
# The reason for flagging the item.
|
||||
reason: String!
|
||||
|
||||
# An optional message sent with the flagging action by the user.
|
||||
message: String
|
||||
}
|
||||
|
||||
# CreateFlagResponse is the response returned with possibly some errors
|
||||
# relating to the creating the flag action attempt and possibly the flag that
|
||||
# was created.
|
||||
type CreateFlagResponse implements Response {
|
||||
|
||||
# The like that was created.
|
||||
flag: FlagAction
|
||||
|
||||
# An array of errors relating to the mutation that occured.
|
||||
errors: [UserError]
|
||||
}
|
||||
|
||||
# DeleteActionResponse is the response returned with possibly some errors
|
||||
# relating to the delete action attempt.
|
||||
type DeleteActionResponse implements Response {
|
||||
|
||||
# An array of errors relating to the mutation that occured.
|
||||
errors: [UserError]
|
||||
}
|
||||
|
||||
# All mutations for the application are defined on this object.
|
||||
type RootMutation {
|
||||
# creates a comment on the asset.
|
||||
createComment(asset_id: ID!, parent_id: ID, body: String!): Comment
|
||||
|
||||
# creates an action based on an input.
|
||||
createAction(action: CreateActionInput!): Action
|
||||
# Creates a comment on the asset.
|
||||
createComment(asset_id: ID!, parent_id: ID, body: String!): CreateCommentResponse
|
||||
|
||||
# delete an action based on the action id.
|
||||
deleteAction(id: ID!): Boolean
|
||||
# Creates a like on an entity.
|
||||
createLike(like: CreateLikeInput!): CreateLikeResponse
|
||||
|
||||
# Creates a flag on an entity.
|
||||
createFlag(flag: CreateFlagInput!): CreateFlagResponse
|
||||
|
||||
# Delete an action based on the action id.
|
||||
deleteAction(id: ID!): DeleteActionResponse
|
||||
}
|
||||
|
||||
################################################################################
|
||||
## Schema
|
||||
################################################################################
|
||||
|
||||
schema {
|
||||
query: RootQuery
|
||||
mutation: RootMutation
|
||||
|
||||
@@ -29,6 +29,12 @@ const ActionSchema = new Schema({
|
||||
},
|
||||
item_id: String,
|
||||
user_id: String,
|
||||
|
||||
// The element that summaries will additionally group on in addtion to their action_type, item_type, and
|
||||
// item_id.
|
||||
group_id: String,
|
||||
|
||||
// Additional metadata stored on the field.
|
||||
metadata: Schema.Types.Mixed
|
||||
}, {
|
||||
timestamps: {
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
"express": "^4.14.0",
|
||||
"express-session": "^1.14.2",
|
||||
"graphql": "^0.8.2",
|
||||
"graphql-errors": "^2.1.0",
|
||||
"graphql-server-express": "^0.5.0",
|
||||
"graphql-tag": "^1.2.3",
|
||||
"graphql-tools": "^0.9.0",
|
||||
@@ -120,6 +121,7 @@
|
||||
"eslint-plugin-standard": "^2.0.1",
|
||||
"exports-loader": "^0.6.3",
|
||||
"fetch-mock": "^5.5.0",
|
||||
"graphql-docs": "^0.2.0",
|
||||
"hammerjs": "^2.0.8",
|
||||
"ignore-styles": "^5.0.1",
|
||||
"immutable": "^3.8.1",
|
||||
|
||||
@@ -127,21 +127,4 @@ router.put('/:comment_id/status', authorization.needed('ADMIN'), (req, res, next
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/:comment_id/actions', (req, res, next) => {
|
||||
|
||||
const {
|
||||
action_type,
|
||||
metadata
|
||||
} = req.body;
|
||||
|
||||
CommentsService
|
||||
.addAction(req.params.comment_id, req.user.id, action_type, metadata)
|
||||
.then((action) => {
|
||||
res.status(201).json(action);
|
||||
})
|
||||
.catch((err) => {
|
||||
next(err);
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
+58
-55
@@ -26,7 +26,8 @@ module.exports = class ActionsService {
|
||||
action_type: action.action_type,
|
||||
item_type: action.item_type,
|
||||
item_id: action.item_id,
|
||||
user_id: action.user_id
|
||||
user_id: action.user_id,
|
||||
group_id: action.group_id
|
||||
};
|
||||
|
||||
// Create/Update the action.
|
||||
@@ -86,68 +87,70 @@ module.exports = class ActionsService {
|
||||
* @param {String} ids array of user identifiers (uuid)
|
||||
*/
|
||||
static getActionSummaries(item_ids, current_user_id = '') {
|
||||
return ActionModel.aggregate([
|
||||
{
|
||||
|
||||
// only grab items that match the specified item id's
|
||||
$match: {
|
||||
item_id: {
|
||||
$in: item_ids
|
||||
}
|
||||
}
|
||||
// only grab items that match the specified item id's
|
||||
let $match = {
|
||||
item_id: {
|
||||
$in: item_ids
|
||||
}
|
||||
};
|
||||
|
||||
let $group = {
|
||||
|
||||
// group unique documents by these properties, we are leveraging the
|
||||
// fact that each uuid is completly unique.
|
||||
_id: {
|
||||
item_id: '$item_id',
|
||||
action_type: '$action_type',
|
||||
group_id: '$group_id'
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
|
||||
// group unique documents by these properties, we are leveraging the
|
||||
// fact that each uuid is completly unique.
|
||||
_id: {
|
||||
item_id: '$item_id',
|
||||
action_type: '$action_type'
|
||||
},
|
||||
|
||||
// and sum up all actions matching the above grouping criteria
|
||||
count: {
|
||||
$sum: 1
|
||||
},
|
||||
|
||||
// we are leveraging the fact that each uuid is completly unique and
|
||||
// just grabbing the last instance of the item type here.
|
||||
item_type: {
|
||||
$last: '$item_type'
|
||||
},
|
||||
|
||||
current_user: {
|
||||
$max: {
|
||||
$cond: {
|
||||
if: {
|
||||
$eq: ['$user_id', current_user_id],
|
||||
},
|
||||
then: '$$CURRENT',
|
||||
else: null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// and sum up all actions matching the above grouping criteria
|
||||
count: {
|
||||
$sum: 1
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
|
||||
// suppress the _id field
|
||||
_id: false,
|
||||
// we are leveraging the fact that each uuid is completly unique and
|
||||
// just grabbing the last instance of the item type here.
|
||||
item_type: {
|
||||
$first: '$item_type'
|
||||
},
|
||||
|
||||
// map the fields from the _id grouping down a level
|
||||
item_id: '$_id.item_id',
|
||||
action_type: '$_id.action_type',
|
||||
|
||||
// map the field directly
|
||||
count: '$count',
|
||||
item_type: '$item_type',
|
||||
|
||||
// set the current user to false here
|
||||
current_user: '$current_user'
|
||||
current_user: {
|
||||
$max: {
|
||||
$cond: {
|
||||
if: {
|
||||
$eq: ['$user_id', current_user_id],
|
||||
},
|
||||
then: '$$CURRENT',
|
||||
else: null
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let $project = {
|
||||
|
||||
// suppress the _id field
|
||||
_id: false,
|
||||
|
||||
// map the fields from the _id grouping down a level
|
||||
item_id: '$_id.item_id',
|
||||
action_type: '$_id.action_type',
|
||||
group_id: '$_id.group_id',
|
||||
|
||||
// map the field directly
|
||||
count: '$count',
|
||||
item_type: '$item_type',
|
||||
|
||||
// set the current user to false here
|
||||
current_user: '$current_user'
|
||||
};
|
||||
|
||||
return ActionModel.aggregate([
|
||||
{$match},
|
||||
{$group},
|
||||
{$project}
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -267,79 +267,3 @@ describe('/api/v1/comments/:comment_id', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('/api/v1/comments/:comment_id/actions', () => {
|
||||
|
||||
const comments = [{
|
||||
id: 'abc',
|
||||
body: 'comment 10',
|
||||
asset_id: 'asset',
|
||||
author_id: '123',
|
||||
status_history: []
|
||||
}, {
|
||||
id: 'def',
|
||||
body: 'comment 20',
|
||||
asset_id: 'asset',
|
||||
author_id: '456',
|
||||
status: 'REJECTED',
|
||||
status_history: [{
|
||||
type: 'REJECTED'
|
||||
}]
|
||||
}, {
|
||||
id: 'hij',
|
||||
body: 'comment 30',
|
||||
asset_id: '456',
|
||||
status: 'ACCEPTED',
|
||||
status_history: [{
|
||||
type: 'ACCEPTED'
|
||||
}]
|
||||
}];
|
||||
|
||||
const users = [{
|
||||
displayName: 'Ana',
|
||||
email: 'ana@gmail.com',
|
||||
password: '123456789'
|
||||
}, {
|
||||
displayName: 'Maria',
|
||||
email: 'maria@gmail.com',
|
||||
password: '123456789'
|
||||
}];
|
||||
|
||||
const actions = [{
|
||||
action_type: 'FLAG',
|
||||
item_type: 'COMMENTS',
|
||||
item_id: 'abc'
|
||||
}, {
|
||||
action_type: 'LIKE',
|
||||
item_type: 'COMMENTS',
|
||||
item_id: 'hij'
|
||||
}];
|
||||
|
||||
beforeEach(() => {
|
||||
return SettingsService.init(settings).then(() => {
|
||||
return Promise.all([
|
||||
CommentModel.create(comments),
|
||||
UsersService.createLocalUsers(users),
|
||||
ActionModel.create(actions)
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#post', () => {
|
||||
it('it should create an action', () => {
|
||||
return chai.request(app)
|
||||
.post('/api/v1/comments/abc/actions')
|
||||
.set(passport.inject({id: '456', roles: ['ADMIN']}))
|
||||
.send({'action_type': 'flag', 'metadata': {'reason': 'Comment is too awesome.'}})
|
||||
.then((res) => {
|
||||
expect(res).to.have.status(201);
|
||||
expect(res).to.have.body;
|
||||
|
||||
expect(res.body).to.have.property('action_type', 'flag');
|
||||
expect(res.body).to.have.property('metadata');
|
||||
expect(res.body.metadata).to.deep.equal({'reason': 'Comment is too awesome.'});
|
||||
expect(res.body).to.have.property('item_id', 'abc');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Talk: GraphQL Docs</title>
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500" rel="stylesheet">
|
||||
<style media="screen">
|
||||
body, #root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
background-color: #FAFAFA;
|
||||
font-family: 'Roboto', sans-serif;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
margin: 0 auto;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
a {
|
||||
border-bottom: 2px dotted #f67150;
|
||||
color: #333 !important;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script src="/client/coral-docs/bundle.js" charset="utf-8"></script>
|
||||
</body>
|
||||
</html>
|
||||
+2
-1
@@ -7,7 +7,8 @@ const webpack = require('webpack');
|
||||
// Edit the build targets and embeds below.
|
||||
|
||||
const buildTargets = [
|
||||
'coral-admin'
|
||||
'coral-admin',
|
||||
'coral-docs'
|
||||
];
|
||||
|
||||
const buildEmbeds = [
|
||||
|
||||
@@ -1011,7 +1011,7 @@ babel-register@^6.22.0:
|
||||
mkdirp "^0.5.1"
|
||||
source-map-support "^0.4.2"
|
||||
|
||||
babel-runtime@^6.11.6, babel-runtime@^6.18.0, babel-runtime@^6.2.0, babel-runtime@^6.22.0:
|
||||
babel-runtime@^6.11.6, babel-runtime@^6.18.0, babel-runtime@^6.2.0, babel-runtime@^6.22.0, babel-runtime@^6.6.1:
|
||||
version "6.22.0"
|
||||
resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.22.0.tgz#1cf8b4ac67c77a4ddb0db2ae1f74de52ac4ca611"
|
||||
dependencies:
|
||||
@@ -3307,6 +3307,21 @@ graphql-anywhere@^2.0.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/graphql-anywhere/-/graphql-anywhere-2.1.0.tgz#888c0a1718db3ff866b313070747777380560f69"
|
||||
|
||||
graphql-docs@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/graphql-docs/-/graphql-docs-0.2.0.tgz#cf803f9c9d354fa03e89073d74e419261a5bfa74"
|
||||
dependencies:
|
||||
marked "^0.3.5"
|
||||
request "^2.74.0"
|
||||
yargs "^5.0.0"
|
||||
|
||||
graphql-errors@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/graphql-errors/-/graphql-errors-2.1.0.tgz#831c8c491b354859ee7a0c07bff101af64731195"
|
||||
dependencies:
|
||||
babel-runtime "^6.6.1"
|
||||
uuid "^2.0.2"
|
||||
|
||||
graphql-server-core@^0.5.2:
|
||||
version "0.5.2"
|
||||
resolved "https://registry.yarnpkg.com/graphql-server-core/-/graphql-server-core-0.5.2.tgz#7e23fc516cb754e42c16f92928b595c354d6c8a7"
|
||||
@@ -4480,7 +4495,7 @@ lodash.assign@^3.0.0:
|
||||
lodash._createassigner "^3.0.0"
|
||||
lodash.keys "^3.0.0"
|
||||
|
||||
lodash.assign@^4.0.3, lodash.assign@^4.0.6:
|
||||
lodash.assign@^4.0.3, lodash.assign@^4.0.6, lodash.assign@^4.1.0, lodash.assign@^4.2.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7"
|
||||
|
||||
@@ -4696,6 +4711,10 @@ map-stream@~0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.1.0.tgz#e56aa94c4c8055a16404a0674b78f215f7c8e194"
|
||||
|
||||
marked@^0.3.5:
|
||||
version "0.3.6"
|
||||
resolved "https://registry.yarnpkg.com/marked/-/marked-0.3.6.tgz#b2c6c618fccece4ef86c4fc6cb8a7cbf5aeda8d7"
|
||||
|
||||
material-design-lite@^1.2.1:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/material-design-lite/-/material-design-lite-1.3.0.tgz#d004ce3fee99a1eeb74a78b8a325134a5f1171d3"
|
||||
@@ -6707,7 +6726,7 @@ repeating@^2.0.0:
|
||||
dependencies:
|
||||
is-finite "^1.0.0"
|
||||
|
||||
request@2.79.0, request@^2.55.0, request@^2.79.0:
|
||||
request@2.79.0, request@^2.55.0, request@^2.74.0, request@^2.79.0:
|
||||
version "2.79.0"
|
||||
resolved "https://registry.yarnpkg.com/request/-/request-2.79.0.tgz#4dfe5bf6be8b8cdc37fcf93e04b65577722710de"
|
||||
dependencies:
|
||||
@@ -7286,7 +7305,7 @@ supports-color@1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-1.2.0.tgz#ff1ed1e61169d06b3cf2d588e188b18d8847e17e"
|
||||
|
||||
supports-color@3.1.2:
|
||||
supports-color@3.1.2, supports-color@^3.1.0:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.1.2.tgz#72a262894d9d408b956ca05ff37b2ed8a6e2a2d5"
|
||||
dependencies:
|
||||
@@ -7300,7 +7319,7 @@ supports-color@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
|
||||
|
||||
supports-color@^3.1.0, supports-color@^3.1.2, supports-color@^3.2.3:
|
||||
supports-color@^3.1.2, supports-color@^3.2.3:
|
||||
version "3.2.3"
|
||||
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6"
|
||||
dependencies:
|
||||
@@ -7658,7 +7677,7 @@ utils-merge@1.0.0, utils-merge@1.x.x:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.0.tgz#0294fb922bb9375153541c4f7096231f287c8af8"
|
||||
|
||||
uuid@^2.0.1, uuid@^2.0.3:
|
||||
uuid@^2.0.1, uuid@^2.0.2, uuid@^2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a"
|
||||
|
||||
@@ -7908,6 +7927,13 @@ yargs-parser@^2.4.1:
|
||||
camelcase "^3.0.0"
|
||||
lodash.assign "^4.0.6"
|
||||
|
||||
yargs-parser@^3.2.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-3.2.0.tgz#5081355d19d9d0c8c5d81ada908cb4e6d186664f"
|
||||
dependencies:
|
||||
camelcase "^3.0.0"
|
||||
lodash.assign "^4.1.0"
|
||||
|
||||
yargs-parser@^4.2.0:
|
||||
version "4.2.1"
|
||||
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-4.2.1.tgz#29cceac0dc4f03c6c87b4a9f217dd18c9f74871c"
|
||||
@@ -7933,6 +7959,25 @@ yargs@^4.0.0:
|
||||
y18n "^3.2.1"
|
||||
yargs-parser "^2.4.1"
|
||||
|
||||
yargs@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/yargs/-/yargs-5.0.0.tgz#3355144977d05757dbb86d6e38ec056123b3a66e"
|
||||
dependencies:
|
||||
cliui "^3.2.0"
|
||||
decamelize "^1.1.1"
|
||||
get-caller-file "^1.0.1"
|
||||
lodash.assign "^4.2.0"
|
||||
os-locale "^1.4.0"
|
||||
read-pkg-up "^1.0.1"
|
||||
require-directory "^2.1.1"
|
||||
require-main-filename "^1.0.1"
|
||||
set-blocking "^2.0.0"
|
||||
string-width "^1.0.2"
|
||||
which-module "^1.0.0"
|
||||
window-size "^0.2.0"
|
||||
y18n "^3.2.1"
|
||||
yargs-parser "^3.2.0"
|
||||
|
||||
yargs@^6.0.0:
|
||||
version "6.6.0"
|
||||
resolved "https://registry.yarnpkg.com/yargs/-/yargs-6.6.0.tgz#782ec21ef403345f830a808ca3d513af56065208"
|
||||
|
||||
Reference in New Issue
Block a user