diff --git a/src/graphql/resolvers.ts b/src/graphql/resolvers.ts index 5b5a40b..1898212 100644 --- a/src/graphql/resolvers.ts +++ b/src/graphql/resolvers.ts @@ -1,12 +1,11 @@ +import {AggregateError} from "bluebird"; import {GraphQLError} from "graphql"; import * as status from "http-status"; import dataaccess from "../lib/dataaccess"; -import {UserNotFoundError} from "../lib/errors/UserNotFoundError"; -import {Group} from "../lib/models"; -import * as models from "../lib/models"; -import {GroupNotFoundGqlError, NotLoggedInGqlError, PostNotFoundGqlError} from "../lib/errors/graphqlErrors"; +import {NotLoggedInGqlError, PostNotFoundGqlError} from "../lib/errors/graphqlErrors"; import globals from "../lib/globals"; import {InternalEvents} from "../lib/InternalEvents"; +import * as models from "../lib/models"; import {is} from "../lib/regex"; /** @@ -228,6 +227,15 @@ export function resolver(req: any, res: any): any { return new GraphQLError("No sender or type given."); } }, + async removeFriend({friendId}: {friendId: number}) { + if (req.session.userId) { + const self = await models.User.findByPk(req.session.userId); + return await self.removeFriend(friendId); + } else { + res.status(status.UNAUTHORIZED); + return new NotLoggedInGqlError(); + } + }, async getPosts({first, offset, sort}: {first: number, offset: number, sort: dataaccess.SortType}) { return await dataaccess.getPosts(first, offset, sort); }, @@ -240,15 +248,12 @@ export function resolver(req: any, res: any): any { }, async joinGroup({id}: {id: number}) { if (req.session.userId) { - const group = await models.Group.findByPk(id); - if (group) { - const user = await models.User.findByPk(req.session.userId); - await group.$add("rMembers", user); - await (await group.chat()).$add("rMembers", user); - return group; - } else { + try { + return await dataaccess + .changeGroupMembership(id, req.session.userId, dataaccess.MembershipChangeAction.ADD); + } catch (err) { res.status(status.BAD_REQUEST); - return new GroupNotFoundGqlError(id); + return err.graphqlError; } } else { res.status(status.UNAUTHORIZED); @@ -257,15 +262,12 @@ export function resolver(req: any, res: any): any { }, async leaveGroup({id}: {id: number}) { if (req.session.userId) { - const group = await models.Group.findByPk(id); - if (group) { - const user = await models.User.findByPk(req.session.userId); - await group.$remove("rMembers", user); - await (await group.chat()).$remove("rMembers", user); - return group; - } else { + try { + return await dataaccess + .changeGroupMembership(id, req.session.userId, dataaccess.MembershipChangeAction.REMOVE); + } catch (err) { res.status(status.BAD_REQUEST); - return new GroupNotFoundGqlError(id); + return err.graphqlError; } } else { res.status(status.UNAUTHORIZED); @@ -275,22 +277,18 @@ export function resolver(req: any, res: any): any { async addGroupAdmin({groupId, userId}: {groupId: number, userId: number}) { if (req.session.userId) { const group = await models.Group.findByPk(groupId); - const user = await models.User.findByPk(userId); const self = await models.User.findByPk(req.session.userId); - if (!group) { - res.status(status.BAD_REQUEST); - return new GroupNotFoundGqlError(groupId); - } - if (!user) { - res.status(status.BAD_REQUEST); - return new UserNotFoundError(userId.toString()).graphqlError; - } - if (!(await group.$has("rAdmins", self)) && (await group.creator()) !== self.id) { + if (group && !(await group.$has("rAdmins", self)) && (await group.creator()) !== self.id) { res.status(status.FORBIDDEN); return new GraphQLError("You are not a group admin!"); } - await group.$add("rAdmins", user); - return group; + try { + return await dataaccess + .changeGroupMembership(groupId, userId, dataaccess.MembershipChangeAction.OP); + } catch (err) { + res.status(status.BAD_REQUEST); + return err.graphqlError; + } } else { res.status(status.UNAUTHORIZED); @@ -300,25 +298,23 @@ export function resolver(req: any, res: any): any { async removeGroupAdmin({groupId, userId}: {groupId: number, userId: number}) { if (req.session.userId) { const group = await models.Group.findByPk(groupId); - const user = await models.User.findByPk(userId); - if (!group) { - res.status(status.BAD_REQUEST); - return new GroupNotFoundGqlError(groupId); - } - if (!user) { - res.status(status.BAD_REQUEST); - return new UserNotFoundError(userId.toString()).graphqlError; - } - if ((await group.creator()).id === userId) { + const isCreator = Number(group.creatorId) === Number(req.session.userId); + const userIsCreator = Number(group.creatorId) === Number(userId) ; + if (group && !isCreator && Number(userId) !== Number(req.session.userId)) { res.status(status.FORBIDDEN); - return new GraphQLError("You can't remove the creator of a group."); - } - if ((await group.creator()).id !== req.session.userId) { + return new GraphQLError("You are not the group creator!"); + } else if (userIsCreator) { res.status(status.FORBIDDEN); - return new GraphQLError("You are not a group admin!"); + return new GraphQLError("You are not allowed to remove a creator as an admin."); + } + + try { + return await dataaccess + .changeGroupMembership(groupId, userId, dataaccess.MembershipChangeAction.DEOP); + } catch (err) { + res.status(status.BAD_REQUEST); + return err.graphqlError; } - await group.$remove("rAdmins", user); - return group; } else { res.status(status.UNAUTHORIZED); diff --git a/src/graphql/schema.graphql b/src/graphql/schema.graphql index f81050c..2ca3ac2 100644 --- a/src/graphql/schema.graphql +++ b/src/graphql/schema.graphql @@ -49,6 +49,9 @@ type Mutation { "lets you deny a request for a given request id" denyRequest(requestId: ID!): Boolean + "removes a friend" + removeFriend(friendId: ID!): Boolean + "send a message in a Chatroom" sendMessage(chatId: ID!, content: String!): ChatMessage @@ -101,6 +104,12 @@ interface UserData { "all friends of the user" friends: [User] + + "the points of the user" + points: Int + + "the levels of the user depending on the points" + level: Int } "represents a single user account" @@ -128,6 +137,15 @@ type User implements UserData{ "all friends of the user" friends: [User] + + "the points of the user" + points: Int + + "the groups the user has joined" + groups: [Group] + + "the levels of the user depending on the points" + level: Int } type Profile implements UserData { @@ -173,6 +191,11 @@ type Profile implements UserData { "all groups the user has joined" groups: [Group] + "the points of the user" + points: Int + + "the levels of the user depending on the points" + level: Int } "represents a single user post" diff --git a/src/lib/dataaccess.ts b/src/lib/dataaccess.ts index ec753e4..839a75d 100644 --- a/src/lib/dataaccess.ts +++ b/src/lib/dataaccess.ts @@ -1,7 +1,11 @@ import * as crypto from "crypto"; +import * as status from "http-status"; import {Sequelize} from "sequelize-typescript"; import {ChatNotFoundError} from "./errors/ChatNotFoundError"; import {EmailAlreadyRegisteredError} from "./errors/EmailAlreadyRegisteredError"; +import {GroupNotFoundGqlError, NotLoggedInGqlError} from "./errors/graphqlErrors"; +import {GroupNotFoundError} from "./errors/GroupNotFoundError"; +import {NoActionSpecifiedError} from "./errors/NoActionSpecifiedError"; import {UserNotFoundError} from "./errors/UserNotFoundError"; import globals from "./globals"; import {InternalEvents} from "./InternalEvents"; @@ -40,6 +44,8 @@ namespace dataaccess { models.Group, models.GroupAdmin, models.GroupMember, + models.EventParticipant, + models.Event, ]); } catch (err) { globals.logger.error(err.message); @@ -207,7 +213,7 @@ namespace dataaccess { requestType = requestType || RequestType.FRIENDREQUEST; const request = await models.Request.create({senderId: sender, receiverId: receiver, requestType}); - globals.internalEmitter.emit(InternalEvents.REQUESTCREATE, Request); + globals.internalEmitter.emit(InternalEvents.REQUESTCREATE, request); return request; } @@ -232,6 +238,38 @@ namespace dataaccess { }); } + /** + * Changes the membership of a user + * @param groupId + * @param userId + * @param action + */ + export async function changeGroupMembership(groupId: number, userId: number, action: MembershipChangeAction): + Promise { + const group = await models.Group.findByPk(groupId); + if (group) { + const user = await models.User.findByPk(userId); + if (user) { + if (action === MembershipChangeAction.ADD) { + await group.$add("rMembers", user); + } else if (action === MembershipChangeAction.REMOVE) { + await group.$remove("rMembers", user); + } else if (action === MembershipChangeAction.OP) { + await group.$add("rAdmins", user); + } else if (action === MembershipChangeAction.DEOP) { + await group.$remove("rAdmins", user); + } else { + throw new NoActionSpecifiedError(MembershipChangeAction); + } + return group; + } else { + throw new UserNotFoundError(userId); + } + } else { + throw new GroupNotFoundError(groupId); + } + } + /** * Enum representing the types of votes that can be performed on a post. */ @@ -256,6 +294,13 @@ namespace dataaccess { TOP = "TOP", NEW = "NEW", } + + export enum MembershipChangeAction { + ADD, + REMOVE, + OP, + DEOP, + } } export default dataaccess; diff --git a/src/lib/errors/GroupNotFoundError.ts b/src/lib/errors/GroupNotFoundError.ts new file mode 100644 index 0000000..99d89a5 --- /dev/null +++ b/src/lib/errors/GroupNotFoundError.ts @@ -0,0 +1,8 @@ +import {BaseError} from "./BaseError"; + +export class GroupNotFoundError extends BaseError { + constructor(groupId: number) { + super(`Group ${groupId} not found!`); + } + +} diff --git a/src/lib/errors/NoActionSpecifiedError.ts b/src/lib/errors/NoActionSpecifiedError.ts new file mode 100644 index 0000000..ee4eedb --- /dev/null +++ b/src/lib/errors/NoActionSpecifiedError.ts @@ -0,0 +1,11 @@ +import {BaseError} from "./BaseError"; + +export class NoActionSpecifiedError extends BaseError { + constructor(actions?: any) { + if (actions) { + super(`No action of '${Object.keys(actions).join(", ")}'`); + } else { + super("No action specified!"); + } + } +} diff --git a/src/lib/errors/UserNotFoundError.ts b/src/lib/errors/UserNotFoundError.ts index 7869242..f327549 100644 --- a/src/lib/errors/UserNotFoundError.ts +++ b/src/lib/errors/UserNotFoundError.ts @@ -1,7 +1,7 @@ import {BaseError} from "./BaseError"; export class UserNotFoundError extends BaseError { - constructor(username: string) { + constructor(username: (string|number)) { super(`User ${username} not found!`); } } diff --git a/src/lib/models/Event.ts b/src/lib/models/Event.ts new file mode 100644 index 0000000..49aede3 --- /dev/null +++ b/src/lib/models/Event.ts @@ -0,0 +1,34 @@ +import {BelongsTo, BelongsToMany, Column, ForeignKey, Model, NotNull, Table} from "sequelize-typescript"; +import {EventParticipant} from "./EventParticipant"; +import {Group} from "./Group"; +import {User} from "./User"; + +@Table({underscored: true}) +export class Event extends Model { + @NotNull + @Column({allowNull: false}) + public name: string; + + @NotNull + @Column({allowNull: false}) + public dueDate: Date; + + @NotNull + @ForeignKey(() => Group) + @Column({allowNull: false}) + public groupId: number; + + @BelongsTo(() => Group, "groupId") + public rGroup: Group; + + @BelongsToMany(() => User, () => EventParticipant) + public rParticipants: User[]; + + public async group(): Promise { + return await this.$get("rGroup") as Group; + } + + public async participants({first, offset}: {first: number, offset: number}): Promise { + return await this.$get("rParticipants") as User[]; + } +} diff --git a/src/lib/models/EventParticipant.ts b/src/lib/models/EventParticipant.ts new file mode 100644 index 0000000..c7c7199 --- /dev/null +++ b/src/lib/models/EventParticipant.ts @@ -0,0 +1,16 @@ +import {BelongsTo, BelongsToMany, Column, ForeignKey, Model, NotNull, Table} from "sequelize-typescript"; +import {Event} from "./Event"; +import {User} from "./User"; + +@Table({underscored: true}) +export class EventParticipant extends Model { + @NotNull + @ForeignKey(() => User) + @Column({allowNull: false}) + public userId: number; + + @NotNull + @ForeignKey(() => Event) + @Column({allowNull: false}) + public eventId: number; +} diff --git a/src/lib/models/Friendship.ts b/src/lib/models/Friendship.ts index 4d7773b..a49be50 100644 --- a/src/lib/models/Friendship.ts +++ b/src/lib/models/Friendship.ts @@ -1,15 +1,17 @@ -import {Column, ForeignKey, Model, NotNull, Table} from "sequelize-typescript"; +import {Column, ForeignKey, Model, NotNull, PrimaryKey, Table} from "sequelize-typescript"; import {User} from "./User"; @Table({underscored: true}) export class Friendship extends Model { @ForeignKey(() => User) + @PrimaryKey @NotNull @Column({allowNull: false}) public userId: number; @ForeignKey(() => User) + @PrimaryKey @NotNull @Column({allowNull: false}) public friendId: number; diff --git a/src/lib/models/Group.ts b/src/lib/models/Group.ts index 222a971..c07544f 100644 --- a/src/lib/models/Group.ts +++ b/src/lib/models/Group.ts @@ -1,18 +1,6 @@ -import * as sqz from "sequelize"; -import { - BelongsTo, - BelongsToMany, - Column, - CreatedAt, ForeignKey, - HasMany, - Model, - NotNull, - Table, - Unique, - UpdatedAt, -} from "sequelize-typescript"; -import {ChatMessage} from "./ChatMessage"; +import {BelongsTo, BelongsToMany, Column, ForeignKey, HasMany, Model, NotNull, Table} from "sequelize-typescript"; import {ChatRoom} from "./ChatRoom"; +import {Event} from "./Event"; import {GroupAdmin} from "./GroupAdmin"; import {GroupMember} from "./GroupMember"; import {User} from "./User"; @@ -20,7 +8,7 @@ import {User} from "./User"; @Table({underscored: true}) export class Group extends Model { @NotNull - @Column( {allowNull: false}) + @Column({allowNull: false}) public name: string; @NotNull @@ -45,6 +33,9 @@ export class Group extends Model { @BelongsTo(() => ChatRoom) public rChat: ChatRoom; + @HasMany(() => Event, "groupId") + public rEvents: Event[]; + public async creator(): Promise { return await this.$get("rCreator") as User; } @@ -53,7 +44,7 @@ export class Group extends Model { return await this.$get("rAdmins") as User[]; } - public async members({first, offset}: {first: number, offset: number}): Promise { + public async members({first, offset}: { first: number, offset: number }): Promise { const limit = first || 10; offset = offset || 0; return await this.$get("rMembers", {limit, offset}) as User[]; @@ -62,4 +53,8 @@ export class Group extends Model { public async chat(): Promise { return await this.$get("rChat") as ChatRoom; } + + public async events(): Promise { + return await this.$get("rEvents") as Event[]; + } } diff --git a/src/lib/models/GroupAdmin.ts b/src/lib/models/GroupAdmin.ts index c04c48c..19bd84a 100644 --- a/src/lib/models/GroupAdmin.ts +++ b/src/lib/models/GroupAdmin.ts @@ -1,16 +1,4 @@ -import * as sqz from "sequelize"; -import { - BelongsTo, - BelongsToMany, - Column, - CreatedAt, ForeignKey, - HasMany, - Model, Not, - NotNull, - Table, - Unique, - UpdatedAt, -} from "sequelize-typescript"; +import {Column, ForeignKey, Model, NotNull, Table,} from "sequelize-typescript"; import {Group} from "./Group"; import {User} from "./User"; diff --git a/src/lib/models/GroupMember.ts b/src/lib/models/GroupMember.ts index f9ba6c4..78c4455 100644 --- a/src/lib/models/GroupMember.ts +++ b/src/lib/models/GroupMember.ts @@ -1,16 +1,4 @@ -import * as sqz from "sequelize"; -import { - BelongsTo, - BelongsToMany, - Column, - CreatedAt, ForeignKey, - HasMany, - Model, Not, - NotNull, - Table, - Unique, - UpdatedAt, -} from "sequelize-typescript"; +import {Column, ForeignKey, Model, NotNull, Table,} from "sequelize-typescript"; import {Group} from "./Group"; import {User} from "./User"; diff --git a/src/lib/models/User.ts b/src/lib/models/User.ts index c79cd5a..3150803 100644 --- a/src/lib/models/User.ts +++ b/src/lib/models/User.ts @@ -1,5 +1,6 @@ import * as sqz from "sequelize"; import { + BelongsTo, BelongsToMany, Column, CreatedAt, @@ -11,9 +12,12 @@ import { UpdatedAt, } from "sequelize-typescript"; import {RequestNotFoundError} from "../errors/RequestNotFoundError"; +import {UserNotFoundError} from "../errors/UserNotFoundError"; import {ChatMember} from "./ChatMember"; import {ChatMessage} from "./ChatMessage"; import {ChatRoom} from "./ChatRoom"; +import {Event} from "./Event"; +import {EventParticipant} from "./EventParticipant"; import {Friendship} from "./Friendship"; import {Group} from "./Group"; import {GroupAdmin} from "./GroupAdmin"; @@ -46,9 +50,12 @@ export class User extends Model { @Column({defaultValue: 0, allowNull: false}) public rankpoints: number; - @BelongsToMany(() => User, () => Friendship) + @BelongsToMany(() => User, () => Friendship, "userId") public rFriends: User[]; + @BelongsToMany(() => User, () => Friendship, "friendId") + public rFriendOf: User[]; + @BelongsToMany(() => Post, () => PostVote) public votes: Array; @@ -58,13 +65,16 @@ export class User extends Model { @BelongsToMany(() => Group, () => GroupAdmin) public rAdministratedGroups: Group[]; + @BelongsToMany(() => Event, () => EventParticipant) + public rEvents: Event[]; + @BelongsToMany(() => Group, () => GroupMember) public rGroups: Group[]; @HasMany(() => Post, "authorId") public rPosts: Post[]; - @HasMany(() => Request, "receiverId") + @HasMany(() => Request, "senderId") public rSentRequests: Request[]; @HasMany(() => Request, "receiverId") @@ -90,8 +100,16 @@ export class User extends Model { return this.getDataValue("createdAt"); } + public get points(): number { + return this.rankpoints; + } + + public get level(): number { + return Math.ceil(this.rankpoints / 100); + } + public async friends(): Promise { - return await this.$get("rFriends") as User[]; + return await this.$get("rFriendOf") as User[]; } public async chats(): Promise { @@ -126,6 +144,10 @@ export class User extends Model { return await this.$get("rGroups") as Group[]; } + public async events(): Promise { + return await this.$get("rEvents") as Event[]; + } + public async denyRequest(sender: number, type: RequestType) { const request = await this.$get("rReceivedRequests", {where: {senderId: sender, requestType: type}}) as Request[]; @@ -140,11 +162,25 @@ export class User extends Model { if (requests.length > 0) { const request = requests[0]; if (request.requestType === RequestType.FRIENDREQUEST) { - await this.$add("friends", sender); + await Friendship.bulkCreate([ + {userId: this.id, friendId: sender}, + {userId: sender, friendId: this.id}, + ], {ignoreDuplicates: true}); await request.destroy(); } } else { throw new RequestNotFoundError(sender, this.id, type); } } + + public async removeFriend(friendId: number) { + const friend = await User.findByPk(friendId); + if (friend) { + await this.$remove("rFriends", friend); + await this.$remove("rFriendOf", friend); + return true; + } else { + throw new UserNotFoundError(friendId); + } + } } diff --git a/src/lib/models/index.ts b/src/lib/models/index.ts index 0388fbc..bc2a53e 100644 --- a/src/lib/models/index.ts +++ b/src/lib/models/index.ts @@ -9,3 +9,5 @@ export {User} from "./User"; export {Group} from "./Group"; export {GroupAdmin} from "./GroupAdmin"; export {GroupMember} from "./GroupMember"; +export {Event} from "./Event"; +export {EventParticipant} from "./EventParticipant";