}
diff --git a/client/coral-plugin-likes/LikeButton.js b/client/coral-plugin-likes/LikeButton.js
index cc2c6d83e..d7a981e95 100644
--- a/client/coral-plugin-likes/LikeButton.js
+++ b/client/coral-plugin-likes/LikeButton.js
@@ -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);
}
};
diff --git a/errors.js b/errors.js
index 7ee932c96..7debe6a43 100644
--- a/errors.js
+++ b/errors.js
@@ -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
});
diff --git a/graph/loaders/actions.js b/graph/loaders/actions.js
index c41f17219..2dd6472a0 100644
--- a/graph/loaders/actions.js
+++ b/graph/loaders/actions.js
@@ -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)
}
diff --git a/graph/mutators/action.js b/graph/mutators/action.js
index b5264fd49..3499719e1 100644
--- a/graph/mutators/action.js
+++ b/graph/mutators/action.js
@@ -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)
}
};
};
diff --git a/graph/mutators/comment.js b/graph/mutators/comment.js
index 3c35b0e60..2f18ae135 100644
--- a/graph/mutators/comment.js
+++ b/graph/mutators/comment.js
@@ -171,7 +171,7 @@ module.exports = (context) => {
return {
Comment: {
- create: () => {}
+ create: () => Promise.reject(errors.ErrNotAuthorized)
}
};
};
diff --git a/graph/resolvers/action.js b/graph/resolvers/action.js
index f8aa60245..0962f59af 100644
--- a/graph/resolvers/action.js
+++ b/graph/resolvers/action.js
@@ -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.
diff --git a/graph/resolvers/action_summary.js b/graph/resolvers/action_summary.js
index 48eff50c7..93848d285 100644
--- a/graph/resolvers/action_summary.js
+++ b/graph/resolvers/action_summary.js
@@ -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;
diff --git a/graph/resolvers/comment.js b/graph/resolvers/comment.js
index 110067174..ef77ff6b5 100644
--- a/graph/resolvers/comment.js
+++ b/graph/resolvers/comment.js
@@ -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}}) {
diff --git a/graph/resolvers/flag_action.js b/graph/resolvers/flag_action.js
new file mode 100644
index 000000000..44cf7a410
--- /dev/null
+++ b/graph/resolvers/flag_action.js
@@ -0,0 +1,9 @@
+const FlagAction = {
+
+ // Stored in the metadata, extract and return.
+ reason({metadata: {reason}}) {
+ return reason;
+ }
+};
+
+module.exports = FlagAction;
diff --git a/graph/resolvers/flag_action_summary.js b/graph/resolvers/flag_action_summary.js
new file mode 100644
index 000000000..58dda689e
--- /dev/null
+++ b/graph/resolvers/flag_action_summary.js
@@ -0,0 +1,7 @@
+const FlagActionSummary = {
+ reason({group_id}) {
+ return group_id;
+ }
+};
+
+module.exports = FlagActionSummary;
diff --git a/graph/resolvers/generic_user_error.js b/graph/resolvers/generic_user_error.js
new file mode 100644
index 000000000..45b656f5b
--- /dev/null
+++ b/graph/resolvers/generic_user_error.js
@@ -0,0 +1,3 @@
+const GenericUserError = {};
+
+module.exports = GenericUserError;
diff --git a/graph/resolvers/index.js b/graph/resolvers/index.js
index f25664f34..84dd10fdc 100644
--- a/graph/resolvers/index.js
+++ b/graph/resolvers/index.js
@@ -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,
};
diff --git a/graph/resolvers/like_action.js b/graph/resolvers/like_action.js
new file mode 100644
index 000000000..12d10f81e
--- /dev/null
+++ b/graph/resolvers/like_action.js
@@ -0,0 +1,5 @@
+const LikeAction = {
+
+};
+
+module.exports = LikeAction;
diff --git a/graph/resolvers/root_mutation.js b/graph/resolvers/root_mutation.js
index ef46b839a..8369b7281 100644
--- a/graph/resolvers/root_mutation.js
+++ b/graph/resolvers/root_mutation.js
@@ -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}));
},
};
diff --git a/graph/resolvers/user.js b/graph/resolvers/user.js
index c75cdbf54..13efe29e9 100644
--- a/graph/resolvers/user.js
+++ b/graph/resolvers/user.js
@@ -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
diff --git a/graph/resolvers/user_error.js b/graph/resolvers/user_error.js
new file mode 100644
index 000000000..3716fa126
--- /dev/null
+++ b/graph/resolvers/user_error.js
@@ -0,0 +1,11 @@
+const UserError = {
+ __resolveType({field_name}) {
+ if (field_name) {
+ return 'ValidationUserError';
+ }
+
+ return 'GenericUserError';
+ }
+};
+
+module.exports = UserError;
diff --git a/graph/resolvers/validation_user_error.js b/graph/resolvers/validation_user_error.js
new file mode 100644
index 000000000..211a0e505
--- /dev/null
+++ b/graph/resolvers/validation_user_error.js
@@ -0,0 +1,3 @@
+const ValidationUserError = {};
+
+module.exports = ValidationUserError;
diff --git a/graph/schema.js b/graph/schema.js
index b4b42b809..2fca1a87f 100644
--- a/graph/schema.js
+++ b/graph/schema.js
@@ -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;
diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql
index 8369b93c6..e231bc0c3 100644
--- a/graph/typeDefs.graphql
+++ b/graph/typeDefs.graphql
@@ -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
diff --git a/models/action.js b/models/action.js
index ed871f56b..36d83574e 100644
--- a/models/action.js
+++ b/models/action.js
@@ -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: {
diff --git a/package.json b/package.json
index 548119cfc..a40e1ad97 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/routes/api/comments/index.js b/routes/api/comments/index.js
index 19346274d..96f3911d4 100644
--- a/routes/api/comments/index.js
+++ b/routes/api/comments/index.js
@@ -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;
diff --git a/services/actions.js b/services/actions.js
index c336dd974..0bcb81af0 100644
--- a/services/actions.js
+++ b/services/actions.js
@@ -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}
]);
}
diff --git a/test/routes/api/comments/index.js b/test/routes/api/comments/index.js
index c305f9f6b..0ee19fc5e 100644
--- a/test/routes/api/comments/index.js
+++ b/test/routes/api/comments/index.js
@@ -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');
- });
- });
- });
-});
diff --git a/views/admin/docs.ejs b/views/admin/docs.ejs
new file mode 100644
index 000000000..de288e055
--- /dev/null
+++ b/views/admin/docs.ejs
@@ -0,0 +1,33 @@
+
+
+
+
+ Talk: GraphQL Docs
+
+
+
+
+
+
+
+
diff --git a/webpack.config.js b/webpack.config.js
index dde801744..5a7bb297b 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -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 = [
diff --git a/yarn.lock b/yarn.lock
index e76614702..f1c047be2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -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"