diff --git a/src/business/commands/create-comment.ts b/src/business/commands/create-comment.ts new file mode 100644 index 00000000..962ceb2a --- /dev/null +++ b/src/business/commands/create-comment.ts @@ -0,0 +1,14 @@ +import { Observable } from 'rxjs'; +import { CommentInput } from '../models/inputs/comment-input'; +import { ICommentRepository } from '../repositories/i-comment-repository'; +import { ICreateComment } from './i-create-comment'; + + + +export class CreateComment implements ICreateComment { + constructor(private readonly commentRepository: ICommentRepository) { } + + execute(input: CommentInput): Observable { + return this.commentRepository.insert(input); + } +} diff --git a/src/business/commands/delete-comment.ts b/src/business/commands/delete-comment.ts new file mode 100644 index 00000000..b687424c --- /dev/null +++ b/src/business/commands/delete-comment.ts @@ -0,0 +1,12 @@ +import { Observable, iif, of } from 'rxjs'; +import { ICommentRepository } from '../repositories/i-comment-repository'; +import { IDeleteComment } from './i-delete-comment'; +import { flatMap, catchError, tap } from 'rxjs/operators'; + +export class DeleteComment implements IDeleteComment { + constructor(private readonly commentRepository: ICommentRepository) { } + + execute(id: string): Observable { + return this.commentRepository.delete(id); + } +} diff --git a/src/business/commands/get-comments.ts b/src/business/commands/get-comments.ts new file mode 100644 index 00000000..1c165ba5 --- /dev/null +++ b/src/business/commands/get-comments.ts @@ -0,0 +1,13 @@ +import { Observable } from 'rxjs'; +import { Comment as CommentResult } from '../../business/models/results/comment'; +import { ICommentRepository } from '../repositories/i-comment-repository'; +import { IGetComments } from './i-get-comments'; + + +export class GetComments implements IGetComments { + constructor(private commentRepository: ICommentRepository) { } + + execute(parameter: string): Observable { + return this.commentRepository.get(parameter); + } +} diff --git a/src/business/commands/i-create-comment.ts b/src/business/commands/i-create-comment.ts new file mode 100644 index 00000000..b8d0c70b --- /dev/null +++ b/src/business/commands/i-create-comment.ts @@ -0,0 +1,5 @@ +import { CommentInput } from '../models/inputs/comment-input'; +import { IObservableCommand } from './core/i-observable-command'; + +export interface ICreateComment extends IObservableCommand { +} diff --git a/src/business/commands/i-delete-comment.ts b/src/business/commands/i-delete-comment.ts new file mode 100644 index 00000000..4f716a2b --- /dev/null +++ b/src/business/commands/i-delete-comment.ts @@ -0,0 +1,4 @@ +import { IObservableCommand } from './core/i-observable-command'; + +export interface IDeleteComment extends IObservableCommand { +} diff --git a/src/business/commands/i-get-comments.ts b/src/business/commands/i-get-comments.ts new file mode 100644 index 00000000..cc5a4f5f --- /dev/null +++ b/src/business/commands/i-get-comments.ts @@ -0,0 +1,4 @@ +import { Comment as CommentResult } from '../../business/models/results/comment'; +import { IObservableCommand } from './core/i-observable-command'; + +export interface IGetComments extends IObservableCommand { } diff --git a/src/business/models/inputs/comment-input.ts b/src/business/models/inputs/comment-input.ts new file mode 100644 index 00000000..77bad43c --- /dev/null +++ b/src/business/models/inputs/comment-input.ts @@ -0,0 +1,12 @@ +export class CommentInput { + authorId!: string; + eventId!: string; + description!: string; + date!: Date; + isDeleted!: boolean; + + constructor(init?: any) { + Object.assign(this, init); + this.isDeleted = false; + } +} diff --git a/src/business/models/results/comment.ts b/src/business/models/results/comment.ts new file mode 100644 index 00000000..5393dc35 --- /dev/null +++ b/src/business/models/results/comment.ts @@ -0,0 +1,15 @@ +import { User } from '../../../data/entities/user'; + +export class Comment { + id!: string; + user!: User; + eventId!: string; + description!: string; + date!: Date; + isDeleted!: boolean; + + constructor(init?: any) { + Object.assign(this, init); + this.isDeleted = false; + } +} diff --git a/src/business/repositories/i-comment-repository.ts b/src/business/repositories/i-comment-repository.ts new file mode 100644 index 00000000..3e02847d --- /dev/null +++ b/src/business/repositories/i-comment-repository.ts @@ -0,0 +1,17 @@ +import { Observable } from 'rxjs'; +import { CommentInput } from '../models/inputs/comment-input'; +import { Comment as CommentResult } from '../../business/models/results/comment'; + + +export interface ICommentRepository { + + get(eventId: string): Observable; + + insert(comment: CommentInput): Observable; + + update(input: CommentInput): Observable; + + getById(id: string): Observable; + + delete(id: string): Observable; +} diff --git a/src/data/entities/comment.ts b/src/data/entities/comment.ts new file mode 100644 index 00000000..c5772e67 --- /dev/null +++ b/src/data/entities/comment.ts @@ -0,0 +1,32 @@ +import { BaseEntity, Column, Entity, PrimaryGeneratedColumn, JoinColumn, ManyToOne } from 'typeorm'; +import { User } from './user'; +import { Event } from './event'; + + +@Entity('comment') +export class Comment extends BaseEntity { + @PrimaryGeneratedColumn('uuid') + id!: string; + + @JoinColumn({ name: 'author_id' }) + @ManyToOne(() => User, (user) => user.comments) + user!: User; + + @JoinColumn({ name: 'event_id'}) + @ManyToOne(() => Event, (event) => event.comments) + event!: Event; + + @Column() + description!: string; + + @Column() + date!: Date; + + @Column() + isDeleted!: boolean; + + constructor(init?: Partial) { + super(); + Object.assign(this, init); + } +} diff --git a/src/data/entities/event.ts b/src/data/entities/event.ts index 66ba6db9..5ad223c5 100644 --- a/src/data/entities/event.ts +++ b/src/data/entities/event.ts @@ -1,7 +1,8 @@ -import { BaseEntity, Column, Entity, JoinColumn, OneToOne, PrimaryGeneratedColumn, CreateDateColumn } from 'typeorm'; +import { BaseEntity, Column, Entity, JoinColumn, OneToOne, PrimaryGeneratedColumn, CreateDateColumn, OneToMany } from 'typeorm'; import { Address } from './address'; import { Sport } from './sport'; import { User } from './user'; +import { Comment } from '../entities/comment'; @Entity('event') export class Event extends BaseEntity { @@ -54,6 +55,9 @@ export class Event extends BaseEntity { @Column() status!: number; + @OneToMany(() => Comment, (comment) => comment.event, { cascade: true, nullable : true, eager : true }) + comments!: Comment[]; + constructor(init?: Partial) { super(); Object.assign(this, init); diff --git a/src/data/entities/user.ts b/src/data/entities/user.ts index 37c837c8..e7719088 100644 --- a/src/data/entities/user.ts +++ b/src/data/entities/user.ts @@ -1,5 +1,6 @@ -import { BaseEntity, Column, Entity, PrimaryGeneratedColumn, OneToOne, JoinColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm'; +import { BaseEntity, Column, Entity, PrimaryGeneratedColumn, OneToOne, JoinColumn, CreateDateColumn, UpdateDateColumn, OneToMany } from 'typeorm'; import { UserDetails } from './user-details'; +import { Comment } from '../entities/comment'; @Entity('user') export class User extends BaseEntity { @@ -29,6 +30,9 @@ export class User extends BaseEntity { @UpdateDateColumn({ name: 'modified_date' }) modifiedDate!: Date; + @OneToMany(() => Comment, (comment) => comment.user, { cascade: true }) + comments!: Comment[]; + constructor(init?: any) { super(); Object.assign(this, init); diff --git a/src/data/factories/comment-factory.ts b/src/data/factories/comment-factory.ts new file mode 100644 index 00000000..88d1bf6b --- /dev/null +++ b/src/data/factories/comment-factory.ts @@ -0,0 +1,30 @@ +import { from } from 'rxjs'; +import { Comment as CommentResult } from '../../business/models/results/comment'; +import { Comment, Comment as CommentEntity } from '../entities/comment'; +import { CommentInput } from '../../business/models/inputs/comment-input'; +import { UserFactory } from './user-factory'; +import { EventFactory } from './event-factory'; + +export class CommentFactory { + + static entity = { + fromCommentResult: (comment: CommentResult) => new CommentEntity(comment), + fromId: (id: string) => from(Comment.findOne(id)), + fromCommentInput: (comment: CommentInput, userId: string): CommentEntity => new CommentEntity({ + ...comment, + description: comment.description, + date: comment.date, + user: UserFactory.entity.fromId(userId), + event: EventFactory.entity.fromId(comment.eventId), + }), + }; + + static result = { + fromCommentEntity: (comment: CommentEntity) => new CommentResult(comment), + }; + + static results = { + fromCommentEntities: (comments: CommentEntity[]): CommentResult[] => + comments.map((comment) => new CommentResult(comment)), + }; +} diff --git a/src/data/factories/event-factory.ts b/src/data/factories/event-factory.ts index 7adf1631..28a6a2f6 100644 --- a/src/data/factories/event-factory.ts +++ b/src/data/factories/event-factory.ts @@ -9,6 +9,7 @@ import { UserFactory } from './user-factory'; import { UserEvents } from '../entities/user-events'; import { NotificationType } from '../enums/notification-type'; import { EventStatus } from '../enums/event-status'; +import { CommentFactory } from './comment-factory'; export class EventFactory { static entity = { @@ -19,6 +20,7 @@ export class EventFactory { owner: UserFactory.entity.fromId(userId), status: event.id ? EventStatus.Edited : EventStatus.Active, }), + fromId: (id: string) => new EventEntity({ id }), }; static result = { @@ -26,8 +28,7 @@ export class EventFactory { ...event, address: Address.findOne(event.address.id), sport: Sport.findOne(event.sport.id), - numberOfParticipants: UserEvents.count({ eventId: event.id, status: NotificationType.ApproveJoin }), - }), + numberOfParticipants: UserEvents.count({ eventId: event.id, status: NotificationType.ApproveJoin })}), }; static results = { diff --git a/src/data/factories/user-events-factory.ts b/src/data/factories/user-events-factory.ts index a65c1e59..ab3c2ade 100644 --- a/src/data/factories/user-events-factory.ts +++ b/src/data/factories/user-events-factory.ts @@ -20,7 +20,7 @@ export class UserEventsFactory { fromUserEventsEntities: (userEvents: UserEventsEntity[]): UserEventsResult[] => { return userEvents.map((userEvent) => new UserEventsResult({ ...userEvent, - user: User.findOne(userEvent.userId), + user: User.findOne(userEvent.userId, { relations: ['details']}), })); }, diff --git a/src/data/migrations/1596093629882-create-comment-table.ts b/src/data/migrations/1596093629882-create-comment-table.ts new file mode 100644 index 00000000..c9d53b79 --- /dev/null +++ b/src/data/migrations/1596093629882-create-comment-table.ts @@ -0,0 +1,60 @@ +import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm'; + +export class CreateCommentsTable1596093629882 implements MigrationInterface { + + async up(queryRunner: QueryRunner): Promise { + await queryRunner.createTable(new Table({ + columns: [ + { + isPrimary: true, + name: 'id', + type: 'varchar(36)', + }, + { + name: 'author_id', + type: 'varchar(36)', + }, + { + name: 'event_id', + type: 'varchar(36)', + }, + { + name: 'description', + type: 'varchar(200)', + }, + { + name: 'date', + type: 'DATETIME', + }, + { + name: 'isDeleted', + type: 'boolean', + default: 'false', + }, + ], + name: 'comment', + }), true); + + await queryRunner.createForeignKey('comment', new TableForeignKey({ + columnNames: ['author_id'], + name: 'FK_comment_users', + onDelete: 'CASCADE', + onUpdate: 'CASCADE', + referencedColumnNames: ['id'], + referencedTableName: 'user', + })); + + await queryRunner.createForeignKey('comment', new TableForeignKey({ + columnNames: ['event_id'], + name: 'FK_comment_events', + onDelete: 'CASCADE', + onUpdate: 'CASCADE', + referencedColumnNames: ['id'], + referencedTableName: 'event', + })); + } + + async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropTable('comment'); + } +} diff --git a/src/data/repositories/comment-repository.ts b/src/data/repositories/comment-repository.ts new file mode 100644 index 00000000..df16c958 --- /dev/null +++ b/src/data/repositories/comment-repository.ts @@ -0,0 +1,49 @@ +import { from, Observable, of, throwError, zip } from 'rxjs'; +import { catchError, flatMap, map, switchMap, tap } from 'rxjs/operators'; +import { CommentInput } from '../../business/models/inputs/comment-input'; +import { Comment as CommentResult } from '../../business/models/results/comment'; +import { ICommentRepository } from '../../business/repositories/i-comment-repository'; +import { UserContext } from '../../utilities/user-context'; +import { Comment } from '../entities/comment'; +import { CommentFactory } from '../factories/comment-factory'; + +export class CommentRepository implements ICommentRepository { + constructor(private readonly userContext: UserContext) { + } + get(eventId: string): Observable { + return from(Comment.find({ relations: ['user', 'user.details'], where: { event: eventId , isDeleted: false}, order: { date: 'ASC'}})) + .pipe(map((results) => CommentFactory.results.fromCommentEntities(results))); + } + + insert(input: CommentInput): Observable { + const commentInput = of(CommentFactory.entity.fromCommentInput(input, this.userContext.userId)); + + return zip(commentInput) + .pipe(flatMap((result) => { + return result[0].save(); + })) + .pipe(catchError((e) => throwError(new Error(e)))) + .pipe(map((comment) => CommentFactory.result.fromCommentEntity(comment))); + } + + update(input: CommentInput): Observable { + return of(CommentFactory.entity.fromCommentInput(input, input.authorId)) + .pipe(switchMap((entity) => entity.save())) + .pipe(map((entity) => CommentFactory.result.fromCommentEntity(entity))); + } + + getById(id: any): Observable { + return from(Comment.findOneOrFail(id, { relations: ['user'] })) + .pipe(map((event) => CommentFactory.result.fromCommentEntity(event))); + } + + delete(id: string): Observable { + return from(Comment.findOneOrFail(id)) + .pipe(map((entity) => { + entity.isDeleted = true; + entity.save(); + return true; + })) + .pipe(catchError(() => of(false))); + } +} diff --git a/src/data/repositories/event-repository.ts b/src/data/repositories/event-repository.ts index 8980901b..1cfbb1dc 100644 --- a/src/data/repositories/event-repository.ts +++ b/src/data/repositories/event-repository.ts @@ -1,5 +1,5 @@ import { from, Observable, of, zip } from 'rxjs'; -import { map, switchMap, flatMap, catchError } from 'rxjs/operators'; +import { map, switchMap, flatMap, catchError, tap } from 'rxjs/operators'; import { EventInput } from '../../business/models/inputs/event-input'; import { Event as EventResult } from '../../business/models/results/event'; import { Pagination } from '../../business/models/results/pagination'; diff --git a/src/presentation/commands/graph/initialize-graph.ts b/src/presentation/commands/graph/initialize-graph.ts index ce98f12b..1fa1f51b 100644 --- a/src/presentation/commands/graph/initialize-graph.ts +++ b/src/presentation/commands/graph/initialize-graph.ts @@ -41,6 +41,9 @@ import { ISaveEvent } from '../../../business/commands/i-save-event'; import { IActivateUser } from '../../../business/commands/i-activate-user'; import { IResendActivationEmail } from '../../../business/commands/i-resend-activation-email'; import { ISaveUserDetails } from '../../../business/commands/i-save-user-details'; +import { ICreateComment } from '../../../business/commands/i-create-comment'; +import { IGetComments } from '../../../business/commands/i-get-comments'; +import { IDeleteComment } from '../../../business/commands/i-delete-comment'; export class InitializeGraph implements IInitializeGraph { private static readonly rootPath = `${__dirname}/../../graph`; @@ -73,6 +76,7 @@ export class InitializeGraph implements IInitializeGraph { private readonly saveEvent: ISaveEvent, private readonly createUser: ICreateUser, private readonly createTokens: ICreateTokens, + private readonly createComment: ICreateComment, private readonly updateUser: IUpdateUser, private readonly eventById: IGetEventById, private readonly eventDetails: IGetEventDetails, @@ -91,6 +95,8 @@ export class InitializeGraph implements IInitializeGraph { private readonly rejectRequest: IRejectRequest, private readonly cities: IGetCities, private readonly counties: IGetCounties, + private readonly comments: IGetComments, + private readonly deleteComment: IDeleteComment, private readonly markAsRead: IMarkAsRead, private readonly markAllAsRead: IMarkAllAsRead, private readonly recoverPassword: IRecoverPassword, @@ -151,6 +157,7 @@ export class InitializeGraph implements IInitializeGraph { rootValue: { auth: (context: any) => this.executer(expContext, () => this.createTokens.execute(context.input).toPromise(), false), saveEvent: (context: any) => this.executer(expContext, () => this.saveEvent.execute(context.input).toPromise()), + saveComment: (context: any) => this.executer(expContext, () => this.createComment.execute(context.input).toPromise()), createUser: (context: any) => this.executer(expContext, () => this.createUser.execute(context.input).toPromise(), false), eventById: (context: any) => this.executer(expContext, () => this.eventById.execute(context).toPromise(), false), @@ -184,6 +191,8 @@ export class InitializeGraph implements IInitializeGraph { () => this.cities.execute(context.parameter).toPromise()), counties: (context: any) => this.executer(expContext, () => this.counties.execute(context.parameter).toPromise()), + comments: (context: any) => this.executer(expContext, + () => this.comments.execute(context.parameter).toPromise()), markAsRead: (context: any) => this.executer(expContext, () => this.markAsRead.execute(context.parameter).toPromise()), markAllAsRead: (context: any) => this.executer(expContext, @@ -194,6 +203,8 @@ export class InitializeGraph implements IInitializeGraph { () => this.updateCredentials.execute(context.input).toPromise(), false), cancelEvent: (context: any) => this.executer(expContext, () => this.cancelEvent.execute(context.parameter).toPromise()), + deleteComment: (context: any) => this.executer(expContext, + () => this.deleteComment.execute(context.parameter).toPromise()), leaveEvent: (context: any) => this.executer(expContext, () => this.leaveEvent.execute(context.parameter).toPromise()), kickoutUser: (context: any) => this.executer(expContext, diff --git a/src/presentation/commands/ioc/create-common-container.ts b/src/presentation/commands/ioc/create-common-container.ts index a66812cb..8d75a099 100644 --- a/src/presentation/commands/ioc/create-common-container.ts +++ b/src/presentation/commands/ioc/create-common-container.ts @@ -53,6 +53,10 @@ import { UpdateEvent } from '../../../business/commands/update-event'; import { ActivateUser } from '../../../business/commands/activate-user'; import { ResendActivationEmail } from '../../../business/commands/resend-activation-email'; import { SaveUserDetails } from '../../../business/commands/save-user-details'; +import { CommentRepository } from '../../../data/repositories/comment-repository'; +import { CreateComment } from '../../../business/commands/create-comment'; +import { GetComments } from '../../../business/commands/get-comments'; +import { DeleteComment } from '../../../business/commands/delete-comment'; export class CreateCommonContainer implements ICreateContainer { private readonly dataDatabaseConnection: ReadonlyArray = [ @@ -66,6 +70,7 @@ export class CreateCommonContainer implements ICreateContainer { ]; private readonly businessCommands: ReadonlyArray = [ + { createComment: asClass(CreateComment).transient().classic() }, { createEvent: asClass(CreateEvent).transient().classic() }, { createUser: asClass(CreateUser).transient().classic() }, { createTokens: asClass(CreateTokens).transient().classic() }, @@ -80,8 +85,10 @@ export class CreateCommonContainer implements ICreateContainer { { createNotificationToken: asClass(CreateNotificationToken).transient().classic() }, { getNotificationTokens: asClass(GetNotificationTokens).transient().classic() }, { deleteNotificationToken: asClass(DeleteNotificationToken).transient().classic() }, + { deleteComment: asClass(DeleteComment).transient().classic() }, { joinEvent: asClass(JoinEvent).transient().classic() }, { getUserById: asClass(GetUserById).transient().classic() }, + { comments: asClass(GetComments).transient().classic() }, { refreshToken: asClass(RefreshToken).transient().classic() }, { loginFacebook: asClass(LoginFacebook).transient().classic() }, { approveRequest: asClass(ApproveRequest).transient().classic() }, @@ -112,6 +119,7 @@ export class CreateCommonContainer implements ICreateContainer { { userEventsRepository: asClass(UserEventsRepository).transient().classic() }, { cityRepository: asClass(CityRepository).transient().classic() }, { countyRepository: asClass(CountyRepository).transient().classic() }, + { commentRepository: asClass(CommentRepository).transient().classic() }, ]; private readonly presentationCommands: ReadonlyArray = [ diff --git a/src/presentation/graph/Mutation.graphql b/src/presentation/graph/Mutation.graphql index e13626e3..08bb4838 100644 --- a/src/presentation/graph/Mutation.graphql +++ b/src/presentation/graph/Mutation.graphql @@ -74,5 +74,10 @@ type Mutation { "It's used to save the details relate to the user" saveUserDetails(input: UserDetailsInput): User + + "It's used to create a new comment." + saveComment(input: CommentInput!): Comment! + "It's used to delete a comment." + deleteComment(parameter: String!): Boolean } diff --git a/src/presentation/graph/Query.graphql b/src/presentation/graph/Query.graphql index b2b7d3b2..9d4eadf7 100644 --- a/src/presentation/graph/Query.graphql +++ b/src/presentation/graph/Query.graphql @@ -26,4 +26,7 @@ type Query { "It's used to fetch the counties from server." counties(parameter: String): [County] + + "It's used to fetch the comments of an event." + comments(parameter: String): [Comment] } diff --git a/src/presentation/graph/inputs/CommentInput.graphql b/src/presentation/graph/inputs/CommentInput.graphql new file mode 100644 index 00000000..bc680d9a --- /dev/null +++ b/src/presentation/graph/inputs/CommentInput.graphql @@ -0,0 +1,16 @@ +""" +An input model used for messages. +""" +input CommentInput { + "The id of the author." + authorId: String! + + "The id of the event." + eventId: String! + + "The message content." + description: String! + + "The Message date." + date: DateTime! +} diff --git a/src/presentation/graph/results/Comment.graphql b/src/presentation/graph/results/Comment.graphql new file mode 100644 index 00000000..3b7bdcbe --- /dev/null +++ b/src/presentation/graph/results/Comment.graphql @@ -0,0 +1,22 @@ +type Comment { + "The id of the comment." + id: String! + + "The id of the author." + authorId: String! + + "The id of the event." + eventId: String! + + "The user information." + user: User! + + "The message content." + description: String! + + "The Message date." + date: DateTime! + + "The delete status of message" + isDeleted: Boolean! +} diff --git a/src/presentation/graph/results/Event.graphql b/src/presentation/graph/results/Event.graphql index be657efd..44ed038b 100644 --- a/src/presentation/graph/results/Event.graphql +++ b/src/presentation/graph/results/Event.graphql @@ -47,4 +47,7 @@ type Event { "The status of the event" status: Int + + "Comments of the event" + comments: [Comment] }