src/request/request.service.ts
Service responsible for managing request-related operations.
Properties |
|
Methods |
|
constructor(prismaService: PrismaService, temporalProvider: TemporalProvider, notificationService: NotificationService)
|
||||||||||||
|
Defined in src/request/request.service.ts:38
|
||||||||||||
|
Constructs a new instance of the RequestService class.
Parameters :
|
| Async approveRequest | ||||||||||||
approveRequest(requestId: number, userId: string)
|
||||||||||||
|
Defined in src/request/request.service.ts:289
|
||||||||||||
|
Sends signal to temporal workflow to approve request
Parameters :
Returns :
Promise<void>
void. |
| Async createNotification | |||||||||||||||
createNotification(event: RequestEvents, channel: NotificationChannel, requestId: number, userId: string)
|
|||||||||||||||
|
Defined in src/request/request.service.ts:384
|
|||||||||||||||
|
Parameters :
Returns :
unknown
|
| Async createRequest | ||||||||||||
createRequest(request: CreateRequestDto, userId)
|
||||||||||||
|
Defined in src/request/request.service.ts:77
|
||||||||||||
|
Creates a new request. Note:
Parameters :
Returns :
Promise<Request>
A promise that resolves to the created request. |
| Async declineRequest | ||||||||||||
declineRequest(requestId: number, userId: string)
|
||||||||||||
|
Defined in src/request/request.service.ts:269
|
||||||||||||
|
Sends signal to temporal workflow to decline request
Parameters :
Returns :
Promise<void>
void. |
| Async deleteRequest | ||||||||
deleteRequest(requestId: number)
|
||||||||
|
Defined in src/request/request.service.ts:234
|
||||||||
|
Deletes a request by setting its requestStatus to DELETED (soft delete).
Parameters :
Returns :
Promise<Request>
A Promise that resolves to the deleted Request object. |
| formatRequestReturnObject | ||||||
formatRequestReturnObject(request)
|
||||||
|
Defined in src/request/request.service.ts:367
|
||||||
|
Formats the request object into a structured return object. Note:
Parameters :
Returns :
Request
The formatted request object. |
| Async getRequestDetails | ||||||||
getRequestDetails(requestId: number)
|
||||||||
|
Defined in src/request/request.service.ts:250
|
||||||||
|
Retrieves the details of a request by its ID.
Parameters :
Returns :
Promise<Request>
A Promise that resolves to the Request object. |
| Async logActivity | ||||||||||||||||||||
logActivity(requestId: number, userId: string, eventType: RequestEvents, details: any)
|
||||||||||||||||||||
|
Defined in src/request/request.service.ts:446
|
||||||||||||||||||||
|
create entry in request activity log.
Parameters :
Returns :
unknown
entry after create log |
| Async requestCancellation | ||||||||
requestCancellation(requestId: number)
|
||||||||
|
Defined in src/request/request.service.ts:212
|
||||||||
|
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 :
Returns :
unknown
Request object up for delete. |
| Private requestFieldListToObject | ||||||||
requestFieldListToObject(requestFieldList: RequestField[])
|
||||||||
|
Defined in src/request/request.service.ts:349
|
||||||||
|
Converts an array of RequestField objects into a key-value object.
Parameters :
Returns :
object
The converted key-value object. |
| Private requestFieldObjectToList | ||||||||
requestFieldObjectToList(requestFieldObject: object)
|
||||||||
|
Defined in src/request/request.service.ts:333
|
||||||||
|
Converts a request field object into a list of RequestField objects that align with the internal format.
Parameters :
Returns :
RequestField[]
An array of RequestField objects. |
| Async requests | ||||||||
requests(params: literal type)
|
||||||||
|
Defined in src/request/request.service.ts:310
|
||||||||
|
Retrieves the list of a request based on params ref https://www.prisma.io/docs/orm/reference/prisma-client-reference#findmany.
Parameters :
Returns :
Promise<Request[]>
A Promise that resolves to the Request object List. |
| Async triggerRequestWorkflow | |||||||||
triggerRequestWorkflow(requestType, args: any)
|
|||||||||
|
Defined in src/request/request.service.ts:57
|
|||||||||
|
Function to trigger async temporal workflow based on requestType input and args
Parameters :
Returns :
unknown
workflowId for triggered workflow |
| Async updateRequest | ||||||||||||
updateRequest(requestId: number, request: UpdateRequestDto, userId: string)
|
||||||||||||
|
Defined in src/request/request.service.ts:113
|
||||||||||||
|
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 :
Returns :
Promise<Request>
|
| Private RequestTypeWorkflowMap |
Type : object
|
Default value : {
PROJECT: "ProjectApprovalWorkflow",
PROJECT_ACCESS: "ProjectUserApprovalWorkflow",
}
|
|
Defined in src/request/request.service.ts:35
|
|
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;
}
}