File

src/request/request.service.ts

Description

Service responsible for managing request-related operations.

Index

Properties
Methods

Constructor

constructor(prismaService: PrismaService, temporalProvider: TemporalProvider, notificationService: NotificationService)

Constructs a new instance of the RequestService class.

Parameters :
Name Type Optional
prismaService PrismaService No
temporalProvider TemporalProvider No
notificationService NotificationService No

Methods

Async approveRequest
approveRequest(requestId: number, userId: string)

Sends signal to temporal workflow to approve request

Parameters :
Name Type Optional Description
requestId number No
  • The ID of the request to retrieve.
userId string No
Returns : Promise<void>

void.

Async createNotification
createNotification(event: RequestEvents, channel: NotificationChannel, requestId: number, userId: string)
Parameters :
Name Type Optional
event RequestEvents No
channel NotificationChannel No
requestId number No
userId string No
Returns : unknown
Async createRequest
createRequest(request: CreateRequestDto, userId)

Creates a new request. Note:

  • Currently creating request and not triggering any workflow.
  • TODO: add logs
Parameters :
Name Type Optional Description
request CreateRequestDto No
  • The request data.
userId No
  • The ID of the user creating the request.
Returns : Promise<Request>

A promise that resolves to the created request.

Async declineRequest
declineRequest(requestId: number, userId: string)

Sends signal to temporal workflow to decline request

Parameters :
Name Type Optional Description
requestId number No
  • The ID of the request to retrieve.
userId string No
Returns : Promise<void>

void.

Async deleteRequest
deleteRequest(requestId: number)

Deletes a request by setting its requestStatus to DELETED (soft delete).

Parameters :
Name Type Optional Description
requestId number No
  • The ID of the request to be deleted.
Returns : Promise<Request>

A Promise that resolves to the deleted Request object.

formatRequestReturnObject
formatRequestReturnObject(request)

Formats the request object into a structured return object. Note:

  • there is a difference b/w how request fields in stored in database and how it will be returned to user, have created a function to do this task to reduce redundent code.
Parameters :
Name Optional Description
request No
  • The request object to be formatted.
Returns : Request

The formatted request object.

Async getRequestDetails
getRequestDetails(requestId: number)

Retrieves the details of a request by its ID.

Parameters :
Name Type Optional Description
requestId number No
  • The ID of the request to retrieve.
Returns : Promise<Request>

A Promise that resolves to the Request object.

Async logActivity
logActivity(requestId: number, userId: string, eventType: RequestEvents, details: any)

create entry in request activity log.

Parameters :
Name Type Optional Description
requestId number No
  • requestId for request where we wish to add log
userId string No
  • user who did the update
eventType RequestEvents No
  • type of activit FIELD_UPDATE/ COMMENT etc
details any No
  • metadata related to activity on request
Returns : unknown

entry after create log

Async requestCancellation
requestCancellation(requestId: number)

Sends a cancellation request to running workflow so that workflow initiates a cleanup if required if there is no running workflow just soft delete the request

Parameters :
Name Type Optional Description
requestId number No
  • The ID of the request to be deleted.
Returns : unknown

Request object up for delete.

Private requestFieldListToObject
requestFieldListToObject(requestFieldList: RequestField[])

Converts an array of RequestField objects into a key-value object.

Parameters :
Name Type Optional Description
requestFieldList RequestField[] No
  • The array of RequestField objects to convert.
Returns : object

The converted key-value object.

Private requestFieldObjectToList
requestFieldObjectToList(requestFieldObject: object)

Converts a request field object into a list of RequestField objects that align with the internal format.

Parameters :
Name Type Optional Description
requestFieldObject object No
  • The request field object to convert.
Returns : RequestField[]

An array of RequestField objects.

Async requests
requests(params: literal type)

Retrieves the list of a request based on params ref https://www.prisma.io/docs/orm/reference/prisma-client-reference#findmany.

Parameters :
Name Type Optional Description
params literal type No
  • object with search filters for requests.
Returns : Promise<Request[]>

A Promise that resolves to the Request object List.

Async triggerRequestWorkflow
triggerRequestWorkflow(requestType, args: any)

Function to trigger async temporal workflow based on requestType input and args

Parameters :
Name Type Optional
requestType No
args any No
Returns : unknown

workflowId for triggered workflow

Async updateRequest
updateRequest(requestId: number, request: UpdateRequestDto, userId: string)

Function to update database entry for a request every updated in request is logged using this.logActivity function Returns the updated database entry for request

Parameters :
Name Type Optional
requestId number No
request UpdateRequestDto No
userId string No
Returns : Promise<Request>

Properties

