453 lines
13 KiB
TypeScript
453 lines
13 KiB
TypeScript
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.Left<string> | E.Right<UserRequest>> {
|
|
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<number> {
|
|
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.Left<string> | E.Right<UserRequest>> {
|
|
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.Left<string> | E.Right<UserRequest>> {
|
|
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.Left<string> | E.Right<boolean>> {
|
|
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.Left<string> | E.Right<UserRequest>> {
|
|
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<string>
|
|
| 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.Left<string> | E.Right<DbUserRequest>> {
|
|
try {
|
|
return await this.prisma.$transaction<
|
|
E.Left<string> | E.Right<DbUserRequest>
|
|
>(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);
|
|
}
|
|
}
|
|
}
|