From 33fd6d70538c5783700ec99010e249382cc47037 Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Thu, 25 Apr 2024 01:21:39 +0200 Subject: [PATCH 01/15] Added delete reservation UI --- .../dto/get-user-reservations-result.dto.ts | 2 + .../queries/get-user-reservations.query.ts | 1 + apps/app/src/api/reservations.ts | 9 +++++ .../src/components/ReservationDeleteModal.vue | 40 +++++++++++++++++++ apps/app/src/models/reservation.ts | 1 + apps/app/src/views/MyReservations.vue | 33 +++++++++++++++ 6 files changed, 86 insertions(+) create mode 100644 apps/app/src/components/ReservationDeleteModal.vue diff --git a/apps/api/src/reservation/application/dto/get-user-reservations-result.dto.ts b/apps/api/src/reservation/application/dto/get-user-reservations-result.dto.ts index 9cc7a9c..2353fcc 100644 --- a/apps/api/src/reservation/application/dto/get-user-reservations-result.dto.ts +++ b/apps/api/src/reservation/application/dto/get-user-reservations-result.dto.ts @@ -5,6 +5,8 @@ export class GetUserReservationsResult implements IQueryResult { @ApiProperty() id: number; @ApiProperty() + inventoryItemId: number; + @ApiProperty() startDate: Date; @ApiProperty() endDate: Date; diff --git a/apps/api/src/reservation/application/queries/get-user-reservations.query.ts b/apps/api/src/reservation/application/queries/get-user-reservations.query.ts index f368737..7620e81 100644 --- a/apps/api/src/reservation/application/queries/get-user-reservations.query.ts +++ b/apps/api/src/reservation/application/queries/get-user-reservations.query.ts @@ -36,6 +36,7 @@ export class GetUserReservationsHandler 'reservation.start_date as startDate', 'reservation.end_date as endDate', 'inventory_item.title', + 'inventory_item.id as inventoryItemId', ]) .innerJoin( 'inventory_item', diff --git a/apps/app/src/api/reservations.ts b/apps/app/src/api/reservations.ts index 22d826d..a71e23c 100644 --- a/apps/app/src/api/reservations.ts +++ b/apps/app/src/api/reservations.ts @@ -13,3 +13,12 @@ export function create(payload: { .post('/reservations', payload) .then(response => response.data); } + +export function deleteReservation(payload: { + inventoryItemId: number; + reservationId: number; +}) { + return instance + .delete('/reservations', { data: payload }) + .then(response => response.data); +} diff --git a/apps/app/src/components/ReservationDeleteModal.vue b/apps/app/src/components/ReservationDeleteModal.vue new file mode 100644 index 0000000..d90cd2b --- /dev/null +++ b/apps/app/src/components/ReservationDeleteModal.vue @@ -0,0 +1,40 @@ + + + + + diff --git a/apps/app/src/models/reservation.ts b/apps/app/src/models/reservation.ts index f1adaef..9a81a8f 100644 --- a/apps/app/src/models/reservation.ts +++ b/apps/app/src/models/reservation.ts @@ -3,4 +3,5 @@ export type Reservation = { startDate: Date; endDate: Date; title: string; + inventoryItemId: number; }; diff --git a/apps/app/src/views/MyReservations.vue b/apps/app/src/views/MyReservations.vue index 1961cbf..9821371 100644 --- a/apps/app/src/views/MyReservations.vue +++ b/apps/app/src/views/MyReservations.vue @@ -6,6 +6,15 @@ :items="reservations" :items-length="reservations.length" :loading="isLoading"> + @@ -13,11 +22,13 @@ From d684d8390fee17c0b943b5104f6255092d123f01 Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Thu, 25 Apr 2024 02:02:31 +0200 Subject: [PATCH 02/15] Added delete reservation logic --- .../api/http/reservation.controller.ts | 33 ++++++++++ .../commands/delete-reservation-command.ts | 66 +++++++++++++++++++ .../dto/delete-reservation-payload.dto.ts | 8 +++ .../reservation-delete.exception.ts | 6 ++ .../api/src/reservation/reservation.module.ts | 2 + 5 files changed, 115 insertions(+) create mode 100644 apps/api/src/reservation/application/commands/delete-reservation-command.ts create mode 100644 apps/api/src/reservation/application/dto/delete-reservation-payload.dto.ts create mode 100644 apps/api/src/reservation/core/exceptions/reservation-delete.exception.ts diff --git a/apps/api/src/reservation/api/http/reservation.controller.ts b/apps/api/src/reservation/api/http/reservation.controller.ts index 21c7330..8a78e42 100644 --- a/apps/api/src/reservation/api/http/reservation.controller.ts +++ b/apps/api/src/reservation/api/http/reservation.controller.ts @@ -1,8 +1,10 @@ import { OptimisticLockError } from '@mikro-orm/core'; import { + BadRequestException, Body, ConflictException, Controller, + Delete, Get, Post, Req, @@ -13,9 +15,12 @@ import { CommandBus, QueryBus } from '@nestjs/cqrs'; import { ApiBearerAuth, ApiBody, ApiResponse, ApiTags } from '@nestjs/swagger'; import { Request } from 'express'; import { CreateReservationCommand } from 'reservation/application/commands/create-reservation.command'; +import { DeleteReservationCommand } from 'reservation/application/commands/delete-reservation-command'; import { CreateReservationPayload } from 'reservation/application/dto/create-reservation-payload.dto'; +import { DeleteReservationPayload } from 'reservation/application/dto/delete-reservation-payload.dto'; import { GetUserReservationsResult } from 'reservation/application/dto/get-user-reservations-result.dto'; import { GetUserReservationsQuery } from 'reservation/application/queries/get-user-reservations.query'; +import { ReservationDeleteException } from 'reservation/core/exceptions/reservation-delete.exception'; import { ReservationOverlapException } from 'reservation/core/exceptions/reservation-overlap.exception'; import { AuthGuard } from 'shared/auth/auth.guard'; import { Permissions } from 'shared/auth/permission.decorator'; @@ -68,6 +73,34 @@ export class ReservationController { } } + @ApiBody({ type: DeleteReservationPayload }) + @ApiResponse({ + status: 200, + description: 'The reservation record has been successfully deleted.', + }) + @ApiResponse({ + status: 400, + description: 'The payload is invalid.', + }) + @Permissions('delete:reservations') + @Delete() + async delete(@Req() req: Request, @Body() body: any): Promise { + await this.commandBus + .execute( + new DeleteReservationCommand( + body.inventoryItemId, + body.reservationId, + req.userId!, + ), + ) + .catch((err) => { + if (err instanceof ReservationDeleteException) { + throw new BadRequestException(err.message); + } + throw err; + }); + } + @Get() @ApiResponse({ status: 200, diff --git a/apps/api/src/reservation/application/commands/delete-reservation-command.ts b/apps/api/src/reservation/application/commands/delete-reservation-command.ts new file mode 100644 index 0000000..ae734bd --- /dev/null +++ b/apps/api/src/reservation/application/commands/delete-reservation-command.ts @@ -0,0 +1,66 @@ +import { + CreateRequestContext, + EntityManager, + EntityRepository, +} from '@mikro-orm/postgresql'; +import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; +import { InjectPinoLogger, PinoLogger } from 'nestjs-pino'; +import { ReservationDeleteException } from 'reservation/core/exceptions/reservation-delete.exception'; +import { EntityNotFoundException } from 'shared/exceptions/entity-not-found.exception'; +import { idSchema, userIdSchema } from 'reservation/core/validation'; +import { InjectRepository } from '@mikro-orm/nestjs'; +import { InventoryItem } from 'reservation/core/entities/inventory-item.entity'; +import { MikroORM } from '@mikro-orm/core'; +import { z } from 'zod'; + +export class DeleteReservationCommand { + constructor( + public readonly inventoryItemId: number, + public readonly reservationId: number, + public readonly userId: number, + ) {} +} + +const deleteReservationSchema = z.object({ + inventoryItemId: idSchema, + reservationId: idSchema, + userId: userIdSchema, +}); + +@CommandHandler(DeleteReservationCommand) +export class DeleteReservationHandler + implements ICommandHandler +{ + constructor( + @InjectRepository(InventoryItem) + private repository: EntityRepository, + private em: EntityManager, + private orm: MikroORM, + @InjectPinoLogger(DeleteReservationHandler.name) + private readonly logger: PinoLogger, + ) {} + + @CreateRequestContext() + async execute(command: DeleteReservationCommand): Promise { + this.logger.info({ command }, 'Executing DeleteReservationCommand'); + + const { userId, inventoryItemId, reservationId } = + await deleteReservationSchema.parseAsync(command); + + const entity = await this.repository.findOne({ id: inventoryItemId }); + if (!entity) throw new EntityNotFoundException('InventoryItem'); + + const reservation = entity.reservations.find( + (reservation) => reservation.id === reservationId, + ); + + if (!reservation) throw new EntityNotFoundException('Reservation'); + + if (reservation.userId !== userId) + throw new ReservationDeleteException( + 'You can only delete your reservation', + ); + + await this.em.remove(reservation).flush(); + } +} diff --git a/apps/api/src/reservation/application/dto/delete-reservation-payload.dto.ts b/apps/api/src/reservation/application/dto/delete-reservation-payload.dto.ts new file mode 100644 index 0000000..9d27393 --- /dev/null +++ b/apps/api/src/reservation/application/dto/delete-reservation-payload.dto.ts @@ -0,0 +1,8 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class DeleteReservationPayload { + @ApiProperty() + inventoryItemId: string; + @ApiProperty() + reservationId: string; +} diff --git a/apps/api/src/reservation/core/exceptions/reservation-delete.exception.ts b/apps/api/src/reservation/core/exceptions/reservation-delete.exception.ts new file mode 100644 index 0000000..f53f4c0 --- /dev/null +++ b/apps/api/src/reservation/core/exceptions/reservation-delete.exception.ts @@ -0,0 +1,6 @@ +export class ReservationDeleteException extends Error { + constructor(message: string) { + super(message); + this.name = this.constructor.name; + } +} diff --git a/apps/api/src/reservation/reservation.module.ts b/apps/api/src/reservation/reservation.module.ts index 382ef2b..a8f584e 100644 --- a/apps/api/src/reservation/reservation.module.ts +++ b/apps/api/src/reservation/reservation.module.ts @@ -11,9 +11,11 @@ import { InventoryItem } from './core/entities/inventory-item.entity'; import { GetInventoryItemsHandler } from './application/queries/get-inventory-items.query'; import { UpsertInventoryItemHandler } from './application/commands/upsert-inventory-items.command'; import { UpsertLocationHandler } from './application/commands/upsert-location.command'; +import { DeleteReservationHandler } from './application/commands/delete-reservation-command'; const commands = [ CreateReservationHandler, + DeleteReservationHandler, UpsertInventoryItemHandler, UpsertLocationHandler, ]; From 5e4c8ce4f30b7166f6c5da2b954c6e3f64dbdfb3 Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Thu, 25 Apr 2024 02:11:19 +0200 Subject: [PATCH 03/15] Added visual update on delete reservation --- apps/app/src/views/MyReservations.vue | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/apps/app/src/views/MyReservations.vue b/apps/app/src/views/MyReservations.vue index 9821371..ba3f1ef 100644 --- a/apps/app/src/views/MyReservations.vue +++ b/apps/app/src/views/MyReservations.vue @@ -12,7 +12,7 @@ @@ -67,14 +67,20 @@ const deleteReservation = (reservation: Reservation) => { }; const deleteReservationConfirm = (reservation: Reservation) => { - reservationsApi.deleteReservation({ - inventoryItemId: reservation.inventoryItemId, - reservationId: reservation.id, - }); - cancelDelete(); + reservationsApi + .deleteReservation({ + inventoryItemId: reservation.inventoryItemId, + reservationId: reservation.id, + }) + .then(() => { + reservations.value = reservations.value.filter( + res => res.id !== reservation.id, + ); + }) + .finally(() => closeDeleteModal()); }; -const cancelDelete = () => { +const closeDeleteModal = () => { selectedReservation.value = null; }; From d8df5319ebdaec7d52f5889f47c28110707f992a Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Thu, 25 Apr 2024 10:09:06 +0200 Subject: [PATCH 04/15] Moved delete logic to entity --- .../api/http/reservation.controller.ts | 8 ++++++-- .../commands/delete-reservation-command.ts | 14 ++------------ .../core/entities/inventory-item.entity.ts | 17 +++++++++++++++++ 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/apps/api/src/reservation/api/http/reservation.controller.ts b/apps/api/src/reservation/api/http/reservation.controller.ts index 8a78e42..bee3f22 100644 --- a/apps/api/src/reservation/api/http/reservation.controller.ts +++ b/apps/api/src/reservation/api/http/reservation.controller.ts @@ -1,10 +1,10 @@ import { OptimisticLockError } from '@mikro-orm/core'; import { - BadRequestException, Body, ConflictException, Controller, Delete, + ForbiddenException, Get, Post, Req, @@ -82,6 +82,10 @@ export class ReservationController { status: 400, description: 'The payload is invalid.', }) + @ApiResponse({ + status: 403, + description: 'The user is forbidden from deleting reservation.', + }) @Permissions('delete:reservations') @Delete() async delete(@Req() req: Request, @Body() body: any): Promise { @@ -95,7 +99,7 @@ export class ReservationController { ) .catch((err) => { if (err instanceof ReservationDeleteException) { - throw new BadRequestException(err.message); + throw new ForbiddenException(err.message); } throw err; }); diff --git a/apps/api/src/reservation/application/commands/delete-reservation-command.ts b/apps/api/src/reservation/application/commands/delete-reservation-command.ts index ae734bd..bacc5bf 100644 --- a/apps/api/src/reservation/application/commands/delete-reservation-command.ts +++ b/apps/api/src/reservation/application/commands/delete-reservation-command.ts @@ -5,7 +5,6 @@ import { } from '@mikro-orm/postgresql'; import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; import { InjectPinoLogger, PinoLogger } from 'nestjs-pino'; -import { ReservationDeleteException } from 'reservation/core/exceptions/reservation-delete.exception'; import { EntityNotFoundException } from 'shared/exceptions/entity-not-found.exception'; import { idSchema, userIdSchema } from 'reservation/core/validation'; import { InjectRepository } from '@mikro-orm/nestjs'; @@ -50,17 +49,8 @@ export class DeleteReservationHandler const entity = await this.repository.findOne({ id: inventoryItemId }); if (!entity) throw new EntityNotFoundException('InventoryItem'); - const reservation = entity.reservations.find( - (reservation) => reservation.id === reservationId, - ); + entity.deleteReservation(reservationId, userId); - if (!reservation) throw new EntityNotFoundException('Reservation'); - - if (reservation.userId !== userId) - throw new ReservationDeleteException( - 'You can only delete your reservation', - ); - - await this.em.remove(reservation).flush(); + await this.em.flush(); } } diff --git a/apps/api/src/reservation/core/entities/inventory-item.entity.ts b/apps/api/src/reservation/core/entities/inventory-item.entity.ts index 25c3687..0dbfa64 100644 --- a/apps/api/src/reservation/core/entities/inventory-item.entity.ts +++ b/apps/api/src/reservation/core/entities/inventory-item.entity.ts @@ -11,6 +11,8 @@ import { AggregateRoot } from 'shared/database/aggregate-root'; import { Reservation } from './reservation.entity'; import { ReservationCreatedEvent } from '../events/reservation-created.event'; import { ReservationOverlapException } from '../exceptions/reservation-overlap.exception'; +import { EntityNotFoundException } from 'shared/exceptions/entity-not-found.exception'; +import { ReservationDeleteException } from '../exceptions/reservation-delete.exception'; export const InventoryType = { ROOM: 'room', @@ -106,4 +108,19 @@ export class InventoryItem extends AggregateRoot { this.data = data; this.locationId = locationId; } + + deleteReservation(reservationId: number, userId: number) { + const reservation = this.reservations.find( + (res) => res.id === reservationId, + ); + + if (!reservation) throw new EntityNotFoundException('Reservation'); + + if (reservation.userId !== userId) + throw new ReservationDeleteException( + 'You can only delete your reservation', + ); + + this._reservations.remove(reservation); + } } From 2f612ecdea0a47fe8dbef669e941b2c028b89f52 Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Thu, 25 Apr 2024 16:24:37 +0200 Subject: [PATCH 05/15] Add actions table for reservation create and delete actions --- apps/api/src/action/action.entity.ts | 26 +++++++++++++++++ apps/api/src/action/action.module.ts | 14 +++++++++ apps/api/src/action/action.service.ts | 21 ++++++++++++++ .../reservation-created-event.handler.ts | 26 +++++++++++++++++ .../reservation-deleted-event.handler.ts | 26 +++++++++++++++++ apps/api/src/app.module.ts | 2 ++ .../20240425132148-create-action-table.ts | 29 +++++++++++++++++++ apps/api/src/shared/event/messaging.ts | 8 +++++ 8 files changed, 152 insertions(+) create mode 100644 apps/api/src/action/action.entity.ts create mode 100644 apps/api/src/action/action.module.ts create mode 100644 apps/api/src/action/action.service.ts create mode 100644 apps/api/src/action/api/event-handlers/reservation-created-event.handler.ts create mode 100644 apps/api/src/action/api/event-handlers/reservation-deleted-event.handler.ts create mode 100644 apps/api/src/shared/database/migrations/20240425132148-create-action-table.ts diff --git a/apps/api/src/action/action.entity.ts b/apps/api/src/action/action.entity.ts new file mode 100644 index 0000000..ac3ef13 --- /dev/null +++ b/apps/api/src/action/action.entity.ts @@ -0,0 +1,26 @@ +import { Entity, Enum, Property } from '@mikro-orm/core'; +import { AggregateRoot } from 'shared/database/aggregate-root'; + +export const Type = { + RESERVATION_CREATED: 'reservation.created', + RESERVATION_DELETED: 'reservation.deleted', +} as const; + +export type Type = (typeof Type)[keyof typeof Type]; + +@Entity({ tableName: 'action' }) +export class Action extends AggregateRoot { + @Enum(() => Type) + readonly type: Type; + + @Property() + userId: number; + + constructor(userId: number, createdAt: Date, type: Type) { + super(); + this.type = type; + this.userId = userId; + this.createdAt = createdAt; + this.updatedAt = createdAt; + } +} diff --git a/apps/api/src/action/action.module.ts b/apps/api/src/action/action.module.ts new file mode 100644 index 0000000..0bf90f1 --- /dev/null +++ b/apps/api/src/action/action.module.ts @@ -0,0 +1,14 @@ +import { ReservationCreatedHandler } from './api/event-handlers/reservation-created-event.handler'; +import { ReservationDeletedHandler } from './api/event-handlers/reservation-deleted-event.handler'; +import { MikroOrmModule } from '@mikro-orm/nestjs'; +import { ActionService } from './action.service'; +import { Action } from './action.entity'; +import { Module } from '@nestjs/common'; + +const eventHandlers = [ReservationCreatedHandler, ReservationDeletedHandler]; + +@Module({ + imports: [MikroOrmModule.forFeature([Action])], + providers: [ActionService, ...eventHandlers], +}) +export class ActionModule {} diff --git a/apps/api/src/action/action.service.ts b/apps/api/src/action/action.service.ts new file mode 100644 index 0000000..4209ec0 --- /dev/null +++ b/apps/api/src/action/action.service.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@nestjs/common'; +import { Action, Type } from './action.entity'; +import { EntityManager } from '@mikro-orm/core'; + +@Injectable() +export class ActionService { + constructor(private em: EntityManager) {} + + async createAction({ + userId, + occurredOn, + type, + }: { + userId: number; + occurredOn: Date; + type: Type; + }) { + const action = new Action(userId, occurredOn, type); + this.em.persistAndFlush(action); + } +} diff --git a/apps/api/src/action/api/event-handlers/reservation-created-event.handler.ts b/apps/api/src/action/api/event-handlers/reservation-created-event.handler.ts new file mode 100644 index 0000000..588cdb0 --- /dev/null +++ b/apps/api/src/action/api/event-handlers/reservation-created-event.handler.ts @@ -0,0 +1,26 @@ +import { Injectable } from '@nestjs/common'; +import { OnEvent } from '@nestjs/event-emitter'; +import { ReservationCreated, Topic } from 'shared/event/messaging'; +import { InjectPinoLogger, PinoLogger } from 'nestjs-pino'; +import { ActionService } from 'action/action.service'; +import { Type } from 'action/action.entity'; + +@Injectable() +export class ReservationCreatedHandler { + constructor( + @InjectPinoLogger(ReservationCreatedHandler.name) + private readonly logger: PinoLogger, + private actionService: ActionService, + ) {} + + @OnEvent(Topic.RESERVATION_CREATED, { async: true }) + async handle(payload: ReservationCreated) { + this.logger.info({ payload }, 'Executing ReservationCreatedHandler'); + const { userId, occurredOn } = payload; + await this.actionService.createAction({ + userId, + occurredOn, + type: Type.RESERVATION_CREATED, + }); + } +} diff --git a/apps/api/src/action/api/event-handlers/reservation-deleted-event.handler.ts b/apps/api/src/action/api/event-handlers/reservation-deleted-event.handler.ts new file mode 100644 index 0000000..d90b130 --- /dev/null +++ b/apps/api/src/action/api/event-handlers/reservation-deleted-event.handler.ts @@ -0,0 +1,26 @@ +import { Injectable } from '@nestjs/common'; +import { OnEvent } from '@nestjs/event-emitter'; +import { ReservationDeleted, Topic } from 'shared/event/messaging'; +import { InjectPinoLogger, PinoLogger } from 'nestjs-pino'; +import { ActionService } from 'action/action.service'; +import { Type } from 'action/action.entity'; + +@Injectable() +export class ReservationDeletedHandler { + constructor( + @InjectPinoLogger(ReservationDeletedHandler.name) + private readonly logger: PinoLogger, + private actionService: ActionService, + ) {} + + @OnEvent(Topic.RESERVATION_DELETED, { async: true }) + async handle(payload: ReservationDeleted) { + this.logger.info({ payload }, 'Executing ReservationDeletedHandler'); + const { userId, occurredOn } = payload; + await this.actionService.createAction({ + userId, + occurredOn, + type: Type.RESERVATION_DELETED, + }); + } +} diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts index 30c2c83..e776c3f 100644 --- a/apps/api/src/app.module.ts +++ b/apps/api/src/app.module.ts @@ -16,6 +16,7 @@ import { ReservationModule } from './reservation/reservation.module'; import { NotificationModule } from './notification/notification.module'; import mailConfig from 'config/mail.config'; import cmsConfig from 'config/cms.config'; +import { ActionModule } from 'action/action.module'; @Module({ imports: [ @@ -39,6 +40,7 @@ import cmsConfig from 'config/cms.config'; AuthModule, ReservationModule, NotificationModule, + ActionModule, ], controllers: [HealthController], }) diff --git a/apps/api/src/shared/database/migrations/20240425132148-create-action-table.ts b/apps/api/src/shared/database/migrations/20240425132148-create-action-table.ts new file mode 100644 index 0000000..f05ede1 --- /dev/null +++ b/apps/api/src/shared/database/migrations/20240425132148-create-action-table.ts @@ -0,0 +1,29 @@ +import { Migration } from '@mikro-orm/migrations'; + +const TABLE_NAME = 'action'; +const types = ['reservation.created', 'reservation.deleted']; + +export class CreateReservationTable extends Migration { + async up(): Promise { + const knex = this.getKnex(); + const createActionTable = knex.schema.createTable(TABLE_NAME, (table) => { + table.increments('id').primary(); + table.enum('type', types); + table.integer('user_id').notNullable(); + table.foreign('user_id').references('user.id').onDelete('CASCADE'); + table + .timestamp('created_at', { useTz: true }) + .notNullable() + .defaultTo(knex.fn.now()); + table + .timestamp('updated_at', { useTz: true }) + .notNullable() + .defaultTo(knex.fn.now()); + }); + this.addSql(createActionTable.toQuery()); + } + + async down(): Promise { + this.addSql(this.getKnex().schema.dropTable(TABLE_NAME).toQuery()); + } +} diff --git a/apps/api/src/shared/event/messaging.ts b/apps/api/src/shared/event/messaging.ts index f0228ec..5054666 100644 --- a/apps/api/src/shared/event/messaging.ts +++ b/apps/api/src/shared/event/messaging.ts @@ -1,6 +1,7 @@ export const Topic = { USER_CREATED: 'user.created', RESERVATION_CREATED: 'reservation.created', + RESERVATION_DELETED: 'reservation.deleted', } as const; interface IIntegrationEvent { @@ -25,3 +26,10 @@ export class ReservationCreated implements IIntegrationEvent { readonly occurredOn: Date, ) {} } + +export class ReservationDeleted implements IIntegrationEvent { + constructor( + readonly userId: number, + readonly occurredOn: Date, + ) {} +} From 43acc452172024e2e4a7aa74886ddb51a49ed35c Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Thu, 25 Apr 2024 16:59:50 +0200 Subject: [PATCH 06/15] Added delete reservation integratione event --- .../commands/delete-reservation-command.ts | 9 ++++++- ...ation-deleted-integration-event.handler.ts | 24 +++++++++++++++++++ .../core/entities/inventory-item.entity.ts | 2 ++ .../core/events/reservation-deleted.event.ts | 7 ++++++ .../api/src/reservation/reservation.module.ts | 6 ++++- 5 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 apps/api/src/reservation/application/event-handlers/send-reservation-deleted-integration-event.handler.ts create mode 100644 apps/api/src/reservation/core/events/reservation-deleted.event.ts diff --git a/apps/api/src/reservation/application/commands/delete-reservation-command.ts b/apps/api/src/reservation/application/commands/delete-reservation-command.ts index bacc5bf..9b7c210 100644 --- a/apps/api/src/reservation/application/commands/delete-reservation-command.ts +++ b/apps/api/src/reservation/application/commands/delete-reservation-command.ts @@ -11,6 +11,8 @@ import { InjectRepository } from '@mikro-orm/nestjs'; import { InventoryItem } from 'reservation/core/entities/inventory-item.entity'; import { MikroORM } from '@mikro-orm/core'; import { z } from 'zod'; +import { IDomainEventPublisher } from 'shared/event/domain-event-publisher'; +import { Inject } from '@nestjs/common'; export class DeleteReservationCommand { constructor( @@ -31,6 +33,8 @@ export class DeleteReservationHandler implements ICommandHandler { constructor( + @Inject(IDomainEventPublisher) + private eventPublisher: IDomainEventPublisher, @InjectRepository(InventoryItem) private repository: EntityRepository, private em: EntityManager, @@ -49,8 +53,11 @@ export class DeleteReservationHandler const entity = await this.repository.findOne({ id: inventoryItemId }); if (!entity) throw new EntityNotFoundException('InventoryItem'); - entity.deleteReservation(reservationId, userId); + const reservation = this.eventPublisher.mergeObjectContext(entity); + + reservation.deleteReservation(reservationId, userId); await this.em.flush(); + reservation.commit(); } } diff --git a/apps/api/src/reservation/application/event-handlers/send-reservation-deleted-integration-event.handler.ts b/apps/api/src/reservation/application/event-handlers/send-reservation-deleted-integration-event.handler.ts new file mode 100644 index 0000000..0bdc0c8 --- /dev/null +++ b/apps/api/src/reservation/application/event-handlers/send-reservation-deleted-integration-event.handler.ts @@ -0,0 +1,24 @@ +import { EventsHandler, IEventHandler } from '@nestjs/cqrs'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { InjectPinoLogger, PinoLogger } from 'nestjs-pino'; +import { ReservationDeletedEvent } from 'reservation/core/events/reservation-deleted.event'; +import { ReservationDeleted, Topic } from 'shared/event/messaging'; + +@EventsHandler(ReservationDeletedEvent) +export class SendReservationDeletedIntegrationEventHandler + implements IEventHandler +{ + constructor( + private eventEmitter: EventEmitter2, + @InjectPinoLogger(SendReservationDeletedIntegrationEventHandler.name) + private readonly logger: PinoLogger, + ) {} + + handle(event: ReservationDeletedEvent) { + this.logger.info({ event }, 'Sending ReservationDeleted integration event'); + this.eventEmitter.emit( + Topic.RESERVATION_DELETED, + new ReservationDeleted(event.userId, event.createdAt), + ); + } +} diff --git a/apps/api/src/reservation/core/entities/inventory-item.entity.ts b/apps/api/src/reservation/core/entities/inventory-item.entity.ts index 0dbfa64..8d29349 100644 --- a/apps/api/src/reservation/core/entities/inventory-item.entity.ts +++ b/apps/api/src/reservation/core/entities/inventory-item.entity.ts @@ -10,6 +10,7 @@ import { isAfter, isBefore, isEqual } from 'date-fns'; import { AggregateRoot } from 'shared/database/aggregate-root'; import { Reservation } from './reservation.entity'; import { ReservationCreatedEvent } from '../events/reservation-created.event'; +import { ReservationDeletedEvent } from '../events/reservation-deleted.event'; import { ReservationOverlapException } from '../exceptions/reservation-overlap.exception'; import { EntityNotFoundException } from 'shared/exceptions/entity-not-found.exception'; import { ReservationDeleteException } from '../exceptions/reservation-delete.exception'; @@ -122,5 +123,6 @@ export class InventoryItem extends AggregateRoot { ); this._reservations.remove(reservation); + this.apply(new ReservationDeletedEvent(userId)); } } diff --git a/apps/api/src/reservation/core/events/reservation-deleted.event.ts b/apps/api/src/reservation/core/events/reservation-deleted.event.ts new file mode 100644 index 0000000..79e2f53 --- /dev/null +++ b/apps/api/src/reservation/core/events/reservation-deleted.event.ts @@ -0,0 +1,7 @@ +import { BaseEvent } from 'shared/event/base.event'; + +export class ReservationDeletedEvent extends BaseEvent { + constructor(public readonly userId: number) { + super(); + } +} diff --git a/apps/api/src/reservation/reservation.module.ts b/apps/api/src/reservation/reservation.module.ts index a8f584e..77d3ac9 100644 --- a/apps/api/src/reservation/reservation.module.ts +++ b/apps/api/src/reservation/reservation.module.ts @@ -12,6 +12,7 @@ import { GetInventoryItemsHandler } from './application/queries/get-inventory-it import { UpsertInventoryItemHandler } from './application/commands/upsert-inventory-items.command'; import { UpsertLocationHandler } from './application/commands/upsert-location.command'; import { DeleteReservationHandler } from './application/commands/delete-reservation-command'; +import { SendReservationDeletedIntegrationEventHandler } from './application/event-handlers/send-reservation-deleted-integration-event.handler'; const commands = [ CreateReservationHandler, @@ -20,7 +21,10 @@ const commands = [ UpsertLocationHandler, ]; const queries = [GetUserReservationsHandler, GetInventoryItemsHandler]; -const eventHandlers = [SendReservationCreatedIntegrationEventHandler]; +const eventHandlers = [ + SendReservationCreatedIntegrationEventHandler, + SendReservationDeletedIntegrationEventHandler, +]; @Module({ imports: [MikroOrmModule.forFeature([Reservation, Location, InventoryItem])], From cea88eeadaae65885022974430f123cb3b476386 Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Mon, 29 Apr 2024 11:50:46 +0200 Subject: [PATCH 07/15] Renamed action to activity --- .../action.entity.ts => activity/activity.entity.ts} | 4 ++-- .../action.module.ts => activity/activity.module.ts} | 10 +++++----- .../action.service.ts => activity/activity.service.ts} | 10 +++++----- .../reservation-created-event.handler.ts | 8 ++++---- .../reservation-deleted-event.handler.ts | 8 ++++---- apps/api/src/app.module.ts | 4 ++-- ...able.ts => 20240425132148-create-activity-table.ts} | 6 +++--- 7 files changed, 25 insertions(+), 25 deletions(-) rename apps/api/src/{action/action.entity.ts => activity/activity.entity.ts} (87%) rename apps/api/src/{action/action.module.ts => activity/activity.module.ts} (62%) rename apps/api/src/{action/action.service.ts => activity/activity.service.ts} (56%) rename apps/api/src/{action => activity}/api/event-handlers/reservation-created-event.handler.ts (77%) rename apps/api/src/{action => activity}/api/event-handlers/reservation-deleted-event.handler.ts (77%) rename apps/api/src/shared/database/migrations/{20240425132148-create-action-table.ts => 20240425132148-create-activity-table.ts} (83%) diff --git a/apps/api/src/action/action.entity.ts b/apps/api/src/activity/activity.entity.ts similarity index 87% rename from apps/api/src/action/action.entity.ts rename to apps/api/src/activity/activity.entity.ts index ac3ef13..c185e8f 100644 --- a/apps/api/src/action/action.entity.ts +++ b/apps/api/src/activity/activity.entity.ts @@ -8,8 +8,8 @@ export const Type = { export type Type = (typeof Type)[keyof typeof Type]; -@Entity({ tableName: 'action' }) -export class Action extends AggregateRoot { +@Entity({ tableName: 'activity' }) +export class Activity extends AggregateRoot { @Enum(() => Type) readonly type: Type; diff --git a/apps/api/src/action/action.module.ts b/apps/api/src/activity/activity.module.ts similarity index 62% rename from apps/api/src/action/action.module.ts rename to apps/api/src/activity/activity.module.ts index 0bf90f1..1843836 100644 --- a/apps/api/src/action/action.module.ts +++ b/apps/api/src/activity/activity.module.ts @@ -1,14 +1,14 @@ import { ReservationCreatedHandler } from './api/event-handlers/reservation-created-event.handler'; import { ReservationDeletedHandler } from './api/event-handlers/reservation-deleted-event.handler'; import { MikroOrmModule } from '@mikro-orm/nestjs'; -import { ActionService } from './action.service'; -import { Action } from './action.entity'; +import { ActivityService } from './activity.service'; +import { Activity } from './activity.entity'; import { Module } from '@nestjs/common'; const eventHandlers = [ReservationCreatedHandler, ReservationDeletedHandler]; @Module({ - imports: [MikroOrmModule.forFeature([Action])], - providers: [ActionService, ...eventHandlers], + imports: [MikroOrmModule.forFeature([Activity])], + providers: [ActivityService, ...eventHandlers], }) -export class ActionModule {} +export class ActivityModule {} diff --git a/apps/api/src/action/action.service.ts b/apps/api/src/activity/activity.service.ts similarity index 56% rename from apps/api/src/action/action.service.ts rename to apps/api/src/activity/activity.service.ts index 4209ec0..095dc8b 100644 --- a/apps/api/src/action/action.service.ts +++ b/apps/api/src/activity/activity.service.ts @@ -1,12 +1,12 @@ import { Injectable } from '@nestjs/common'; -import { Action, Type } from './action.entity'; +import { Activity, Type } from './activity.entity'; import { EntityManager } from '@mikro-orm/core'; @Injectable() -export class ActionService { +export class ActivityService { constructor(private em: EntityManager) {} - async createAction({ + async createActivity({ userId, occurredOn, type, @@ -15,7 +15,7 @@ export class ActionService { occurredOn: Date; type: Type; }) { - const action = new Action(userId, occurredOn, type); - this.em.persistAndFlush(action); + const activity = new Activity(userId, occurredOn, type); + this.em.persistAndFlush(activity); } } diff --git a/apps/api/src/action/api/event-handlers/reservation-created-event.handler.ts b/apps/api/src/activity/api/event-handlers/reservation-created-event.handler.ts similarity index 77% rename from apps/api/src/action/api/event-handlers/reservation-created-event.handler.ts rename to apps/api/src/activity/api/event-handlers/reservation-created-event.handler.ts index 588cdb0..6ba0cef 100644 --- a/apps/api/src/action/api/event-handlers/reservation-created-event.handler.ts +++ b/apps/api/src/activity/api/event-handlers/reservation-created-event.handler.ts @@ -2,22 +2,22 @@ import { Injectable } from '@nestjs/common'; import { OnEvent } from '@nestjs/event-emitter'; import { ReservationCreated, Topic } from 'shared/event/messaging'; import { InjectPinoLogger, PinoLogger } from 'nestjs-pino'; -import { ActionService } from 'action/action.service'; -import { Type } from 'action/action.entity'; +import { ActivityService } from 'activity/activity.service'; +import { Type } from 'activity/activity.entity'; @Injectable() export class ReservationCreatedHandler { constructor( @InjectPinoLogger(ReservationCreatedHandler.name) private readonly logger: PinoLogger, - private actionService: ActionService, + private activityService: ActivityService, ) {} @OnEvent(Topic.RESERVATION_CREATED, { async: true }) async handle(payload: ReservationCreated) { this.logger.info({ payload }, 'Executing ReservationCreatedHandler'); const { userId, occurredOn } = payload; - await this.actionService.createAction({ + await this.activityService.createActivity({ userId, occurredOn, type: Type.RESERVATION_CREATED, diff --git a/apps/api/src/action/api/event-handlers/reservation-deleted-event.handler.ts b/apps/api/src/activity/api/event-handlers/reservation-deleted-event.handler.ts similarity index 77% rename from apps/api/src/action/api/event-handlers/reservation-deleted-event.handler.ts rename to apps/api/src/activity/api/event-handlers/reservation-deleted-event.handler.ts index d90b130..97f7977 100644 --- a/apps/api/src/action/api/event-handlers/reservation-deleted-event.handler.ts +++ b/apps/api/src/activity/api/event-handlers/reservation-deleted-event.handler.ts @@ -2,22 +2,22 @@ import { Injectable } from '@nestjs/common'; import { OnEvent } from '@nestjs/event-emitter'; import { ReservationDeleted, Topic } from 'shared/event/messaging'; import { InjectPinoLogger, PinoLogger } from 'nestjs-pino'; -import { ActionService } from 'action/action.service'; -import { Type } from 'action/action.entity'; +import { ActivityService } from 'activity/activity.service'; +import { Type } from 'activity/activity.entity'; @Injectable() export class ReservationDeletedHandler { constructor( @InjectPinoLogger(ReservationDeletedHandler.name) private readonly logger: PinoLogger, - private actionService: ActionService, + private activityService: ActivityService, ) {} @OnEvent(Topic.RESERVATION_DELETED, { async: true }) async handle(payload: ReservationDeleted) { this.logger.info({ payload }, 'Executing ReservationDeletedHandler'); const { userId, occurredOn } = payload; - await this.actionService.createAction({ + await this.activityService.createActivity({ userId, occurredOn, type: Type.RESERVATION_DELETED, diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts index e776c3f..08f6b63 100644 --- a/apps/api/src/app.module.ts +++ b/apps/api/src/app.module.ts @@ -16,7 +16,7 @@ import { ReservationModule } from './reservation/reservation.module'; import { NotificationModule } from './notification/notification.module'; import mailConfig from 'config/mail.config'; import cmsConfig from 'config/cms.config'; -import { ActionModule } from 'action/action.module'; +import { ActivityModule } from 'activity/activity.module'; @Module({ imports: [ @@ -40,7 +40,7 @@ import { ActionModule } from 'action/action.module'; AuthModule, ReservationModule, NotificationModule, - ActionModule, + ActivityModule, ], controllers: [HealthController], }) diff --git a/apps/api/src/shared/database/migrations/20240425132148-create-action-table.ts b/apps/api/src/shared/database/migrations/20240425132148-create-activity-table.ts similarity index 83% rename from apps/api/src/shared/database/migrations/20240425132148-create-action-table.ts rename to apps/api/src/shared/database/migrations/20240425132148-create-activity-table.ts index f05ede1..daa88da 100644 --- a/apps/api/src/shared/database/migrations/20240425132148-create-action-table.ts +++ b/apps/api/src/shared/database/migrations/20240425132148-create-activity-table.ts @@ -1,12 +1,12 @@ import { Migration } from '@mikro-orm/migrations'; -const TABLE_NAME = 'action'; +const TABLE_NAME = 'activity'; const types = ['reservation.created', 'reservation.deleted']; export class CreateReservationTable extends Migration { async up(): Promise { const knex = this.getKnex(); - const createActionTable = knex.schema.createTable(TABLE_NAME, (table) => { + const createActivityTable = knex.schema.createTable(TABLE_NAME, (table) => { table.increments('id').primary(); table.enum('type', types); table.integer('user_id').notNullable(); @@ -20,7 +20,7 @@ export class CreateReservationTable extends Migration { .notNullable() .defaultTo(knex.fn.now()); }); - this.addSql(createActionTable.toQuery()); + this.addSql(createActivityTable.toQuery()); } async down(): Promise { From 3dd0dc01f1424f1a9f1ecf2c73abe9fd284373db Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Tue, 30 Apr 2024 10:18:49 +0200 Subject: [PATCH 08/15] Added websocket message send backend --- apps/api/package.json | 3 + apps/api/pnpm-lock.yaml | 157 +++++++++++++++++- apps/api/src/app.module.ts | 2 + ...ge-on-reservation-created-event.handler.ts | 27 +++ ...ge-on-reservation-deleted-event.handler.ts | 27 +++ apps/api/src/websocket/event-type.ts | 3 + .../websocket/websocket-provider.service.ts | 12 ++ apps/api/src/websocket/websocket.module.ts | 15 ++ 8 files changed, 238 insertions(+), 8 deletions(-) create mode 100644 apps/api/src/websocket/api/event-handlers/send-message-on-reservation-created-event.handler.ts create mode 100644 apps/api/src/websocket/api/event-handlers/send-message-on-reservation-deleted-event.handler.ts create mode 100644 apps/api/src/websocket/event-type.ts create mode 100644 apps/api/src/websocket/websocket-provider.service.ts create mode 100644 apps/api/src/websocket/websocket.module.ts diff --git a/apps/api/package.json b/apps/api/package.json index ecd7d28..0a96473 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -60,7 +60,9 @@ "@nestjs/cqrs": "^10.2.7", "@nestjs/event-emitter": "^2.0.4", "@nestjs/platform-express": "^10.0.0", + "@nestjs/platform-socket.io": "^10.3.8", "@nestjs/swagger": "^7.3.0", + "@nestjs/websockets": "^10.3.8", "@sanity/client": "^6.15.3", "@sanity/webhook": "^4.0.2", "auth0": "^4.3.1", @@ -76,6 +78,7 @@ "radash": "^12.0.0", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.1", + "socket.io": "^4.7.5", "zod": "^3.22.4" }, "devDependencies": { diff --git a/apps/api/pnpm-lock.yaml b/apps/api/pnpm-lock.yaml index ebd7e25..da2ad8f 100644 --- a/apps/api/pnpm-lock.yaml +++ b/apps/api/pnpm-lock.yaml @@ -34,7 +34,7 @@ dependencies: version: 3.2.0(@nestjs/common@10.0.0)(rxjs@7.8.1) '@nestjs/core': specifier: ^10.0.0 - version: 10.0.0(@nestjs/common@10.0.0)(@nestjs/platform-express@10.0.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + version: 10.0.0(@nestjs/common@10.0.0)(@nestjs/platform-express@10.0.0)(@nestjs/websockets@10.3.8)(reflect-metadata@0.1.13)(rxjs@7.8.1) '@nestjs/cqrs': specifier: ^10.2.7 version: 10.2.7(@nestjs/common@10.0.0)(@nestjs/core@10.0.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) @@ -44,9 +44,15 @@ dependencies: '@nestjs/platform-express': specifier: ^10.0.0 version: 10.0.0(@nestjs/common@10.0.0)(@nestjs/core@10.0.0) + '@nestjs/platform-socket.io': + specifier: ^10.3.8 + version: 10.3.8(@nestjs/common@10.0.0)(@nestjs/websockets@10.3.8)(rxjs@7.8.1) '@nestjs/swagger': specifier: ^7.3.0 version: 7.3.0(@nestjs/common@10.0.0)(@nestjs/core@10.0.0)(reflect-metadata@0.1.13) + '@nestjs/websockets': + specifier: ^10.3.8 + version: 10.3.8(@nestjs/common@10.0.0)(@nestjs/core@10.0.0)(@nestjs/platform-socket.io@10.3.8)(reflect-metadata@0.1.13)(rxjs@7.8.1) '@sanity/client': specifier: ^6.15.3 version: 6.15.3 @@ -92,6 +98,9 @@ dependencies: rxjs: specifier: ^7.8.1 version: 7.8.1 + socket.io: + specifier: ^4.7.5 + version: 4.7.5 zod: specifier: ^3.22.4 version: 3.22.4 @@ -1007,7 +1016,7 @@ packages: dependencies: '@mikro-orm/core': 6.1.5 '@nestjs/common': 10.0.0(reflect-metadata@0.1.13)(rxjs@7.8.1) - '@nestjs/core': 10.0.0(@nestjs/common@10.0.0)(@nestjs/platform-express@10.0.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/core': 10.0.0(@nestjs/common@10.0.0)(@nestjs/platform-express@10.0.0)(@nestjs/websockets@10.3.8)(reflect-metadata@0.1.13)(rxjs@7.8.1) dev: false /@mikro-orm/postgresql@6.1.5(@mikro-orm/core@6.1.5): @@ -1117,7 +1126,7 @@ packages: uuid: 9.0.1 dev: false - /@nestjs/core@10.0.0(@nestjs/common@10.0.0)(@nestjs/platform-express@10.0.0)(reflect-metadata@0.1.13)(rxjs@7.8.1): + /@nestjs/core@10.0.0(@nestjs/common@10.0.0)(@nestjs/platform-express@10.0.0)(@nestjs/websockets@10.3.8)(reflect-metadata@0.1.13)(rxjs@7.8.1): resolution: {integrity: sha512-HFTdj4vsF+2qOaq97ZPRDle6Q/KyL5lmMah0/ZR0ie+e1/tnlvmlqw589xFACTemLJFFOjZMy763v+icO9u72w==} requiresBuild: true peerDependencies: @@ -1137,6 +1146,7 @@ packages: dependencies: '@nestjs/common': 10.0.0(reflect-metadata@0.1.13)(rxjs@7.8.1) '@nestjs/platform-express': 10.0.0(@nestjs/common@10.0.0)(@nestjs/core@10.0.0) + '@nestjs/websockets': 10.3.8(@nestjs/common@10.0.0)(@nestjs/core@10.0.0)(@nestjs/platform-socket.io@10.3.8)(reflect-metadata@0.1.13)(rxjs@7.8.1) '@nuxtjs/opencollective': 0.3.2 fast-safe-stringify: 2.1.1 iterare: 1.2.1 @@ -1157,7 +1167,7 @@ packages: rxjs: ^7.2.0 dependencies: '@nestjs/common': 10.0.0(reflect-metadata@0.1.13)(rxjs@7.8.1) - '@nestjs/core': 10.0.0(@nestjs/common@10.0.0)(@nestjs/platform-express@10.0.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/core': 10.0.0(@nestjs/common@10.0.0)(@nestjs/platform-express@10.0.0)(@nestjs/websockets@10.3.8)(reflect-metadata@0.1.13)(rxjs@7.8.1) reflect-metadata: 0.1.13 rxjs: 7.8.1 uuid: 9.0.1 @@ -1170,7 +1180,7 @@ packages: '@nestjs/core': ^8.0.0 || ^9.0.0 || ^10.0.0 dependencies: '@nestjs/common': 10.0.0(reflect-metadata@0.1.13)(rxjs@7.8.1) - '@nestjs/core': 10.0.0(@nestjs/common@10.0.0)(@nestjs/platform-express@10.0.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/core': 10.0.0(@nestjs/common@10.0.0)(@nestjs/platform-express@10.0.0)(@nestjs/websockets@10.3.8)(reflect-metadata@0.1.13)(rxjs@7.8.1) eventemitter2: 6.4.9 dev: false @@ -1198,7 +1208,7 @@ packages: '@nestjs/core': ^10.0.0 dependencies: '@nestjs/common': 10.0.0(reflect-metadata@0.1.13)(rxjs@7.8.1) - '@nestjs/core': 10.0.0(@nestjs/common@10.0.0)(@nestjs/platform-express@10.0.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/core': 10.0.0(@nestjs/common@10.0.0)(@nestjs/platform-express@10.0.0)(@nestjs/websockets@10.3.8)(reflect-metadata@0.1.13)(rxjs@7.8.1) body-parser: 1.20.2 cors: 2.8.5 express: 4.18.2 @@ -1207,6 +1217,23 @@ packages: transitivePeerDependencies: - supports-color + /@nestjs/platform-socket.io@10.3.8(@nestjs/common@10.0.0)(@nestjs/websockets@10.3.8)(rxjs@7.8.1): + resolution: {integrity: sha512-CpsWw/AaJMDTan0FoYHkKyptsrFz8JSnHBGcajvj9UMeKCJ8Hi80T9ymKnP7OozbArKimh1oBusN+k4sKRxRTg==} + peerDependencies: + '@nestjs/common': ^10.0.0 + '@nestjs/websockets': ^10.0.0 + rxjs: ^7.1.0 + dependencies: + '@nestjs/common': 10.0.0(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/websockets': 10.3.8(@nestjs/common@10.0.0)(@nestjs/core@10.0.0)(@nestjs/platform-socket.io@10.3.8)(reflect-metadata@0.1.13)(rxjs@7.8.1) + rxjs: 7.8.1 + socket.io: 4.7.5 + tslib: 2.6.2 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + /@nestjs/schematics@10.0.0(chokidar@3.5.3)(typescript@5.1.3): resolution: {integrity: sha512-gfUy/N1m1paN33BXq4d7HoCM+zM4rFxYjqAb8jkrBfBHiwyEhHHozfX/aRy/kOnAcy/VP8v4Zs4HKKrbRRlHnw==} peerDependencies: @@ -1241,7 +1268,7 @@ packages: dependencies: '@microsoft/tsdoc': 0.14.2 '@nestjs/common': 10.0.0(reflect-metadata@0.1.13)(rxjs@7.8.1) - '@nestjs/core': 10.0.0(@nestjs/common@10.0.0)(@nestjs/platform-express@10.0.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/core': 10.0.0(@nestjs/common@10.0.0)(@nestjs/platform-express@10.0.0)(@nestjs/websockets@10.3.8)(reflect-metadata@0.1.13)(rxjs@7.8.1) '@nestjs/mapped-types': 2.0.5(@nestjs/common@10.0.0)(reflect-metadata@0.1.13) js-yaml: 4.1.0 lodash: 4.17.21 @@ -1264,11 +1291,32 @@ packages: optional: true dependencies: '@nestjs/common': 10.0.0(reflect-metadata@0.1.13)(rxjs@7.8.1) - '@nestjs/core': 10.0.0(@nestjs/common@10.0.0)(@nestjs/platform-express@10.0.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/core': 10.0.0(@nestjs/common@10.0.0)(@nestjs/platform-express@10.0.0)(@nestjs/websockets@10.3.8)(reflect-metadata@0.1.13)(rxjs@7.8.1) '@nestjs/platform-express': 10.0.0(@nestjs/common@10.0.0)(@nestjs/core@10.0.0) tslib: 2.5.3 dev: true + /@nestjs/websockets@10.3.8(@nestjs/common@10.0.0)(@nestjs/core@10.0.0)(@nestjs/platform-socket.io@10.3.8)(reflect-metadata@0.1.13)(rxjs@7.8.1): + resolution: {integrity: sha512-DTSCK+FYtSTljT6XjVUUZhf1cPxKEJf1AG1y2n+ERnd0vzMpnYpMFgGkDlXqa3uC+LAMcOcx1EyTCcHsSHrOVg==} + peerDependencies: + '@nestjs/common': ^10.0.0 + '@nestjs/core': ^10.0.0 + '@nestjs/platform-socket.io': ^10.0.0 + reflect-metadata: ^0.1.12 || ^0.2.0 + rxjs: ^7.1.0 + peerDependenciesMeta: + '@nestjs/platform-socket.io': + optional: true + dependencies: + '@nestjs/common': 10.0.0(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/core': 10.0.0(@nestjs/common@10.0.0)(@nestjs/platform-express@10.0.0)(@nestjs/websockets@10.3.8)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/platform-socket.io': 10.3.8(@nestjs/common@10.0.0)(@nestjs/websockets@10.3.8)(rxjs@7.8.1) + iterare: 1.2.1 + object-hash: 3.0.0 + reflect-metadata: 0.1.13 + rxjs: 7.8.1 + tslib: 2.6.2 + /@nodelib/fs.scandir@2.1.5: resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1381,6 +1429,9 @@ packages: '@sinonjs/commons': 3.0.1 dev: true + /@socket.io/component-emitter@3.1.2: + resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} + /@tsconfig/node10@1.0.9: resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==} dev: true @@ -1443,10 +1494,18 @@ packages: '@types/node': 20.3.1 dev: true + /@types/cookie@0.4.1: + resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==} + /@types/cookiejar@2.1.5: resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==} dev: true + /@types/cors@2.8.17: + resolution: {integrity: sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==} + dependencies: + '@types/node': 20.3.1 + /@types/eslint-scope@3.7.7: resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} dependencies: @@ -2115,6 +2174,10 @@ packages: /base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + /base64id@2.0.0: + resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==} + engines: {node: ^4.5.0 || >= 5.9} + /binary-extensions@2.2.0: resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} engines: {node: '>=8'} @@ -2474,6 +2537,10 @@ packages: /cookie-signature@1.0.6: resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + /cookie@0.4.2: + resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==} + engines: {node: '>= 0.6'} + /cookie@0.5.0: resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} engines: {node: '>= 0.6'} @@ -2698,6 +2765,29 @@ packages: dependencies: once: 1.4.0 + /engine.io-parser@5.2.2: + resolution: {integrity: sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==} + engines: {node: '>=10.0.0'} + + /engine.io@6.5.4: + resolution: {integrity: sha512-KdVSDKhVKyOi+r5uEabrDLZw2qXStVvCsEB/LN3mw4WFi6Gx50jTyuxYVCwAAC0U46FdnzP/ScKRBTXb/NiEOg==} + engines: {node: '>=10.2.0'} + dependencies: + '@types/cookie': 0.4.1 + '@types/cors': 2.8.17 + '@types/node': 20.3.1 + accepts: 1.3.8 + base64id: 2.0.0 + cookie: 0.4.2 + cors: 2.8.5 + debug: 4.3.4 + engine.io-parser: 5.2.2 + ws: 8.11.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + /enhanced-resolve@5.15.0: resolution: {integrity: sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==} engines: {node: '>=10.13.0'} @@ -4574,6 +4664,10 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + /object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + /object-inspect@1.13.1: resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} @@ -5366,6 +5460,41 @@ packages: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} + /socket.io-adapter@2.5.4: + resolution: {integrity: sha512-wDNHGXGewWAjQPt3pyeYBtpWSq9cLE5UW1ZUPL/2eGK9jtse/FpXib7epSTsz0Q0m+6sg6Y4KtcFTlah1bdOVg==} + dependencies: + debug: 4.3.4 + ws: 8.11.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + /socket.io-parser@4.2.4: + resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==} + engines: {node: '>=10.0.0'} + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + + /socket.io@4.7.5: + resolution: {integrity: sha512-DmeAkF6cwM9jSfmp6Dr/5/mfMwb5Z5qRrSXLpo3Fq5SqyU8CMF15jIN4ZhfSwu35ksM1qmHZDQ/DK5XTccSTvA==} + engines: {node: '>=10.2.0'} + dependencies: + accepts: 1.3.8 + base64id: 2.0.0 + cors: 2.8.5 + debug: 4.3.4 + engine.io: 6.5.4 + socket.io-adapter: 2.5.4 + socket.io-parser: 4.2.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + /sonic-boom@3.8.0: resolution: {integrity: sha512-ybz6OYOUjoQQCQ/i4LU8kaToD8ACtYP+Cj5qd2AO36bwbdewxWJ3ArmJ2cr6AvxlL2o0PqnCcPGUgkILbfkaCA==} dependencies: @@ -6078,6 +6207,18 @@ packages: signal-exit: 3.0.7 dev: true + /ws@8.11.0: + resolution: {integrity: sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + /xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts index 08f6b63..ba9b881 100644 --- a/apps/api/src/app.module.ts +++ b/apps/api/src/app.module.ts @@ -17,6 +17,7 @@ import { NotificationModule } from './notification/notification.module'; import mailConfig from 'config/mail.config'; import cmsConfig from 'config/cms.config'; import { ActivityModule } from 'activity/activity.module'; +import { WebsocketModule } from 'websocket/websocket.module'; @Module({ imports: [ @@ -41,6 +42,7 @@ import { ActivityModule } from 'activity/activity.module'; ReservationModule, NotificationModule, ActivityModule, + WebsocketModule, ], controllers: [HealthController], }) diff --git a/apps/api/src/websocket/api/event-handlers/send-message-on-reservation-created-event.handler.ts b/apps/api/src/websocket/api/event-handlers/send-message-on-reservation-created-event.handler.ts new file mode 100644 index 0000000..f9a5cdf --- /dev/null +++ b/apps/api/src/websocket/api/event-handlers/send-message-on-reservation-created-event.handler.ts @@ -0,0 +1,27 @@ +import { Injectable } from '@nestjs/common'; +import { OnEvent } from '@nestjs/event-emitter'; +import { ReservationCreated, Topic } from 'shared/event/messaging'; +import { InjectPinoLogger, PinoLogger } from 'nestjs-pino'; +import { WebSocketProviderService } from 'websocket/websocket-provider.service'; +import { EventType } from 'websocket/event-type'; + +@Injectable() +export class SendReservationCreatedMessageHandler { + constructor( + @InjectPinoLogger(SendReservationCreatedMessageHandler.name) + private readonly logger: PinoLogger, + private readonly websocketProviderService: WebSocketProviderService, + ) {} + + @OnEvent(Topic.RESERVATION_CREATED, { async: true }) + async handle(payload: ReservationCreated) { + this.logger.info( + { payload }, + 'Executing SendReservationCreatedMessageHandler', + ); + this.websocketProviderService.sendMessage(EventType.RESERVATION_ACTIVITY, { + type: Topic.RESERVATION_CREATED, + data: { userId: payload.userId, occuredOn: payload.occurredOn }, + }); + } +} diff --git a/apps/api/src/websocket/api/event-handlers/send-message-on-reservation-deleted-event.handler.ts b/apps/api/src/websocket/api/event-handlers/send-message-on-reservation-deleted-event.handler.ts new file mode 100644 index 0000000..e5951e5 --- /dev/null +++ b/apps/api/src/websocket/api/event-handlers/send-message-on-reservation-deleted-event.handler.ts @@ -0,0 +1,27 @@ +import { Injectable } from '@nestjs/common'; +import { OnEvent } from '@nestjs/event-emitter'; +import { ReservationDeleted, Topic } from 'shared/event/messaging'; +import { InjectPinoLogger, PinoLogger } from 'nestjs-pino'; +import { WebSocketProviderService } from 'websocket/websocket-provider.service'; +import { EventType } from 'websocket/event-type'; + +@Injectable() +export class SendReservationDeletedMessageHandler { + constructor( + @InjectPinoLogger(SendReservationDeletedMessageHandler.name) + private readonly logger: PinoLogger, + private readonly websocketProviderService: WebSocketProviderService, + ) {} + + @OnEvent(Topic.RESERVATION_DELETED, { async: true }) + async handle(payload: ReservationDeleted) { + this.logger.info( + { payload }, + 'Executing SendReservationDeletedMessageHandler', + ); + this.websocketProviderService.sendMessage(EventType.RESERVATION_ACTIVITY, { + type: Topic.RESERVATION_DELETED, + data: { userId: payload.userId, occuredOn: payload.occurredOn }, + }); + } +} diff --git a/apps/api/src/websocket/event-type.ts b/apps/api/src/websocket/event-type.ts new file mode 100644 index 0000000..752b6d4 --- /dev/null +++ b/apps/api/src/websocket/event-type.ts @@ -0,0 +1,3 @@ +export const EventType = { + RESERVATION_ACTIVITY: 'RESERVATION_ACTIVITY', +}; diff --git a/apps/api/src/websocket/websocket-provider.service.ts b/apps/api/src/websocket/websocket-provider.service.ts new file mode 100644 index 0000000..8ddfe63 --- /dev/null +++ b/apps/api/src/websocket/websocket-provider.service.ts @@ -0,0 +1,12 @@ +import { WebSocketGateway, WebSocketServer } from '@nestjs/websockets'; +import { Server } from 'socket.io'; + +@WebSocketGateway() +export class WebSocketProviderService { + @WebSocketServer() + server: Server; + + sendMessage(event: string, data: any) { + this.server.emit(event, data); + } +} diff --git a/apps/api/src/websocket/websocket.module.ts b/apps/api/src/websocket/websocket.module.ts new file mode 100644 index 0000000..49a0df3 --- /dev/null +++ b/apps/api/src/websocket/websocket.module.ts @@ -0,0 +1,15 @@ +import { Module } from '@nestjs/common'; +import { WebSocketProviderService } from './websocket-provider.service'; +import { SendReservationCreatedMessageHandler } from './api/event-handlers/send-message-on-reservation-created-event.handler'; +import { SendReservationDeletedMessageHandler } from './api/event-handlers/send-message-on-reservation-deleted-event.handler'; + +const eventHandlers = [ + SendReservationCreatedMessageHandler, + SendReservationDeletedMessageHandler, +]; + +@Module({ + imports: [], + providers: [WebSocketProviderService, ...eventHandlers], +}) +export class WebsocketModule {} From 0c9cb874870c04aa04197809fe75a4238fc95fa3 Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Tue, 30 Apr 2024 15:30:13 +0200 Subject: [PATCH 09/15] Updated backend logic --- apps/api/.env.example | 4 +++- .../send-message-on-reservation-created-event.handler.ts | 2 +- .../send-message-on-reservation-deleted-event.handler.ts | 2 +- apps/api/src/websocket/websocket-provider.service.ts | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/api/.env.example b/apps/api/.env.example index 885748a..8466d6c 100644 --- a/apps/api/.env.example +++ b/apps/api/.env.example @@ -21,4 +21,6 @@ SMTP_PASSWORD= SANITY_PROJECT_ID= SANITY_WEBHOOK_SECRET=my-hard-to-guess-secret -SANITY_API_TOKEN= \ No newline at end of file +SANITY_API_TOKEN= + +CLIENT_BASE_URL= \ No newline at end of file diff --git a/apps/api/src/websocket/api/event-handlers/send-message-on-reservation-created-event.handler.ts b/apps/api/src/websocket/api/event-handlers/send-message-on-reservation-created-event.handler.ts index f9a5cdf..b708747 100644 --- a/apps/api/src/websocket/api/event-handlers/send-message-on-reservation-created-event.handler.ts +++ b/apps/api/src/websocket/api/event-handlers/send-message-on-reservation-created-event.handler.ts @@ -21,7 +21,7 @@ export class SendReservationCreatedMessageHandler { ); this.websocketProviderService.sendMessage(EventType.RESERVATION_ACTIVITY, { type: Topic.RESERVATION_CREATED, - data: { userId: payload.userId, occuredOn: payload.occurredOn }, + data: { userId: payload.userId, occurredOn: payload.occurredOn }, }); } } diff --git a/apps/api/src/websocket/api/event-handlers/send-message-on-reservation-deleted-event.handler.ts b/apps/api/src/websocket/api/event-handlers/send-message-on-reservation-deleted-event.handler.ts index e5951e5..2948691 100644 --- a/apps/api/src/websocket/api/event-handlers/send-message-on-reservation-deleted-event.handler.ts +++ b/apps/api/src/websocket/api/event-handlers/send-message-on-reservation-deleted-event.handler.ts @@ -21,7 +21,7 @@ export class SendReservationDeletedMessageHandler { ); this.websocketProviderService.sendMessage(EventType.RESERVATION_ACTIVITY, { type: Topic.RESERVATION_DELETED, - data: { userId: payload.userId, occuredOn: payload.occurredOn }, + data: { userId: payload.userId, occurredOn: payload.occurredOn }, }); } } diff --git a/apps/api/src/websocket/websocket-provider.service.ts b/apps/api/src/websocket/websocket-provider.service.ts index 8ddfe63..e4bcef4 100644 --- a/apps/api/src/websocket/websocket-provider.service.ts +++ b/apps/api/src/websocket/websocket-provider.service.ts @@ -1,7 +1,7 @@ import { WebSocketGateway, WebSocketServer } from '@nestjs/websockets'; import { Server } from 'socket.io'; -@WebSocketGateway() +@WebSocketGateway({ cors: true }) export class WebSocketProviderService { @WebSocketServer() server: Server; From 1c30eb2db2e72bf3a8571be0372760aeb64f6f14 Mon Sep 17 00:00:00 2001 From: Petar Trutanic Date: Tue, 30 Apr 2024 15:42:52 +0200 Subject: [PATCH 10/15] Work in progress --- apps/app/package.json | 1 + apps/app/pnpm-lock.yaml | 70 ++++++++++++++++++++++- apps/app/src/components/DefaultLayout.vue | 6 ++ apps/app/src/models/websocket.ts | 13 +++++ apps/app/src/router/index.ts | 7 +++ apps/app/src/socket.ts | 28 +++++++++ apps/app/src/views/UserActivities.vue | 44 ++++++++++++++ 7 files changed, 167 insertions(+), 2 deletions(-) create mode 100644 apps/app/src/models/websocket.ts create mode 100644 apps/app/src/socket.ts create mode 100644 apps/app/src/views/UserActivities.vue diff --git a/apps/app/package.json b/apps/app/package.json index 4ee0f38..64b181b 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -18,6 +18,7 @@ "@vueuse/integrations": "^10.9.0", "axios": "^1.6.7", "date-fns": "^3.3.1", + "socket.io-client": "^4.7.5", "vue": "^3.4.15", "vue-router": "^4.2.5", "vuetify": "^3.5.8" diff --git a/apps/app/pnpm-lock.yaml b/apps/app/pnpm-lock.yaml index 3fe6a20..5fbe6c3 100644 --- a/apps/app/pnpm-lock.yaml +++ b/apps/app/pnpm-lock.yaml @@ -23,6 +23,9 @@ dependencies: date-fns: specifier: ^3.3.1 version: 3.3.1 + socket.io-client: + specifier: ^4.7.5 + version: 4.7.5 vue: specifier: ^3.4.15 version: 3.4.15(typescript@5.3.2) @@ -533,6 +536,10 @@ packages: resolution: {integrity: sha512-0xd7qez0AQ+MbHatZTlI1gu5vkG8r7MYRUJAHPAHJBmGLs16zpkrpAVLvjQKQOqaXPDUBwOiJzNc00znHSCVBw==} dev: true + /@socket.io/component-emitter@3.1.2: + resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} + dev: false + /@tsconfig/node20@20.1.2: resolution: {integrity: sha512-madaWq2k+LYMEhmcp0fs+OGaLFk0OenpHa4gmI4VEmCKX4PJntQ6fnnGADVFrVkBj0wIdAlQnK/MrlYTHsa1gQ==} dev: true @@ -1136,7 +1143,6 @@ packages: optional: true dependencies: ms: 2.1.2 - dev: true /deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -1161,6 +1167,25 @@ packages: esutils: 2.0.3 dev: true + /engine.io-client@6.5.3: + resolution: {integrity: sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==} + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.4 + engine.io-parser: 5.2.2 + ws: 8.11.0 + xmlhttprequest-ssl: 2.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: false + + /engine.io-parser@5.2.2: + resolution: {integrity: sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==} + engines: {node: '>=10.0.0'} + dev: false + /entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} @@ -1691,7 +1716,6 @@ packages: /ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} - dev: true /muggle-string@0.3.1: resolution: {integrity: sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==} @@ -1943,6 +1967,30 @@ packages: engines: {node: '>=8'} dev: true + /socket.io-client@4.7.5: + resolution: {integrity: sha512-sJ/tqHOCe7Z50JCBCXrsY3I2k03iOiUe+tj1OmKeD2lXPiGH/RUCdTZFoqVyN7l1MnpIzPrGtLcijffmeouNlQ==} + engines: {node: '>=10.0.0'} + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.4 + engine.io-client: 6.5.3 + socket.io-parser: 4.2.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: false + + /socket.io-parser@4.2.4: + resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==} + engines: {node: '>=10.0.0'} + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: false + /source-map-js@1.0.2: resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} engines: {node: '>=0.10.0'} @@ -2214,11 +2262,29 @@ packages: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} dev: true + /ws@8.11.0: + resolution: {integrity: sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: false + /xml-name-validator@4.0.0: resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} engines: {node: '>=12'} dev: true + /xmlhttprequest-ssl@2.0.0: + resolution: {integrity: sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==} + engines: {node: '>=0.4.0'} + dev: false + /yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} dev: true diff --git a/apps/app/src/components/DefaultLayout.vue b/apps/app/src/components/DefaultLayout.vue index 4024faf..593e04d 100644 --- a/apps/app/src/components/DefaultLayout.vue +++ b/apps/app/src/components/DefaultLayout.vue @@ -32,6 +32,12 @@ prepend-icon="mdi-account-group" title="User Management" value="user-management" /> +