Private RequestTypeWorkflowMap
Type : object
Default value : { PROJECT: "ProjectApprovalWorkflow", PROJECT_ACCESS: "ProjectUserApprovalWorkflow", }

Map maintained to check which workflow to execute for a given request Type eg: for RequestType = PROJECT , workflowName = ProjectApprovalWorkflow

import { PrismaService } from "../common/prisma/prisma.service";
import { Injectable, InternalServerErrorException } from "@nestjs/common";
import { RequestField } from "./types/request-field.interface";
import { RequestStatus } from "./types/request-status.interface";
import { CreateRequestDto } from "./dto/create-request.dto";
import { UpdateRequestDto } from "./dto/update-request.dto";
import { Request } from "./entity/request.entity";
import { TemporalProvider } from "../providers/temporal/temporal.provider";
import { Prisma } from "@prisma/client";
import {
  getNamesFromEmail,
  getTextFromTemplate,
  getUuidSlug,
} from "../common/helper";
import { RequestEvents } from "./types/request-event-type.interface";
import {
  NotificationChannel,
  NotificationEntityType,
} from "../notification/types/notification.enums";
import { requestTypeTemplates } from "./templates/request-type.templates";
import { requestEventsConfig } from "./request.events";
import { NotificationService } from "../notification/notification.service";
import { EventType } from "../common/events";

/**
 * Service responsible for managing request-related operations.
 */
@Injectable()
export class RequestService {
  /**
   * Map maintained to check which workflow to execute for a
   * given request Type
   * eg: for RequestType = PROJECT , workflowName = ProjectApprovalWorkflow
   */
  private RequestTypeWorkflowMap = {
    PROJECT: "ProjectApprovalWorkflow",
    PROJECT_ACCESS: "ProjectUserApprovalWorkflow",
  };
  /**
   * Constructs a new instance of the RequestService class.
   * @param prismaService
   * @param temporalProvider
   */
  constructor(
    private readonly prismaService: PrismaService,
    private readonly temporalProvider: TemporalProvider,
    private readonly notificationService: NotificationService,
  ) {}

  /**
   * Function to trigger async temporal workflow
   * based on requestType input and args
   * @param requestType
   * @param args
   * @returns workflowId for triggered workflow
   */
  async triggerRequestWorkflow(requestType, args: any) {
    const workflowName = this.RequestTypeWorkflowMap[requestType];
    // trigger temporal workflow without any args
    return await this.temporalProvider.runAsync(
      workflowName,
      args,
      process.env.DEFAULT_QUEUE_NAME,
      `workflow-${workflowName}-${getUuidSlug()}`,
    );
  }

  /**
   * Creates a new request.
   * Note:
   * - Currently creating request and not triggering any workflow.
   * - TODO: add logs
   * @param request - The request data.
   * @param userId - The ID of the user creating the request.
   * @returns A promise that resolves to the created request.
   */
  async createRequest(request: CreateRequestDto, userId): Promise<Request> {
    request.requestedBy = userId;

    const createdRequest = await this.prismaService.request.create({
      data: {
        name: request.name,
        description: request.description,
        requestType: request.requestType,
        requestStatus: RequestStatus.PENDING,
        requestedBy: request.requestedBy,
        assignedTo: request.assignedTo,
        requestFieldList: {
          create: this.requestFieldObjectToList(request.requestFieldObject),
        },
      },
      include: {
        requestFieldList: true,
      },
    });
    await this.logActivity(
      createdRequest.id,
      userId,
      RequestEvents.CREATE_REQUEST,
      { message: "Created Request" },
    );
    return this.formatRequestReturnObject(createdRequest);
  }

