webwoods / streamline-1.0

This comprehensive solution is designed to streamline and optimize your business processes, providing a centralized platform to manage and coordinate various aspects of your enterprise. Whether you are a small business or a large corporation, our ERP software is tailored to meet your organizational needs and enhance overall efficiency.
5 stars 1 forks source link

Set up In-App Notifications for Users, Roles, Files, Request Items, Store Items, and Vendors #74

Closed kodiidok closed 6 months ago

kodiidok commented 6 months ago

Notifications implementation depends on the following methodology.

1. Parent and Child Entities

There are several child entities. The following entities must be creted in the backend\libs\core\src\entities folder

notification.entity.ts

(Don't Edit This File)

This is the super class of all notification types. This is a Entity() with @TableInheritance(). Table inheritance is set so that any child entities created will be saved in a single table instead of multiple tables.

e.g. If there are ChildEntity() such as RequestNotification, FileNotification, UserNotification etc., instead of saving the records in 3 separate tables, all these data will be saved in a single Notification table. The type column specifies the type of the notification.

image

import { ObjectType, Field } from '@nestjs/graphql';
import { Entity, Column, OneToMany, TableInheritance } from 'typeorm';
import { StreamLineEntity } from './streamline.entity';
import { NotificationReciever } from './notification-reciever.entity';

@Entity()
@ObjectType()
@TableInheritance({ column: { type: "varchar", name: "type" } })
export class Notification extends StreamLineEntity {
    @Column()
    @Field()
    message!: string;

    @Column()
    @Field()
    senderId!: string;

    @OneToMany(() => NotificationReciever, (entity: NotificationReciever) => entity.notification)
    @Field(() => [Notification], { nullable: true })
    recievers?: NotificationReciever[];
}

request.entity.ts

(Don't Edit This File)

The RequestNotification has already been implemented, and the other entities should be implemented in the same way.
file path: backend\libs\core\src\entities\request-notification.entity.ts

The class should be of @ChildEntity() instead of @Entity(). @Entity() creates a seprate table, where as @ChildEntity() adds records to parent table.

The body of the class can vary based on the notification type. Make sure to map with correct relationships

import { ObjectType, Field } from '@nestjs/graphql';
import { Entity, Column, ManyToOne, JoinColumn, ChildEntity } from 'typeorm';
import { Request } from './request.entity';

import { Notification } from './notification.entity';

// NOTE : this Notification entitiy should be properly imported. There are also a Notification entity/object type from other packages which do not support the current implementation.

@ChildEntity() // instead of @Entity(),  use @ChildEntity()
@ObjectType()
export class RequestNotification extends Notification {

    // the body of the class can vary based on the notification type.
    // make sure to map with correct relationships

    @Column({ name: 'request_id', nullable: true })
    @Field({ nullable: true })
    requestId?: string;

    @ManyToOne(() => Request, (entity: Request) => entity.notifications, {
        onDelete: 'SET NULL',
        onUpdate: 'CASCADE',
    })
    @JoinColumn({ name: 'request_id', referencedColumnName: 'id' })
    request: Request;

}

Make sure to add the reverse map also. In this case, a reverse mapping from Request is added.

import { Field, ObjectType } from '@nestjs/graphql';
import { StreamLineEntity } from './streamline.entity';
import { Entity, Column, ManyToOne, JoinColumn, ManyToMany, JoinTable, OneToMany, AfterInsert, AfterUpdate, EntityManager, DataSource, getRepository } from 'typeorm';
import { RequestType } from './enum/requestType';
import { RequestNotification } from './request-notification.entity';

@Entity()
@ObjectType()
export class Request extends StreamLineEntity {
  // other fileds and columns

  @OneToMany(() => RequestNotification, (entity: RequestNotification) => entity.request)
  @Field(() => [RequestNotification], { nullable: true })
  notifications: RequestNotification[];
}

notification-reciever.entity.ts

file path: backend\libs\core\src\entities\notification-reciever.entity.ts

The keeps track of recievers who recievees notifications. Without this entity, the created notifications will not reach any user or part of the system.

import { ObjectType, Field } from '@nestjs/graphql';
import { Entity, Column, ManyToOne, JoinColumn } from 'typeorm';
import { StreamLineEntity } from './streamline.entity';
import { Notification } from './notification.entity';

@Entity()
@ObjectType()
export class NotificationReciever extends StreamLineEntity {
    @Column()
    @Field()
    recieverId: string;

    @Column()
    @Field({ defaultValue: false })
    isRead: boolean

    @ManyToOne(() => Notification, (entity: Notification) => entity.recievers, {
        onDelete: 'SET NULL',
        onUpdate: 'CASCADE',
    })
    @JoinColumn({ name: 'notification_id', referencedColumnName: 'id' })
    @Field(() => Notification, { nullable: true })
    notification?: Notification;

    @Column({ name: 'notification_id', nullable: true })
    @Field({ nullable: true })
    notificationId?: string;
}

2. Modules for Parent and Child Entities

The following modules must be creted in the backend\libs\core\src\modules folder. This way, the entities can be exposed to the Notifications service and the resolver.

The request-notification.module.ts and the notification-reiver.module.ts has already been created.

request-notification.module.ts

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { RequestItemNotification } from '../entities/request-item-notification.entity';

@Module({
  imports: [TypeOrmModule.forFeature([RequestItemNotification])],
})
export class RequestNotificationModule {}

notification-reciever.module.ts

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { NotificationReciever } from '../entities/notification-reciever.entity';

@Module({
  imports: [TypeOrmModule.forFeature([NotificationReciever])],
})
export class NotificationRecieverModule {}

notification.module.ts

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Notification } from '../entities/notification.entity';
import { NotificationRecieverModule } from './notification-reciever.module';
import { RequestNotificationModule } from './request-notification.module';
import { NotificationService } from '../services/notifiation.service';
import { RequestNotification } from '../entities/request-notification.entity';
import { NotificationReciever } from '../entities/notification-reciever.entity';
import { NotificationResolver } from '../resolvers/notification.resolver';

@Module({
  imports: [
    TypeOrmModule.forFeature([
        Notification, 
        RequestNotification, 
        NotificationReciever
        // here the other child entities must be added in order to expose the entities for the notification service
    ]),

    NotificationRecieverModule,
    RequestNotificationModule,
  ],
  providers: [NotificationResolver, NotificationService],
  exports: [NotificationService],
})
export class NotificationModule {}

3. Connecting the Notification entities to the Procurement App

procurement-plugin.module.ts

file path: backend\apps\procurement-plugin\src\procurement-plugin.module.ts

The created child entities and the parent entity must be added into the entities section of the TypeORM configurations. This creates the relevant tables and exposes the entities to other entities. Note that, there is also another entity named NotificationReciever. This must also be addded.

@Module({
  imports: [
    ProcurementModule,

    GraphQLModule.forRoot<ApolloFederationDriverConfig>({
    // graphql settings
    }),

    TypeOrmModule.forRoot({
    // other config options for typeorm
      entities: [
        // other entities

        Notification,
        NotificationReciever,
        RequestNotification,
        // Vendor,
      ],
      synchronize: true,
    }),
  ],
  controllers: [ProcurementPluginController],
  providers: [ProcurementPluginService],
  exports: [ProcurementPluginService],
})
export class ProcurementPluginModule {}

procurement.module.ts

file path: ``

import { Module } from '@nestjs/common';
import { ProcurementService } from '../services/procurement.service';
import { ProcurementResolver } from '../resolvers/procurement.resolver';
import { NotificationModule } from './notification.module';

@Module({
  imports: [
    // other imports
    NotificationModule, // import NotificationsModule
  ],
  exports: [ProcurementService],
  providers: [ProcurementService, ProcurementResolver],
})
export class ProcurementModule {}

There are no separate .service.ts and .resolver.ts files for each individual child entity. Instead a common notitification.service.ts and notification.resolver.ts has already been added and connected to the procurement app.

Once all the entities have been created and connected to the procurement app, leave a comment "DONE" to proceed with the next steps.

kodiidok commented 6 months ago

DTO for Create and Update

The folder backend\libs\core\src\entities\dto contains all the dto files. The notification entities that were created,

needs create.ts and update.ts files in order for them to compatible with the notification service. Given below are the dto files for Notification, and RequestNotification. Follow the pattern given in them to create the dto files.

1. Notification Entity

create.notification.ts

import { Field, InputType } from '@nestjs/graphql';

@InputType()
export class CreateNotificationInput {
  @Field()
  message!: string;

  @Field()
  senderId!: string;
}

update.notification.ts

import { Field, InputType } from '@nestjs/graphql';

@InputType()
export class UpdateNotificationInput {
  @Field({ nullable: true })
  message?: string;

  @Field({ nullable: true })
  senderId?: string;
}

notification-page.dto.ts

import { PaginateResult } from './paginate-result.dto';
import { ObjectType } from '@nestjs/graphql';
import { Notification } from '../notification.entity';

@ObjectType()
export class NotificationPage extends PaginateResult(Notification) {}

2. Request Notification Child Entity

create.request-notification.ts

import { Field, InputType } from '@nestjs/graphql';
import { CreateNotificationInput } from './create.notification';

@InputType()
export class CreateRequestNotificationInput extends CreateNotificationInput {
  @Field()
  requestId: string;
}

update.request-notification.ts

import { Field, InputType } from '@nestjs/graphql';
import { UpdateNotificationInput } from './update.notification';

@InputType()
export class UpdateRequestNotificationInput extends UpdateNotificationInput {
  @Field({ nullable: true })
  requestId?: string;
}

request-notification-page.dto.ts

import { PaginateResult } from './paginate-result.dto';
import { ObjectType } from '@nestjs/graphql';
import { RequestNotification } from '../request-notification.entity';

@ObjectType()
export class RequestNotificationPage extends PaginateResult(RequestNotification) {}
kodiidok commented 6 months ago

Notification Service

All the CRUD operations related to child entities must be added into the notification.service.ts file in the backend\libs\core\src\services\ folder.

Given below is the code for the CRUD operations of RequestNotification child entity. There are 6 methods given for the RequestNotification`. All such 6 methods must be implemented for all the child entities.

Repository Injection

NOTE that the RequestNotification has been explicitly injected into the notification service using @InjectRepository(). Without this, the services will not work and the app will break.

 @InjectRepository(RequestNotification)
    private readonly requestNotificationRepository: Repository<RequestNotification>,

To inject into the notification service, the notification.module.ts must have the entity features exposed as follows for the relevant entities. In this instance, note that Notification, RequestNotification and NotificationReciver entities have been exposed by TypeORM.forFeature([]).

import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Notification } from '../entities/notification.entity';
import { RequestNotification } from '../entities/request-notification.entity';
import { NotificationReciever } from '../entities/notification-reciever.entity';

@Injectable()
export class NotificationService {
  constructor(
    @InjectRepository(Notification)
    private readonly notificationRepository: Repository<Notification>,
    @InjectRepository(RequestNotification)
    private readonly requestNotificationRepository: Repository<RequestNotification>,
    @InjectRepository(NotificationReciever)
    private readonly notificationRecieverRepository: Repository<NotificationReciever>
  ) { }

  // Request Notifications

  async findAllRequestNotifications(skip: number, take: number): Promise<RequestNotification[]> {
    const data = await this.requestNotificationRepository.find({
      skip,
      take,
      relations: { recievers: true, request: true },
    });
    return data;
  }

  async findRequestNotificationById(id: string): Promise<RequestNotification | null> {
    return await this.requestNotificationRepository.findOne({
      relations: { recievers: true, request: true },
      where: { id },
    });
  }

  async createRequestNotification(input: Partial<RequestNotification>): Promise<RequestNotification | null> {
    const requestNotification = this.requestNotificationRepository.create(input);
    const createdRequestNotification = await this.requestNotificationRepository.save(requestNotification);
    return await this.requestNotificationRepository.findOne({
      relations: { recievers: true, request: true },
      where: { id: createdRequestNotification.id },
    });
  }

  async updateRequestNotification(id: string, input: Partial<RequestNotification>): Promise<RequestNotification | null> {
    const requestNotification = await this.requestNotificationRepository.findOne({
      relations: { recievers: true, request: true },
      where: { id },
    });

    // If the request notification doesn't exist, throw NotFoundException
    if (!requestNotification) {
      throw new NotFoundException(`Request Notification with id ${id} not found`);
    }

    Object.assign(requestNotification, input);

    await this.requestNotificationRepository.save(requestNotification);
    return await this.findRequestNotificationById(id);
  }

  async deleteRequestNotification(id: string): Promise<RequestNotification | null> {
    const requestNotification = await this.requestNotificationRepository.findOne({
      relations: { recievers: true, request: true },
      where: { id },
    });
    await this.requestNotificationRepository.delete(id);
    return requestNotification;
  }

  async softDeleteRequestNotification(id: string): Promise<RequestNotification | null> {
    const requestNotification = await this.requestNotificationRepository.findOne({
      relations: { recievers: true, request: true },
      where: { id },
    });
    await this.requestNotificationRepository.softDelete(id);
    return requestNotification;
  }
}

Link Notification Service with the Notification Receivers

The notification.service.ts file has a method named createRequestNotificationWithReceivers() which handles creating a notification of type RequestNotification and then assigning the NotificationRecievers.

async createRequestNotificationWithReceivers(
    requestId: string,
    senderId: string,
    message: string,
    sendTo?: string[],
  ): Promise<RequestNotification| null> {
    try {
      const notification = await this.createRequestNotification({
        requestId: requestId,
        message: message,
        senderId: senderId,
      });

      const receivers = [...(sendTo ?? []), senderId].map((receiverId) => ({
        isRead: false,
        recieverId: receiverId,
        notificationId: notification.id,
      }));

      const createdReceivers = await this.createNotificationReceivers(receivers);
      notification.recievers = createdReceivers;

      return notification;
    } catch (error: any) {
      console.error(`Error creating request notification with recievers: ${error.message}`);
      return null;
    }
  }

Such similar functions must be implemented in this file, for all the child entities created.

kodiidok commented 6 months ago

Notification Resolver

The notification.resolver.ts file contains the resolver methods for the notification services. Given below is the code for the notification resolver file with methods for the RequestNotification entity.

import {
  Resolver,
  Query,
  Mutation,
  Args,
  ResolveField,
  Parent,
  Int,
  ResolveReference,
} from '@nestjs/graphql';
import { Request } from '../entities/request.entity';
import { NotificationService } from '../services/notifiation.service';
import { Notification } from '../entities/notification.entity';
import { RequestNotification } from '../entities/request-notification.entity';
import { RequestNotificationPage } from '../entities/dto/request-notification-page.dto';
import { CreateRequestNotificationInput } from '../entities/dto/create.request-notification';
import { UpdateRequestNotificationInput } from '../entities/dto/update.request-notification';

@Resolver()
export class NotificationResolver {
  constructor(
    private readonly notificationService: NotificationService
  ) { }

    // Request Notification

  @Query(() => RequestNotification, { name: 'requestNotification' })
  async getRequestNotificationById(@Args('id') id: string): Promise<RequestNotification> {
    try {
      const requestNotification = this.notificationService.findRequestNotificationById(id);
      if (!requestNotification) {
        throw new Error(`Request Notification with ID ${id} not found`);
      }
      return requestNotification;
    } catch (error: any) {
      throw new Error(`Error fetching Request Notification: ${error.message}`);
    }
  }

  @Query(() => RequestNotificationPage, { name: 'requestNotifications' })
  async getUsers(
    @Args('page', { type: () => Int, defaultValue: 1 }) page: number,
    @Args('pageSize', { type: () => Int, defaultValue: 10 }) pageSize: number,
  ): Promise<RequestNotificationPage> {
    try {
      const skip = (page - 1) * pageSize;
      const requestNotifications = await this.notificationService.findAllRequestNotifications(skip, pageSize);
      const requestNotificationPage: RequestNotificationPage = { data: requestNotifications, totalItems: requestNotifications.length };
      return requestNotificationPage;
    } catch (error: any) {
      throw new Error(`Error fetching request notifications: ${error.message}`);
    }
  }

  @Mutation(() => RequestNotification, { name: 'createRequestNotification' })
  async createRequestNotification(@Args('input') input: CreateRequestNotificationInput): Promise<RequestNotification | null> {
    try {
      return await this.notificationService.createRequestNotification(input);
    } catch (error: any) {
      throw new Error(`Error creating request notification: ${error.message}`);
    }
  }

  @Mutation(() => RequestNotification, { name: 'updateRequestNotification' })
  async updateRequestNotification(
    @Args('id') id: string,
    @Args('input') input: UpdateRequestNotificationInput,
  ): Promise<RequestNotification | null> {
    try {
      return await this.notificationService.updateRequestNotification(id, input);
    } catch (error: any) {
      throw new Error(`Error updating request notification: ${error.message}`);
    }
  }

  @Mutation(() => RequestNotification, { name: 'deleteRequestNotification' })
  async deleteRequestNotification(@Args('id') id: string): Promise<RequestNotification | null> {
    try {
      return await this.notificationService.deleteRequestNotification(id);
    } catch (error: any) {
      throw new Error(`Error deleting request notification: ${error.message}`);
    }
  }

  @Mutation(() => RequestNotification, { name: 'softDeleteRequestNotification' })
  async softDeleteRequestNotification(@Args('id') id: string): Promise<RequestNotification | null> {
    try {
      return await this.notificationService.softDeleteRequestNotification(id);
    } catch (error: any) {
      throw new Error(`Error soft-deleting request notification: ${error.message}`);
    }
  }
}

Such similar methods must be implemented for all the child entities inside this file. Note that, only a single service file, notification.service.ts has been injected.