Added activities

- added Activity model
- added Activity association to Post
- added getActivities field to queries
- added createActivity to mutations
pull/5/head
Trivernis 5 years ago
parent 124fde08d5
commit 4f6caf461c

@ -19,6 +19,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- ability to upload file at `/upload` with the name profilePicture - ability to upload file at `/upload` with the name profilePicture
- publicPath to config file to configure the directory for public files - publicPath to config file to configure the directory for public files
- profilePicture property to User model which is an url to the users profile picture - profilePicture property to User model which is an url to the users profile picture
- activities to posts
- getActivities field to receive all activities
- activities table
### Removed ### Removed

@ -5,7 +5,6 @@ import {Op} from "sequelize";
import dataaccess from "../lib/dataAccess"; import dataaccess from "../lib/dataAccess";
import {NotLoggedInGqlError, PostNotFoundGqlError} from "../lib/errors/graphqlErrors"; import {NotLoggedInGqlError, PostNotFoundGqlError} from "../lib/errors/graphqlErrors";
import {InvalidLoginError} from "../lib/errors/InvalidLoginError"; import {InvalidLoginError} from "../lib/errors/InvalidLoginError";
import {UserNotFoundError} from "../lib/errors/UserNotFoundError";
import globals from "../lib/globals"; import globals from "../lib/globals";
import {InternalEvents} from "../lib/InternalEvents"; import {InternalEvents} from "../lib/InternalEvents";
import * as models from "../lib/models"; import * as models from "../lib/models";
@ -227,15 +226,20 @@ export function resolver(req: any, res: any): any {
return new GraphQLError("No postId or type given."); return new GraphQLError("No postId or type given.");
} }
}, },
async createPost({content}: { content: string }) { async createPost({content, activityId}: { content: string, activityId: number }) {
if (content) { if (content) {
if (req.session.userId) { if (req.session.userId) {
if (content.length > 2048) { if (content.length > 2048) {
return new GraphQLError("Content too long."); return new GraphQLError("Content too long.");
} else { } else {
const post = await dataaccess.createPost(content, req.session.userId); try {
const post = await dataaccess.createPost(content, req.session.userId, activityId);
globals.internalEmitter.emit(InternalEvents.GQLPOSTCREATE, post); globals.internalEmitter.emit(InternalEvents.GQLPOSTCREATE, post);
return post; return post;
} catch (err) {
res.status(status.BAD_REQUEST);
return err.graphqlError ?? new GraphQLError(err.message);
}
} }
} else { } else {
res.status(status.UNAUTHORIZED); res.status(status.UNAUTHORIZED);
@ -482,5 +486,28 @@ export function resolver(req: any, res: any): any {
return new NotLoggedInGqlError(); return new NotLoggedInGqlError();
} }
}, },
async getActivities() {
return models.Activity.findAll();
},
async createActivity({name, description, points}:
{name: string, description: string, points: number}) {
if (req.session.userId) {
const user = await models.User.findByPk(req.session.userId);
if (user.isAdmin) {
const nameExists = await models.Activity.findOne({where: {name}});
if (!nameExists) {
return models.Activity.create({name, description, points});
} else {
return new GraphQLError(`An activity with the name '${name}'`);
}
} else {
res.status(status.FORBIDDEN);
return new GraphQLError("You are not an admin.");
}
} else {
res.status(status.UNAUTHORIZED);
return new NotLoggedInGqlError();
}
},
}; };
} }