  /**
   * Function to update database entry for a request
   * every updated in request is logged using this.logActivity function
   * Returns the updated database entry for request
   * @param {number} requestId
   * @param {Request} request
   * @returns {Request}
   */
  async updateRequest(
    requestId: number,
    request: UpdateRequestDto,
    userId: string,
  ): Promise<Request> {
    const oldRequest = await this.getRequestDetails(requestId);
    const requestFieldList = this.requestFieldObjectToList(
      request.requestFieldObject,
    );
    const updatedRequest = await this.prismaService.$transaction(async (tx) => {
      // check if any submission field updated
      for (const fieldEntry of requestFieldList) {
        if (
          oldRequest.requestFieldObject[fieldEntry.fieldName] ==
          fieldEntry.fieldValue
        ) {
          // if no change in value then skip
          continue;
        }

        const logDetails = {
          fieldName: fieldEntry.fieldName,
          oldValue: oldRequest.requestFieldObject[fieldEntry.fieldName],
          newValue: fieldEntry.fieldValue,
        };
        this.logActivity(
          requestId,
          userId,
          RequestEvents.UPDATE_FIELD,
          logDetails,
        );

        await tx.requestField.update({
          where: {
            id: {
              requestId: requestId,
              fieldName: fieldEntry.fieldName,
            },
          },
          data: {
            fieldValue: fieldEntry.fieldValue,
          },
        });
      }

      // check if any request field updated
      Object.keys(request).forEach((key) => {
        if (
          request[key] != oldRequest[key] &&
          ![
            "updatedAt",
            "requestFieldObject",
            "requestActivityLog",
            "assignedTo",
          ].includes(key)
        ) {
          const logDetails = {
            fieldName: key,
            oldValue: oldRequest[key],
            newValue: request[key],
          };
          this.logActivity(
            requestId,
            userId,
            RequestEvents.UPDATE_FIELD,
            logDetails,
          );
        }
      });

      const updatedRequest = await tx.request.update({
        where: {
          id: requestId,
        },
        data: {
          name: request.name,
          description: request.description,
          requestType: request.requestType,
          requestStatus: request.requestStatus,
          requestedBy: request.requestedBy,
          assignedTo: request.assignedTo ? request.assignedTo : null,
        },
        include: {
          requestFieldList: true,
        },
      });
      return updatedRequest;
    });

    return this.formatRequestReturnObject(updatedRequest);
  }

  /**
   * Sends a cancellation request to running workflow so that
   * workflow initiates a cleanup if required
   * if there is no running workflow just soft delete the request
   * @param requestId - The ID of the request to be deleted.
   * @returns Request object up for delete.
   */
  async requestCancellation(requestId: number) {
    const request = await this.getRequestDetails(requestId);
    if (
      request.requestStatus == RequestStatus.PENDING &&
      "workflowId" in request.requestFieldObject
    ) {
      await this.temporalProvider.cancelWorkflowRun(
        `${request.requestFieldObject["workflowId"]}`,
        `${request.requestFieldObject["runId"]}`,
      );
    } else {
      await this.deleteRequest(requestId);
    }

    return request;
  }

  /**
   * Deletes a request by setting its requestStatus to DELETED (soft delete).
   * @param requestId - The ID of the request to be deleted.
   * @returns A Promise that resolves to the deleted Request object.
   */
  async deleteRequest(requestId: number): Promise<Request> {
    return await this.prismaService.request.update({
      where: {
        id: requestId,
      },
      data: {
        requestStatus: RequestStatus.DELETED,
      },
    });
  }

  /**
   * Retrieves the details of a request by its ID.
   * @param requestId - The ID of the request to retrieve.
   * @returns A Promise that resolves to the Request object.
   */
  async getRequestDetails(requestId: number): Promise<Request> {
    const request = await this.prismaService.request.findUnique({
      where: {
        id: requestId,
      },
      include: {
        requestFieldList: true,
        requestActivityLog: true,
      },
    });

    return this.formatRequestReturnObject(request);
  }

  /**
   * Sends signal to temporal workflow to decline request
   * @param requestId - The ID of the request to retrieve.
   * @returns void.
   */
  async declineRequest(requestId: number, userId: string): Promise<void> {
    try {
      console.log(userId); // fixing lint error
      const request = await this.getRequestDetails(requestId);
      return this.temporalProvider.sendSignal(
        request.requestFieldObject["workflowId"],
        request.requestFieldObject["runId"],
        request.requestFieldObject["declineSignal"],
        { userId: userId },
      );
    } catch (error) {
      throw new InternalServerErrorException(error.message);
    }
  }

  /**
   * Sends signal to temporal workflow to approve request
   * @param requestId - The ID of the request to retrieve.
   * @returns void.
   */
  async approveRequest(requestId: number, userId: string): Promise<void> {
    try {
      console.log(userId); // fixing lint error
      const request = await this.getRequestDetails(requestId);
      return this.temporalProvider.sendSignal(
        request.requestFieldObject["workflowId"],
        request.requestFieldObject["runId"],
        request.requestFieldObject["approveSignal"],
        { userId: userId },
      );
    } catch (error) {
      throw new InternalServerErrorException(error.message);
    }
  }

  /**
   * Retrieves the list of a request based on params
   * ref https://www.prisma.io/docs/orm/reference/prisma-client-reference#findmany.
   * @param params - object with search filters for requests.
   * @returns A Promise that resolves to the Request object List.
   */
  async requests(params: {
    skip?: number;
    take?: number;
    cursor?: Prisma.RequestWhereUniqueInput;
    where?: Prisma.RequestWhereInput;
    orderBy?: Prisma.RequestOrderByWithRelationInput;
  }): Promise<Request[]> {
    const { skip, take, cursor, where, orderBy } = params;
    return await this.prismaService.request.findMany({
      skip,
      take,
      cursor,
      where,
      orderBy,
    });
  }

