import { Injectable } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; import { PubSubService } from '../pubsub/pubsub.service'; import * as E from 'fp-ts/Either'; import { UserRequest } from './user-request.model'; import { UserRequest as DbUserRequest } from '@prisma/client'; import { USER_COLLECTION_NOT_FOUND, USER_REQUEST_CREATION_FAILED, USER_REQUEST_INVALID_TYPE, USER_REQUEST_NOT_FOUND, USER_REQUEST_REORDERING_FAILED, } from 'src/errors'; import { stringToJson } from 'src/utils'; import { AuthUser } from 'src/types/AuthUser'; import { ReqType } from 'src/types/RequestTypes'; import { UserCollectionService } from 'src/user-collection/user-collection.service'; @Injectable() export class UserRequestService { constructor( private readonly prisma: PrismaService, private readonly pubsub: PubSubService, private readonly userCollectionService: UserCollectionService, ) {} /** * Typecast a database user request to a user request * @param dbRequest Database user request * @returns User request */ private cast(dbRequest: DbUserRequest): UserRequest { return { ...dbRequest, type: ReqType[dbRequest.type], request: JSON.stringify(dbRequest.request), }; } /** * Get paginated user requests * @param collectionID ID of the collection to which the request belongs * @param take Number of requests to fetch * @param cursor ID of the request after which to fetch requests * @param user User who owns the requests * @returns Either of an Array of user requests */ async fetchUserRequests( collectionID: string, type: ReqType, cursor: string, take: number, user: AuthUser, ) { const dbRequests = await this.prisma.userRequest.findMany({ where: { userUid: user.uid, collectionID: collectionID, type, }, take: take, // default: 10 skip: cursor ? 1 : 0, cursor: cursor ? { id: cursor } : undefined, orderBy: { orderIndex: 'asc' }, }); const userRequests: UserRequest[] = dbRequests.map((r) => this.cast(r)); return E.right(userRequests); } /** * Get a user request by ID * @param id ID of the request to fetch * @param user User who owns the request * @returns Either of the user request */ async fetchUserRequest( id: string, user: AuthUser, ): Promise | E.Right> { const dbRequest = await this.prisma.userRequest.findUnique({ where: { id }, }); if (!dbRequest || dbRequest.userUid !== user.uid) { return E.left(USER_REQUEST_NOT_FOUND); } return E.right(this.cast(dbRequest)); } /** * Get the number of requests in a collection * @param collectionID ID of the collection to which the request belongs * @param user User who owns the collection * @returns Number of requests in the collection */ getRequestsCountInCollection(collectionID: string): Promise { return this.prisma.userRequest.count({ where: { collectionID }, }); } /** * Create a user request * @param collectionID ID of the collection to which the request belongs * @param title title of the request * @param request request to create * @param type type of the request * @param user User who owns the request * @returns Either of the created user request */ async createRequest( collectionID: string, title: string, request: string, type: ReqType, user: AuthUser, ): Promise | E.Right> { const jsonRequest = stringToJson(request); if (E.isLeft(jsonRequest)) return E.left(jsonRequest.left); const collection = await this.userCollectionService.getUserCollection( collectionID, ); if (E.isLeft(collection)) return E.left(collection.left); if (collection.right.userUid !== user.uid) return E.left(USER_COLLECTION_NOT_FOUND); if (collection.right.type !== ReqType[type]) return E.left(USER_REQUEST_INVALID_TYPE); try { const requestCount = await this.getRequestsCountInCollection( collectionID, ); const request = await this.prisma.userRequest.create({ data: { collectionID, title, request: jsonRequest.right, type: ReqType[type], orderIndex: requestCount + 1, userUid: user.uid, }, }); const userRequest = this.cast(request); await this.pubsub.publish( `user_request/${user.uid}/created`, userRequest, ); return E.right(userRequest); } catch (err) { return E.left(USER_REQUEST_CREATION_FAILED); } } /** * Update a user request * @param id ID of the request to update * @param title title of the request to update * @param type type of the request to update * @param request request to update * @param user User who owns the request */ async updateRequest( id: string, title: string, type: ReqType, request: string, user: AuthUser, ): Promise | E.Right> { const existRequest = await this.prisma.userRequest.findFirst({ where: { id, userUid: user.uid }, }); if (!existRequest) return E.left(USER_REQUEST_NOT_FOUND); if (existRequest.type !== ReqType[type]) return E.left(USER_REQUEST_INVALID_TYPE); let jsonRequest = undefined; if (request) { const jsonRequestE = stringToJson(request); if (E.isLeft(jsonRequestE)) return E.left(jsonRequestE.left); jsonRequest = jsonRequestE.right; } const updatedRequest = await this.prisma.userRequest.update({ where: { id }, data: { title, request: jsonRequest, }, }); const userRequest: UserRequest = this.cast(updatedRequest); await this.pubsub.publish(`user_request/${user.uid}/updated`, userRequest); return E.right(userRequest); } /** * Delete a user request * @param id ID of the request to delete * @param user User who owns the request * @returns Either of a boolean */ async deleteRequest( id: string, user: AuthUser, ): Promise | E.Right> { const request = await this.prisma.userRequest.findFirst({ where: { id, userUid: user.uid }, }); if (!request) return E.left(USER_REQUEST_NOT_FOUND); await this.prisma.userRequest.updateMany({ where: { collectionID: request.collectionID, orderIndex: { gt: request.orderIndex }, }, data: { orderIndex: { decrement: 1 } }, }); await this.prisma.userRequest.delete({ where: { id } }); await this.pubsub.publish( `user_request/${user.uid}/deleted`, this.cast(request), ); return E.right(true); } /** * Move a request for re-ordering inside/across collections * @param srcCollID ID of the source collection * @param destCollID ID of the destination collection * @param requestID ID of the request to move * @param nextRequestID ID of the request after which the request should be moved * @param user User who owns the request * @returns Either of the updated request */ async moveRequest( srcCollID: string, destCollID: string, requestID: string, nextRequestID: string, user: AuthUser, ): Promise | E.Right> { const twoRequests = await this.findRequestAndNextRequest( srcCollID, destCollID, requestID, nextRequestID, user, ); if (E.isLeft(twoRequests)) return twoRequests; const { request: dbRequest, nextRequest: dbNextRequest } = twoRequests.right; const isTypeValidate = await this.validateTypeEqualityForMoveRequest( srcCollID, destCollID, dbRequest, dbNextRequest, ); if (E.isLeft(isTypeValidate)) return E.left(isTypeValidate.left); const updatedRequest = await this.reorderRequests( srcCollID, dbRequest, destCollID, dbNextRequest, ); if (E.isLeft(updatedRequest)) return updatedRequest; const userRequest: UserRequest = this.cast(updatedRequest.right); await this.pubsub.publish(`user_request/${user.uid}/moved`, { request: userRequest, nextRequest: dbNextRequest ? this.cast(dbNextRequest) : null, }); return E.right(userRequest); } /** * This function validate/ensure the same type (REST/GQL) in the source and destination collection and the request * @param srcCollID ID of the source collection * @param destCollID ID of the destination collection * @param request Request to move * @param nextRequest Request after which the request should be moved * @returns Either of a boolean */ async validateTypeEqualityForMoveRequest( srcCollID, destCollID, request, nextRequest, ) { const collections = await Promise.all([ this.userCollectionService.getUserCollection(srcCollID), this.userCollectionService.getUserCollection(destCollID), ]); const srcColl = collections[0]; if (E.isLeft(srcColl)) return E.left(srcColl.left); const destColl = collections[1]; if (E.isLeft(destColl)) return E.left(destColl.left); if ( srcColl.right.type !== destColl.right.type || (nextRequest && request.type !== nextRequest.type) ) { return E.left(USER_REQUEST_INVALID_TYPE); } return E.right(true); } /** * A helper function. * Find the request and the next request(destination collection) * @param srcCollID Source collection ID * @param destCollID Destination collection ID * @param requestID Request ID * @param nextRequestID Next request ID * @param user User who owns the collection * @returns Either Left with error message or Right with the request and the next request */ async findRequestAndNextRequest( srcCollID: string, destCollID: string, requestID: string, nextRequestID: string, user: AuthUser, ): Promise< | E.Left | E.Right<{ request: DbUserRequest; nextRequest: DbUserRequest; }> > { const request = await this.prisma.userRequest.findFirst({ where: { id: requestID, collectionID: srcCollID, userUid: user.uid }, }); if (!request) return E.left(USER_REQUEST_NOT_FOUND); let nextRequest: DbUserRequest = null; if (nextRequestID) { nextRequest = await this.prisma.userRequest.findFirst({ where: { id: nextRequestID, collectionID: destCollID, userUid: user.uid, }, }); if (!nextRequest) return E.left(USER_REQUEST_NOT_FOUND); } return E.right({ request, nextRequest }); } /** * Update order indexes of requests in collection * @param srcCollID - id of collection, where the request is moving from * @param request - request to be moved * @param destCollID - id of collection, where the request is moving to * @param nextRequest - request that comes after the updated request in its new position * @returns Promise of an Either of `DbUserRequest` object or error message */ async reorderRequests( srcCollID: string, request: DbUserRequest, destCollID: string, nextRequest: DbUserRequest, ): Promise | E.Right> { try { return await this.prisma.$transaction< E.Left | E.Right >(async (tx) => { const isSameCollection = srcCollID === destCollID; const isMovingUp = nextRequest?.orderIndex < request.orderIndex; // false, if nextRequest is null const nextReqOrderIndex = nextRequest?.orderIndex; const reqCountInDestColl = nextRequest ? undefined : await this.getRequestsCountInCollection(destCollID); // Updating order indexes of other requests in collection(s) if (isSameCollection) { const updateFrom = isMovingUp ? nextReqOrderIndex : request.orderIndex + 1; const updateTo = isMovingUp ? request.orderIndex : nextReqOrderIndex; await tx.userRequest.updateMany({ where: { collectionID: srcCollID, orderIndex: { gte: updateFrom, lt: updateTo }, }, data: { orderIndex: isMovingUp ? { increment: 1 } : { decrement: 1 }, }, }); } else { await tx.userRequest.updateMany({ where: { collectionID: srcCollID, orderIndex: { gte: request.orderIndex }, }, data: { orderIndex: { decrement: 1 } }, }); if (nextRequest) { await tx.userRequest.updateMany({ where: { collectionID: destCollID, orderIndex: { gte: nextReqOrderIndex }, }, data: { orderIndex: { increment: 1 } }, }); } } // Updating order index of the request let adjust: number; if (isSameCollection) adjust = nextRequest ? (isMovingUp ? 0 : -1) : 0; else adjust = nextRequest ? 0 : 1; const newOrderIndex = (nextReqOrderIndex ?? reqCountInDestColl) + adjust; const updatedRequest = await tx.userRequest.update({ where: { id: request.id }, data: { orderIndex: newOrderIndex, collectionID: destCollID }, }); return E.right(updatedRequest); }); } catch (err) { return E.left(USER_REQUEST_REORDERING_FAILED); } } }