@ -26,6 +26,9 @@ type Query {
"returns the post filtered by the sort type with pagination." "returns the post filtered by the sort type with pagination."
getPosts(first: Int=20, offset: Int=0, sort: SortType = NEW): [Post] getPosts(first: Int=20, offset: Int=0, sort: SortType = NEW): [Post]
"returns all activities"
getActivities: [Activity]
"Returns an access token for the user that can be used in requests. To user the token in requests, it has to be set in the HTTP header 'Authorization' with the format Bearer <token>." "Returns an access token for the user that can be used in requests. To user the token in requests, it has to be set in the HTTP header 'Authorization' with the format Bearer <token>."
getToken(email: String!, passwordHash: String!): Token! getToken(email: String!, passwordHash: String!): Token!
} }
@ -67,8 +70,8 @@ type Mutation {
"send a message in a Chatroom" "send a message in a Chatroom"
sendMessage(chatId: ID!, content: String!): ChatMessage sendMessage(chatId: ID!, content: String!): ChatMessage
"create the post" "create a post that can belong to an activity"
createPost(content: String!): Post! createPost(content: String!, activityId: ID): Post!
"delete the post for a given post id" "delete the post for a given post id"
deletePost(postId: ID!): Boolean! deletePost(postId: ID!): Boolean!
@ -99,6 +102,9 @@ type Mutation {
"Leaves a event." "Leaves a event."
leaveEvent(eventId: ID!): Event leaveEvent(eventId: ID!): Event
"Creates an activity. Can only be used by admins."
createActivity(name: String!, description: String!, points: Int!): Activity
} }
interface UserData { interface UserData {
@ -114,9 +120,6 @@ interface UserData {
"Id of the User" "Id of the User"
id: ID! id: ID!
"DEPRECATED! the total number of posts the user posted"
numberOfPosts: Int!
"the number of posts the user has created" "the number of posts the user has created"
postCount: Int! postCount: Int!
@ -290,6 +293,9 @@ type Post {
"if the post can be deleted by the specified user" "if the post can be deleted by the specified user"
deletable(userId: ID!): Boolean deletable(userId: ID!): Boolean
"the activity that belongs to the post"
activity: Activity
} }
"represents a request of any type" "represents a request of any type"
@ -407,6 +413,22 @@ type SearchResult {
events: [Event!]! events: [Event!]!
} }
"An activity that grants points"
type Activity {
"the id of the activity"
id: ID!
"the name of the activity"
name: String!
"the description of the activity"
description: String!
"the number of points the activity grants"
points: Int!
}
"represents the type of vote performed on a post" "represents the type of vote performed on a post"
enum VoteType { enum VoteType {
UPVOTE UPVOTE

@ -1,9 +1,12 @@
import * as crypto from "crypto"; import * as crypto from "crypto";
import {GraphQLError} from "graphql";
import * as sqz from "sequelize"; import * as sqz from "sequelize";
import {Sequelize} from "sequelize-typescript"; import {Sequelize} from "sequelize-typescript";
import {ActivityNotFoundError} from "./errors/ActivityNotFoundError";
import {ChatNotFoundError} from "./errors/ChatNotFoundError"; import {ChatNotFoundError} from "./errors/ChatNotFoundError";
import {DuplicatedRequestError} from "./errors/DuplicatedRequestError"; import {DuplicatedRequestError} from "./errors/DuplicatedRequestError";
import {EmailAlreadyRegisteredError} from "./errors/EmailAlreadyRegisteredError"; import {EmailAlreadyRegisteredError} from "./errors/EmailAlreadyRegisteredError";
import {PostNotFoundGqlError} from "./errors/graphqlErrors";
import {GroupAlreadyExistsError} from "./errors/GroupAlreadyExistsError"; import {GroupAlreadyExistsError} from "./errors/GroupAlreadyExistsError";
import {GroupNotFoundError} from "./errors/GroupNotFoundError"; import {GroupNotFoundError} from "./errors/GroupNotFoundError";
import {InvalidLoginError} from "./errors/InvalidLoginError"; import {InvalidLoginError} from "./errors/InvalidLoginError";
@ -11,6 +14,7 @@ import {NoActionSpecifiedError} from "./errors/NoActionSpecifiedError";
import {UserNotFoundError} from "./errors/UserNotFoundError"; import {UserNotFoundError} from "./errors/UserNotFoundError";
import globals from "./globals"; import globals from "./globals";
import {InternalEvents} from "./InternalEvents"; import {InternalEvents} from "./InternalEvents";
import {Activity} from "./models";
import * as models from "./models"; import * as models from "./models";
/** /**
@ -54,6 +58,7 @@ namespace dataaccess {
models.GroupMember, models.GroupMember,
models.EventParticipant, models.EventParticipant,
models.Event, models.Event,
models.Activity,
]); ]);
} catch (err) { } catch (err) {
globals.logger.error(err.message); globals.logger.error(err.message);
@ -165,21 +170,38 @@ namespace dataaccess {
* Creates a post * Creates a post
* @param content * @param content
* @param authorId * @param authorId
* @param type * @param activityId
*/ */
export async function createPost(content: string, authorId: number, type?: string): Promise<models.Post> { export async function createPost(content: string, authorId: number, activityId?: number): Promise<models.Post> {
type = type || "MISC"; const activity = await models.Activity.findByPk(activityId);
const post = await models.Post.create({content, authorId}); if (!activityId || activity) {
const post = await models.Post.create({content, authorId, activityId});
globals.internalEmitter.emit(InternalEvents.POSTCREATE, post); globals.internalEmitter.emit(InternalEvents.POSTCREATE, post);
if (activity) {
const user = await models.User.findByPk(authorId);
user.rankpoints += activity.points;
await user.save();
}
return post; return post;
} else {
throw new ActivityNotFoundError(activityId);
}
} }
/** /**
* Deletes a post * Deletes a post
* @param postId * @param postId
*/ */
export async function deletePost(postId: number): Promise<boolean> { export async function deletePost(postId: number): Promise<boolean|GraphQLError> {
await (await models.Post.findByPk(postId)).destroy(); try {
const post = await models.Post.findByPk(postId, {include: [{model: Activity}, {association: "rAuthor"}]});
const activity = await post.activity();
const author = await post.author();
author.rankpoints -= activity.points;
await author.save();
} catch (err) {
return new PostNotFoundGqlError(postId);
}
return true; return true;
} }

@ -0,0 +1,7 @@
import {BaseError} from "./BaseError";
export class ActivityNotFoundError extends BaseError {
constructor(id: number) {
super(`The activity with the id ${id} could not be found.`);
}
}

@ -0,0 +1,18 @@
import * as sqz from "sequelize";
import {Column, ForeignKey, Model, NotNull, Table, Unique} from "sequelize-typescript";
@Table({underscored: true})
export class Activity extends Model {
@Unique
@NotNull
@Column({type: sqz.STRING(128), allowNull: false, unique: true})
public name: string;
@NotNull
@Column({type: sqz.TEXT, allowNull: false})
public description: string;
@Column
public points: number;
}

@ -1,6 +1,7 @@
import * as sqz from "sequelize"; import * as sqz from "sequelize";
import {BelongsTo, BelongsToMany, Column, CreatedAt, ForeignKey, Model, NotNull, Table} from "sequelize-typescript"; import {BelongsTo, BelongsToMany, Column, CreatedAt, ForeignKey, Model, NotNull, Table} from "sequelize-typescript";
import markdown from "../markdown"; import markdown from "../markdown";
import {Activity} from "./Activity";
import {PostVote, VoteType} from "./PostVote"; import {PostVote, VoteType} from "./PostVote";
import {User} from "./User"; import {User} from "./User";
@ -15,9 +16,16 @@ export class Post extends Model<Post> {
@Column({allowNull: false}) @Column({allowNull: false})
public authorId: number; public authorId: number;
@ForeignKey(() => Activity)
@Column({allowNull: true})
public activityId: number;
@BelongsTo(() => User, "authorId") @BelongsTo(() => User, "authorId")
public rAuthor: User; public rAuthor: User;
@BelongsTo(() => Activity, "activityId")
public rActivity?: Activity;
@BelongsToMany(() => User, () => PostVote) @BelongsToMany(() => User, () => PostVote)
public rVotes: Array<User & {PostVote: PostVote}>; public rVotes: Array<User & {PostVote: PostVote}>;
@ -31,6 +39,13 @@ export class Post extends Model<Post> {
return await this.$get("rAuthor") as User; return await this.$get("rAuthor") as User;
} }
/**
* Returns the activity of the post.
*/
public async activity(): Promise<Activity|undefined> {
return await this.$get("rActivity") as Activity;
}
/** /**
* Returns the votes on a post * Returns the votes on a post
*/ */
@ -89,7 +104,7 @@ export class Post extends Model<Post> {
} }
/** /**
* Returns the type of vote that was performend on the post by the user specified by the user id. * Returns the type of vote that was performed on the post by the user specified by the user id.
* @param userId * @param userId
*/ */
public async userVote({userId}: {userId: number}): Promise<VoteType> { public async userVote({userId}: {userId: number}): Promise<VoteType> {

@ -207,20 +207,17 @@ export class User extends Model<User> {
return await this.$get("rReceivedRequests") as Request[]; return await this.$get("rReceivedRequests") as Request[];
} }
/**
* a list of posts the user has created
* @param first
* @param offset
*/
public async posts({first, offset}: { first: number, offset: number }): Promise<Post[]> { public async posts({first, offset}: { first: number, offset: number }): Promise<Post[]> {
const limit = first ?? 10; const limit = first ?? 10;
offset = offset ?? 0; offset = offset ?? 0;
return await this.$get("rPosts", {limit, offset}) as Post[]; return await this.$get("rPosts", {limit, offset}) as Post[];
} }
/**
* @deprecated
* use {@link postCount} instead
*/
public async numberOfPosts(): Promise<number> {
return this.postCount();
}
/** /**
* number of posts the user created * number of posts the user created
*/ */

@ -11,3 +11,4 @@ export {GroupAdmin} from "./GroupAdmin";
export {GroupMember} from "./GroupMember"; export {GroupMember} from "./GroupMember";
export {Event} from "./Event"; export {Event} from "./Event";
export {EventParticipant} from "./EventParticipant"; export {EventParticipant} from "./EventParticipant";
export {Activity} from "./Activity";

Loading…
Cancel
Save