  /**
   * Converts a request field object into a list of RequestField objects
   * that align with the internal format.
   * @param requestFieldObject - The request field object to convert.
   * @returns An array of RequestField objects.
   */
  private requestFieldObjectToList(requestFieldObject: object): RequestField[] {
    const requestFieldList = [];
    for (const [key, value] of Object.entries(requestFieldObject)) {
      requestFieldList.push({
        fieldName: key,
        fieldValue: value,
      });
    }
    return requestFieldList;
  }

  /**
   * Converts an array of RequestField objects into a key-value object.
   * @param requestFieldList - The array of RequestField objects to convert.
   * @returns The converted key-value object.
   */
  private requestFieldListToObject(requestFieldList: RequestField[]): object {
    const requestFieldObject = {};
    requestFieldList.forEach((field) => {
      requestFieldObject[field.fieldName] = field.fieldValue;
    });

    return requestFieldObject;
  }

  /**
   * Formats the request object into a structured return object.
   * Note:
   * - there is a difference b/w how request fields in stored in database and how it
   *   will be returned to user, have created a function to do this task to reduce
   *   redundent code.
   * @param request - The request object to be formatted.
   * @returns The formatted request object.
   */
  formatRequestReturnObject(request): Request {
    return {
      id: request.id,
      name: request.name,
      description: request.description,
      requestType: request.requestType,
      requestStatus: request.requestStatus,
      requestedBy: request.requestedBy,
      assignedTo: request.assignedTo,
      updatedAt: request.updatedAt,
      requestFieldObject: this.requestFieldListToObject(
        request.requestFieldList,
      ),
      requestActivityLog: request.requestActivityLog,
    };
  }

  async createNotification(
    event: RequestEvents,
    channel: NotificationChannel,
    requestId: number,
    userId: string,
  ) {
    const request = await this.getRequestDetails(requestId);
    const requestUsers = new Set(
      [request.assignedTo, request.requestedBy].flat(),
    );

    const usersToNotify = [...requestUsers].filter((elem) => {
      if (Boolean(elem) && elem !== userId) {
        return elem;
      }
    });
    const entityType = NotificationEntityType.REQUEST;
    const entityId = requestId;

    const promises = [];
    usersToNotify.forEach((userToNotify) => {
      const templateVars = {
        sendToName: getNamesFromEmail(userToNotify),
        sendToEmail: userToNotify,
        commenter: getNamesFromEmail(userId),
        request: request,
        requestType: requestTypeTemplates[request.requestType].requestText,
        requestURL: `${process.env.TURO_BASE_URL}${process.env.TURO_REQUEST_BASE_URL}${request.id}`,
      };

      const config = requestEventsConfig[event];
      const template = config.channelConfig[channel];

      const subject = getTextFromTemplate(template["subject"], templateVars);
      const body = getTextFromTemplate(template["body"], templateVars);

      const notificationData = {
        projectId: 0, // requests not directly linked to project
        subject: subject,
        body: body,
        severity: config.severity,
        channel: channel,
        userId: userToNotify,
        entityId: entityId,
        entityType: entityType,
        eventType: EventType.REQUEST,
        eventIdentifier: event,
      };

      promises.push(this.notificationService.create(notificationData));
    });
    return Promise.all(promises);
  }

  /**
   * create entry in request activity log.
   * @param {number} requestId - requestId for request where we wish to add log
   * @param {string} userId - user who did the update
   * @param {string} eventType - type of activit FIELD_UPDATE/ COMMENT etc
   * @param {any} details - metadata related to activity on request
   * @returns entry after create log
   */
  async logActivity(
    requestId: number,
    userId: string,
    eventType: RequestEvents,
    details: any,
  ) {
    const log = await this.prismaService.requestActivityLog.create({
      data: {
        requestId: requestId,
        userId: userId,
        eventType: eventType,
        details: details,
      },
    });

    if (eventType == RequestEvents.COMMENT) {
      const notificationPromises = [];
      notificationPromises.push(
        this.createNotification(
          eventType,
          NotificationChannel.APP,
          requestId,
          userId,
        ),
        this.createNotification(
          eventType,
          NotificationChannel.EMAIL,
          requestId,
          userId,
        ),
      );
      const notifications = (await Promise.all(notificationPromises))
        .flat()
        .flat();

      notifications.map((notification) => {
        if (notification.channel != NotificationChannel.APP) {
          this.temporalProvider.runAsync(
            "deliverNotificationWorkflow",
            [notification.id],
            process.env.DEFAULT_QUEUE_NAME,
            `workflow-deliverNotificationWorkflow-${getUuidSlug()}`,
          );
        }
      });
    }

    return log;
  }
}

results matching ""

    No results matching ""