fix(backend): prevent IDOR in user collection and request endpoints (#5902)
This commit is contained in:
parent
02b3dbcf5c
commit
57be05cdcb
10 changed files with 329 additions and 230 deletions
|
|
@ -787,15 +787,13 @@ export const INFRA_CONFIG_OPERATION_NOT_ALLOWED =
|
|||
* Error message for when the onboarding status fetch fails
|
||||
* (InfraConfigService)
|
||||
*/
|
||||
export const INFRA_CONFIG_FETCH_FAILED =
|
||||
'infra_config/fetch_failed' as const;
|
||||
export const INFRA_CONFIG_FETCH_FAILED = 'infra_config/fetch_failed' as const;
|
||||
|
||||
/**
|
||||
* Onboarding has already been completed and cannot be re-run
|
||||
* (OnboardingController)
|
||||
*/
|
||||
export const ONBOARDING_CANNOT_BE_RERUN =
|
||||
'onboarding/cannot_be_rerun' as const;
|
||||
export const ONBOARDING_CANNOT_BE_RERUN = 'onboarding/cannot_be_rerun' as const;
|
||||
|
||||
/**
|
||||
* Error message for when the database table does not exist
|
||||
|
|
|
|||
|
|
@ -893,7 +893,10 @@ describe('deleteCollection', () => {
|
|||
.spyOn(teamCollectionService, 'getCollection')
|
||||
.mockResolvedValueOnce(E.right(rootTeamCollection));
|
||||
jest
|
||||
.spyOn(teamCollectionService as any, 'deleteCollectionAndUpdateSiblingsOrderIndex')
|
||||
.spyOn(
|
||||
teamCollectionService as any,
|
||||
'deleteCollectionAndUpdateSiblingsOrderIndex',
|
||||
)
|
||||
.mockResolvedValueOnce(E.left(TEAM_COL_REORDERING_FAILED));
|
||||
|
||||
const result = await teamCollectionService.deleteCollection(
|
||||
|
|
@ -1667,23 +1670,6 @@ describe('FIX: updateMany queries now include teamID filter for root collections
|
|||
mockReset(mockPrisma);
|
||||
});
|
||||
|
||||
const team2: Team = {
|
||||
id: 'team_2',
|
||||
name: 'Team 2',
|
||||
};
|
||||
|
||||
// Team 2's root collection - should NOT be affected by Team 1's operations
|
||||
const team2RootCollection: DBTeamCollection = {
|
||||
id: 'team2-root-coll',
|
||||
orderIndex: 1,
|
||||
parentID: null,
|
||||
title: 'Team 2 Root Collection',
|
||||
teamID: team2.id,
|
||||
data: {},
|
||||
createdOn: currentTime,
|
||||
updatedOn: currentTime,
|
||||
};
|
||||
|
||||
test('FIX: deleteCollection - updateMany now correctly filters by teamID for root collections', async () => {
|
||||
/**
|
||||
* Scenario: Team 1 deletes a root collection
|
||||
|
|
@ -1717,7 +1703,8 @@ describe('FIX: updateMany queries now include teamID filter for root collections
|
|||
await teamCollectionService.deleteCollection(team1RootToDelete.id);
|
||||
|
||||
// Get the updateMany call from the transaction
|
||||
const updateManyCall = mockPrisma.teamCollection.updateMany.mock.calls[0][0];
|
||||
const updateManyCall =
|
||||
mockPrisma.teamCollection.updateMany.mock.calls[0][0];
|
||||
|
||||
// FIX VERIFICATION: The query now correctly includes teamID
|
||||
// This ensures only Team 1's root collections are affected
|
||||
|
|
@ -1769,7 +1756,8 @@ describe('FIX: updateMany queries now include teamID filter for root collections
|
|||
);
|
||||
|
||||
// Get the actual updateMany call arguments
|
||||
const updateManyCall = mockPrisma.teamCollection.updateMany.mock.calls[0][0];
|
||||
const updateManyCall =
|
||||
mockPrisma.teamCollection.updateMany.mock.calls[0][0];
|
||||
|
||||
// FIX VERIFICATION: The query now correctly includes teamID
|
||||
expect(updateManyCall.where).toEqual({
|
||||
|
|
@ -1830,7 +1818,8 @@ describe('FIX: updateMany queries now include teamID filter for root collections
|
|||
);
|
||||
|
||||
// Get the actual updateMany call arguments
|
||||
const updateManyCall = mockPrisma.teamCollection.updateMany.mock.calls[0][0];
|
||||
const updateManyCall =
|
||||
mockPrisma.teamCollection.updateMany.mock.calls[0][0];
|
||||
|
||||
// FIX VERIFICATION: The query now correctly includes teamID
|
||||
expect(updateManyCall.where).toEqual({
|
||||
|
|
|
|||
|
|
@ -216,7 +216,11 @@ export class TeamCollectionService {
|
|||
await this.prisma.$transaction(async (tx) => {
|
||||
try {
|
||||
// lock the rows
|
||||
await this.prisma.lockTeamCollectionByTeamAndParent(tx, teamID, parentID);
|
||||
await this.prisma.lockTeamCollectionByTeamAndParent(
|
||||
tx,
|
||||
teamID,
|
||||
parentID,
|
||||
);
|
||||
|
||||
// Get the last order index
|
||||
const lastEntry = await tx.teamCollection.findFirst({
|
||||
|
|
@ -397,15 +401,18 @@ export class TeamCollectionService {
|
|||
* @param collectionID The collection ID
|
||||
* @returns An Either of the Collection details
|
||||
*/
|
||||
async getCollection(collectionID: string, tx: Prisma.TransactionClient | null = null) {
|
||||
async getCollection(
|
||||
collectionID: string,
|
||||
tx: Prisma.TransactionClient | null = null,
|
||||
) {
|
||||
try {
|
||||
const teamCollection = await (tx || this.prisma).teamCollection.findUniqueOrThrow(
|
||||
{
|
||||
where: {
|
||||
id: collectionID,
|
||||
},
|
||||
const teamCollection = await (
|
||||
tx || this.prisma
|
||||
).teamCollection.findUniqueOrThrow({
|
||||
where: {
|
||||
id: collectionID,
|
||||
},
|
||||
);
|
||||
});
|
||||
return E.right(teamCollection);
|
||||
} catch (error) {
|
||||
return E.left(TEAM_COLL_NOT_FOUND);
|
||||
|
|
@ -469,7 +476,11 @@ export class TeamCollectionService {
|
|||
teamCollection = await this.prisma.$transaction(async (tx) => {
|
||||
try {
|
||||
// lock the rows
|
||||
await this.prisma.lockTeamCollectionByTeamAndParent(tx, teamID, parentID);
|
||||
await this.prisma.lockTeamCollectionByTeamAndParent(
|
||||
tx,
|
||||
teamID,
|
||||
parentID,
|
||||
);
|
||||
|
||||
// fetch last collection
|
||||
const lastCollection = await tx.teamCollection.findFirst({
|
||||
|
|
@ -552,15 +563,19 @@ export class TeamCollectionService {
|
|||
await this.prisma.$transaction(async (tx) => {
|
||||
try {
|
||||
// lock the rows
|
||||
await this.prisma.lockTeamCollectionByTeamAndParent(tx, collection.teamID, collection.parentID);
|
||||
await this.prisma.lockTeamCollectionByTeamAndParent(
|
||||
tx,
|
||||
collection.teamID,
|
||||
collection.parentID,
|
||||
);
|
||||
|
||||
const deletedCollection = await tx.teamCollection.delete({
|
||||
where: { id: collection.id },
|
||||
});
|
||||
|
||||
|
||||
// if collection is deleted, update siblings orderIndexes
|
||||
// if collection was deleted before the transaction started (race condition), do not update siblings orderIndexes
|
||||
if (deletedCollection) {
|
||||
if (deletedCollection) {
|
||||
// update siblings orderIndexes
|
||||
await tx.teamCollection.updateMany({
|
||||
where: {
|
||||
|
|
@ -623,7 +638,7 @@ export class TeamCollectionService {
|
|||
);
|
||||
|
||||
return E.right(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change parentID of TeamCollection's
|
||||
|
|
@ -638,11 +653,10 @@ export class TeamCollectionService {
|
|||
newParentID: string | null,
|
||||
) {
|
||||
// fetch last collection
|
||||
const lastCollectionUnderNewParent =
|
||||
await tx.teamCollection.findFirst({
|
||||
where: { teamID: collection.teamID, parentID: newParentID },
|
||||
orderBy: { orderIndex: 'desc' },
|
||||
});
|
||||
const lastCollectionUnderNewParent = await tx.teamCollection.findFirst({
|
||||
where: { teamID: collection.teamID, parentID: newParentID },
|
||||
orderBy: { orderIndex: 'desc' },
|
||||
});
|
||||
|
||||
// decrement orderIndex of all next sibling collections from original collection
|
||||
await tx.teamCollection.updateMany({
|
||||
|
|
@ -736,7 +750,11 @@ export class TeamCollectionService {
|
|||
const collection = await this.getCollection(collectionID, tx);
|
||||
if (E.isLeft(collection)) return E.left(collection.left);
|
||||
// lock the rows of the collection and its siblings
|
||||
await this.prisma.lockTeamCollectionByTeamAndParent(tx, collection.right.teamID, collection.right.parentID);
|
||||
await this.prisma.lockTeamCollectionByTeamAndParent(
|
||||
tx,
|
||||
collection.right.teamID,
|
||||
collection.right.parentID,
|
||||
);
|
||||
// destCollectionID == null i.e move collection to root
|
||||
if (!destCollectionID) {
|
||||
if (!collection.right.parentID) {
|
||||
|
|
@ -744,7 +762,7 @@ export class TeamCollectionService {
|
|||
// Throw error if collection is already a root collection
|
||||
return E.left(TEAM_COL_ALREADY_ROOT);
|
||||
}
|
||||
|
||||
|
||||
// Change parent from child to root i.e child collection becomes a root collection
|
||||
// Move child collection into root and update orderIndexes for root teamCollections
|
||||
const updatedCollection = await this.changeParentAndUpdateOrderIndex(
|
||||
|
|
@ -752,31 +770,32 @@ export class TeamCollectionService {
|
|||
collection.right,
|
||||
null,
|
||||
);
|
||||
if (E.isLeft(updatedCollection)) return E.left(updatedCollection.left);
|
||||
|
||||
if (E.isLeft(updatedCollection))
|
||||
return E.left(updatedCollection.left);
|
||||
|
||||
this.pubsub.publish(
|
||||
`team_coll/${collection.right.teamID}/coll_moved`,
|
||||
updatedCollection.right,
|
||||
);
|
||||
|
||||
|
||||
return E.right(updatedCollection.right);
|
||||
}
|
||||
|
||||
|
||||
// destCollectionID != null i.e move into another collection
|
||||
if (collectionID === destCollectionID) {
|
||||
// Throw error if collectionID and destCollectionID are the same
|
||||
return E.left(TEAM_COLL_DEST_SAME);
|
||||
}
|
||||
|
||||
|
||||
// Get collection details of destCollectionID
|
||||
const destCollection = await this.getCollection(destCollectionID, tx);
|
||||
if (E.isLeft(destCollection)) return E.left(TEAM_COLL_NOT_FOUND);
|
||||
|
||||
|
||||
// Check if collection and destCollection belong to the same user account
|
||||
if (collection.right.teamID !== destCollection.right.teamID) {
|
||||
return E.left(TEAM_COLL_NOT_SAME_TEAM);
|
||||
}
|
||||
|
||||
|
||||
// Check if collection is present on the parent tree for destCollection
|
||||
const checkIfParent = await this.isParent(
|
||||
collection.right,
|
||||
|
|
@ -788,8 +807,12 @@ export class TeamCollectionService {
|
|||
}
|
||||
|
||||
// lock the rows of the destination collection and its siblings
|
||||
await this.prisma.lockTeamCollectionByTeamAndParent(tx, destCollection.right.teamID, destCollection.right.parentID);
|
||||
|
||||
await this.prisma.lockTeamCollectionByTeamAndParent(
|
||||
tx,
|
||||
destCollection.right.teamID,
|
||||
destCollection.right.parentID,
|
||||
);
|
||||
|
||||
// Change parent from null to teamCollection i.e collection becomes a child collection
|
||||
// Move root/child collection into another child collection and update orderIndexes of the previous parent
|
||||
const updatedCollection = await this.changeParentAndUpdateOrderIndex(
|
||||
|
|
@ -807,11 +830,7 @@ export class TeamCollectionService {
|
|||
return E.right(updatedCollection.right);
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
console.error(
|
||||
'Error from TeamCollectionService.moveCollection',
|
||||
error,
|
||||
);
|
||||
console.error('Error from TeamCollectionService.moveCollection', error);
|
||||
return E.left(TEAM_COL_REORDERING_FAILED);
|
||||
}
|
||||
}
|
||||
|
|
@ -823,7 +842,11 @@ export class TeamCollectionService {
|
|||
* @param teamID The Team ID (required when collectionID is null for root collections)
|
||||
* @returns Number of collections
|
||||
*/
|
||||
getCollectionCount(collectionID: string, teamID: string, tx: Prisma.TransactionClient | null = null): Promise<number> {
|
||||
getCollectionCount(
|
||||
collectionID: string,
|
||||
teamID: string,
|
||||
tx: Prisma.TransactionClient | null = null,
|
||||
): Promise<number> {
|
||||
return (tx || this.prisma).teamCollection.count({
|
||||
where: { parentID: collectionID, teamID: teamID },
|
||||
});
|
||||
|
|
@ -867,7 +890,7 @@ export class TeamCollectionService {
|
|||
|
||||
// if collection is found, update orderIndexes of siblings
|
||||
// if collection was deleted before the transaction started (race condition), do not update siblings orderIndexes
|
||||
if(collectionInTx) {
|
||||
if (collectionInTx) {
|
||||
// Step 1: Decrement orderIndex of all items that come after collection.orderIndex till end of list of items
|
||||
await tx.teamCollection.updateMany({
|
||||
where: {
|
||||
|
|
@ -881,7 +904,7 @@ export class TeamCollectionService {
|
|||
orderIndex: { decrement: 1 },
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
// Step 2: Update orderIndex of collection to length of list
|
||||
await tx.teamCollection.update({
|
||||
where: { id: collection.right.id },
|
||||
|
|
@ -894,7 +917,6 @@ export class TeamCollectionService {
|
|||
},
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
throw new ConflictException(error);
|
||||
}
|
||||
|
|
@ -927,7 +949,11 @@ export class TeamCollectionService {
|
|||
await this.prisma.$transaction(async (tx) => {
|
||||
try {
|
||||
// Step 0: lock the rows
|
||||
await this.prisma.lockTeamCollectionByTeamAndParent(tx, collection.right.teamID, collection.right.parentID);
|
||||
await this.prisma.lockTeamCollectionByTeamAndParent(
|
||||
tx,
|
||||
collection.right.teamID,
|
||||
collection.right.parentID,
|
||||
);
|
||||
|
||||
const collectionInTx = await tx.teamCollection.findFirst({
|
||||
where: { id: collectionID },
|
||||
|
|
@ -940,10 +966,10 @@ export class TeamCollectionService {
|
|||
|
||||
// if collection and subsequentCollection are found, update orderIndexes of siblings
|
||||
// if collection or subsequentCollection was deleted before the transaction started (race condition), do not update siblings orderIndexes
|
||||
if(collectionInTx && subsequentCollectionInTx) {
|
||||
if (collectionInTx && subsequentCollectionInTx) {
|
||||
// Step 1: Determine if we are moving collection up or down the list
|
||||
const isMovingUp =
|
||||
subsequentCollectionInTx.orderIndex < collectionInTx.orderIndex;
|
||||
subsequentCollectionInTx.orderIndex < collectionInTx.orderIndex;
|
||||
|
||||
// Step 2: Update OrderIndex of items in list depending on moving up or down
|
||||
const updateFrom = isMovingUp
|
||||
|
|
@ -1490,7 +1516,11 @@ export class TeamCollectionService {
|
|||
|
||||
try {
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
await this.prisma.lockTeamCollectionByTeamAndParent(tx, teamID, parentID);
|
||||
await this.prisma.lockTeamCollectionByTeamAndParent(
|
||||
tx,
|
||||
teamID,
|
||||
parentID,
|
||||
);
|
||||
|
||||
const collections = await tx.teamCollection.findMany({
|
||||
where: { teamID, parentID },
|
||||
|
|
|
|||
|
|
@ -120,7 +120,9 @@ export class TeamRequestService {
|
|||
await this.prisma.$transaction(async (tx) => {
|
||||
try {
|
||||
// lock the rows
|
||||
await this.prisma.lockTeamRequestByCollections(tx, dbTeamReq.teamID, [dbTeamReq.collectionID]);
|
||||
await this.prisma.lockTeamRequestByCollections(tx, dbTeamReq.teamID, [
|
||||
dbTeamReq.collectionID,
|
||||
]);
|
||||
|
||||
const deletedTeamRequest = await tx.teamRequest.delete({
|
||||
where: { id: requestID },
|
||||
|
|
@ -128,7 +130,7 @@ export class TeamRequestService {
|
|||
|
||||
// if request is deleted, update orderIndexes of siblings
|
||||
// if request was deleted before the transaction started (race condition), do not update siblings orderIndexes
|
||||
if(deletedTeamRequest) {
|
||||
if (deletedTeamRequest) {
|
||||
await tx.teamRequest.updateMany({
|
||||
where: {
|
||||
collectionID: dbTeamReq.collectionID,
|
||||
|
|
@ -181,7 +183,9 @@ export class TeamRequestService {
|
|||
dbTeamRequest = await this.prisma.$transaction(async (tx) => {
|
||||
try {
|
||||
// lock the rows
|
||||
await this.prisma.lockTeamRequestByCollections(tx, teamID, [collectionID]);
|
||||
await this.prisma.lockTeamRequestByCollections(tx, teamID, [
|
||||
collectionID,
|
||||
]);
|
||||
|
||||
// fetch last team request
|
||||
const lastTeamRequest = await tx.teamRequest.findFirst({
|
||||
|
|
@ -385,7 +389,10 @@ export class TeamRequestService {
|
|||
* A helper function to get the number of requests in a collection
|
||||
* @param collectionID Collection ID to fetch
|
||||
*/
|
||||
private async getRequestsCountInCollection(collectionID: string, tx: Prisma.TransactionClient | null = null) {
|
||||
private async getRequestsCountInCollection(
|
||||
collectionID: string,
|
||||
tx: Prisma.TransactionClient | null = null,
|
||||
) {
|
||||
return (tx || this.prisma).teamRequest.count({
|
||||
where: { collectionID },
|
||||
});
|
||||
|
|
@ -409,7 +416,10 @@ export class TeamRequestService {
|
|||
E.Left<string> | E.Right<DbTeamRequest>
|
||||
>(async (tx) => {
|
||||
// lock the rows
|
||||
await this.prisma.lockTeamRequestByCollections(tx, request.teamID, [srcCollID, destCollID]);
|
||||
await this.prisma.lockTeamRequestByCollections(tx, request.teamID, [
|
||||
srcCollID,
|
||||
destCollID,
|
||||
]);
|
||||
|
||||
request = await tx.teamRequest.findUnique({
|
||||
where: { id: request.id },
|
||||
|
|
@ -422,22 +432,24 @@ export class TeamRequestService {
|
|||
|
||||
// if request is found in transaction, update orderIndexes of siblings
|
||||
// if request was deleted before the transaction started (race condition), do not update siblings orderIndexes
|
||||
if(request) {
|
||||
if (request) {
|
||||
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, tx);
|
||||
|
||||
|
||||
// Updating order indexes of other requests in collection(s)
|
||||
if (isSameCollection) {
|
||||
const updateFrom = isMovingUp
|
||||
? nextReqOrderIndex
|
||||
: request.orderIndex + 1;
|
||||
const updateTo = isMovingUp ? request.orderIndex : nextReqOrderIndex;
|
||||
|
||||
const updateTo = isMovingUp
|
||||
? request.orderIndex
|
||||
: nextReqOrderIndex;
|
||||
|
||||
await tx.teamRequest.updateMany({
|
||||
where: {
|
||||
collectionID: srcCollID,
|
||||
|
|
@ -455,7 +467,7 @@ export class TeamRequestService {
|
|||
},
|
||||
data: { orderIndex: { decrement: 1 } },
|
||||
});
|
||||
|
||||
|
||||
if (nextRequest) {
|
||||
await tx.teamRequest.updateMany({
|
||||
where: {
|
||||
|
|
@ -466,20 +478,21 @@ export class TeamRequestService {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Updating order index of the request
|
||||
let adjust: number;
|
||||
if (isSameCollection) adjust = nextRequest ? (isMovingUp ? 0 : -1) : 0;
|
||||
if (isSameCollection)
|
||||
adjust = nextRequest ? (isMovingUp ? 0 : -1) : 0;
|
||||
else adjust = nextRequest ? 0 : 1;
|
||||
|
||||
|
||||
const newOrderIndex =
|
||||
(nextReqOrderIndex ?? reqCountInDestColl) + adjust;
|
||||
|
||||
|
||||
const updatedRequest = await tx.teamRequest.update({
|
||||
where: { id: request.id },
|
||||
data: { orderIndex: newOrderIndex, collectionID: destCollID },
|
||||
});
|
||||
|
||||
|
||||
return E.right(updatedRequest);
|
||||
}
|
||||
});
|
||||
|
|
@ -539,7 +552,9 @@ export class TeamRequestService {
|
|||
try {
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
// lock the rows
|
||||
await this.prisma.lockTeamRequestByCollections(tx, teamID, [collectionID]);
|
||||
await this.prisma.lockTeamRequestByCollections(tx, teamID, [
|
||||
collectionID,
|
||||
]);
|
||||
const teamRequests = await tx.teamRequest.findMany({
|
||||
where: { teamID, collectionID },
|
||||
orderBy,
|
||||
|
|
|
|||
|
|
@ -132,6 +132,7 @@ export class UserCollectionResolver {
|
|||
})
|
||||
@UseGuards(GqlAuthGuard)
|
||||
async userCollection(
|
||||
@GqlUser() user: AuthUser,
|
||||
@Args({
|
||||
type: () => ID,
|
||||
name: 'userCollectionID',
|
||||
|
|
@ -139,8 +140,10 @@ export class UserCollectionResolver {
|
|||
})
|
||||
userCollectionID: string,
|
||||
) {
|
||||
const userCollection =
|
||||
await this.userCollectionService.getUserCollection(userCollectionID);
|
||||
const userCollection = await this.userCollectionService.getUserCollection(
|
||||
userCollectionID,
|
||||
user.uid,
|
||||
);
|
||||
|
||||
if (E.isLeft(userCollection)) throwErr(userCollection.left);
|
||||
return <UserCollection>{
|
||||
|
|
|
|||
|
|
@ -653,6 +653,7 @@ describe('getUserCollection', () => {
|
|||
|
||||
const result = await userCollectionService.getUserCollection(
|
||||
rootRESTUserCollection.id,
|
||||
user.uid,
|
||||
);
|
||||
expect(result).toEqualRight(rootRESTUserCollection);
|
||||
});
|
||||
|
|
@ -661,7 +662,20 @@ describe('getUserCollection', () => {
|
|||
'NotFoundError',
|
||||
);
|
||||
|
||||
const result = await userCollectionService.getUserCollection('123');
|
||||
const result = await userCollectionService.getUserCollection(
|
||||
'123',
|
||||
user.uid,
|
||||
);
|
||||
expect(result).toEqualLeft(USER_COLL_NOT_FOUND);
|
||||
});
|
||||
test('should throw USER_COLL_NOT_FOUND when collectionID belongs to a different user', async () => {
|
||||
mockPrisma.userCollection.findUniqueOrThrow.mockRejectedValueOnce(
|
||||
'NotFoundError',
|
||||
);
|
||||
const result = await userCollectionService.getUserCollection(
|
||||
rootRESTUserCollection.id,
|
||||
'another-user',
|
||||
);
|
||||
expect(result).toEqualLeft(USER_COLL_NOT_FOUND);
|
||||
});
|
||||
});
|
||||
|
|
@ -729,16 +743,11 @@ describe('createUserCollection', () => {
|
|||
expect(result).toEqualLeft(USER_COLL_SHORT_TITLE);
|
||||
});
|
||||
|
||||
test('should throw USER_NOT_OWNER when user is not the owner of the collection', async () => {
|
||||
test('should throw USER_COLLECTION_CREATION_FAILED when user is not the owner of the parent collection', async () => {
|
||||
mockPrisma.$transaction.mockImplementation(async (fn) => fn(mockPrisma));
|
||||
jest
|
||||
.spyOn(userCollectionService, 'getUserCollection')
|
||||
.mockResolvedValueOnce(
|
||||
E.right({
|
||||
...rootRESTUserCollection,
|
||||
userUid: 'other-user-uid',
|
||||
}),
|
||||
);
|
||||
.mockResolvedValueOnce(E.left(USER_COLL_NOT_FOUND));
|
||||
|
||||
const result = await userCollectionService.createUserCollection(
|
||||
user,
|
||||
|
|
@ -1041,23 +1050,26 @@ describe('deleteUserCollection', () => {
|
|||
);
|
||||
expect(result).toEqualLeft(USER_COLL_NOT_FOUND);
|
||||
});
|
||||
test('should throw USER_NOT_OWNER when collectionID is invalid ', async () => {
|
||||
// getUserCollection
|
||||
mockPrisma.userCollection.findUniqueOrThrow.mockResolvedValueOnce(
|
||||
rootRESTUserCollection,
|
||||
test('should throw USER_COLL_NOT_FOUND when collectionID belongs to a different user', async () => {
|
||||
// getUserCollection (userUid is now part of the where clause, so it rejects for wrong user)
|
||||
mockPrisma.userCollection.findUniqueOrThrow.mockRejectedValueOnce(
|
||||
'NotFoundError',
|
||||
);
|
||||
const result = await userCollectionService.deleteUserCollection(
|
||||
rootRESTUserCollection.id,
|
||||
'op09',
|
||||
);
|
||||
expect(result).toEqualLeft(USER_NOT_OWNER);
|
||||
expect(result).toEqualLeft(USER_COLL_NOT_FOUND);
|
||||
});
|
||||
test('should throw USER_COLL_REORDERING_FAILED when removeCollectionAndUpdateSiblingsOrderIndex fails', async () => {
|
||||
jest
|
||||
.spyOn(userCollectionService, 'getUserCollection')
|
||||
.mockResolvedValueOnce(E.right(rootRESTUserCollection));
|
||||
jest
|
||||
.spyOn(userCollectionService as any, 'removeCollectionAndUpdateSiblingsOrderIndex')
|
||||
.spyOn(
|
||||
userCollectionService as any,
|
||||
'removeCollectionAndUpdateSiblingsOrderIndex',
|
||||
)
|
||||
.mockResolvedValueOnce(E.left(USER_COLL_REORDERING_FAILED));
|
||||
|
||||
const result = await userCollectionService.deleteUserCollection(
|
||||
|
|
@ -1115,11 +1127,11 @@ describe('moveUserCollection', () => {
|
|||
expect(result).toEqualLeft(USER_COLL_NOT_FOUND);
|
||||
});
|
||||
|
||||
test('should throw USER_NOT_OWNER if user is not owner of collection', async () => {
|
||||
test('should throw USER_COLL_NOT_FOUND if user is not owner of collection', async () => {
|
||||
mockPrisma.$transaction.mockImplementation(async (fn) => fn(mockPrisma));
|
||||
// getUserCollection
|
||||
mockPrisma.userCollection.findUniqueOrThrow.mockResolvedValueOnce(
|
||||
rootRESTUserCollection,
|
||||
// getUserCollection (userUid is now part of the where clause, so it rejects for wrong user)
|
||||
mockPrisma.userCollection.findUniqueOrThrow.mockRejectedValueOnce(
|
||||
'NotFoundError',
|
||||
);
|
||||
|
||||
const result = await userCollectionService.moveUserCollection(
|
||||
|
|
@ -1127,7 +1139,7 @@ describe('moveUserCollection', () => {
|
|||
'009',
|
||||
'op09',
|
||||
);
|
||||
expect(result).toEqualLeft(USER_NOT_OWNER);
|
||||
expect(result).toEqualLeft(USER_COLL_NOT_FOUND);
|
||||
});
|
||||
|
||||
test('should throw USER_COLL_DEST_SAME if userCollectionID and destCollectionID is the same', async () => {
|
||||
|
|
@ -1183,24 +1195,23 @@ describe('moveUserCollection', () => {
|
|||
expect(result).toEqualLeft(USER_COLL_NOT_SAME_TYPE);
|
||||
});
|
||||
|
||||
test('should throw USER_COLL_NOT_SAME_USER if userCollectionID and destCollectionID are not from the same user', async () => {
|
||||
test('should throw USER_COLL_NOT_FOUND if destCollectionID belongs to a different user', async () => {
|
||||
mockPrisma.$transaction.mockImplementation(async (fn) => fn(mockPrisma));
|
||||
// getUserCollection
|
||||
// getUserCollection for source collection
|
||||
mockPrisma.userCollection.findUniqueOrThrow.mockResolvedValueOnce(
|
||||
rootRESTUserCollection,
|
||||
);
|
||||
// getUserCollection for destCollection
|
||||
mockPrisma.userCollection.findUniqueOrThrow.mockResolvedValueOnce({
|
||||
...childRESTUserCollection_2,
|
||||
userUid: 'differentUserUid',
|
||||
});
|
||||
// getUserCollection for destCollection (userUid is now part of the where clause, so it rejects for wrong user)
|
||||
mockPrisma.userCollection.findUniqueOrThrow.mockRejectedValueOnce(
|
||||
'NotFoundError',
|
||||
);
|
||||
|
||||
const result = await userCollectionService.moveUserCollection(
|
||||
rootRESTUserCollection.id,
|
||||
childRESTUserCollection_2.id,
|
||||
user.uid,
|
||||
);
|
||||
expect(result).toEqualLeft(USER_COLL_NOT_SAME_USER);
|
||||
expect(result).toEqualLeft(USER_COLL_NOT_FOUND);
|
||||
});
|
||||
|
||||
test('should throw USER_COLL_IS_PARENT_COLL if userCollectionID is parent of destCollectionID ', async () => {
|
||||
|
|
@ -1416,10 +1427,10 @@ describe('updateUserCollectionOrder', () => {
|
|||
expect(result).toEqualLeft(USER_COLL_NOT_FOUND);
|
||||
});
|
||||
|
||||
test('should throw USER_NOT_OWNER if userUID is of a different user', async () => {
|
||||
// getUserCollection;
|
||||
mockPrisma.userCollection.findUniqueOrThrow.mockResolvedValueOnce(
|
||||
childRESTUserCollectionList[4],
|
||||
test('should throw USER_COLL_NOT_FOUND if userUID is of a different user', async () => {
|
||||
// getUserCollection (userUid is now part of the where clause, so it rejects for wrong user)
|
||||
mockPrisma.userCollection.findUniqueOrThrow.mockRejectedValueOnce(
|
||||
'NotFoundError',
|
||||
);
|
||||
|
||||
const result = await userCollectionService.updateUserCollectionOrder(
|
||||
|
|
@ -1427,7 +1438,7 @@ describe('updateUserCollectionOrder', () => {
|
|||
null,
|
||||
'op09',
|
||||
);
|
||||
expect(result).toEqualLeft(USER_NOT_OWNER);
|
||||
expect(result).toEqualLeft(USER_COLL_NOT_FOUND);
|
||||
});
|
||||
|
||||
test('should successfully move the child user-collection to the end of the list', async () => {
|
||||
|
|
@ -1803,10 +1814,13 @@ describe('FIX: updateMany queries now include userUid filter for root collection
|
|||
);
|
||||
|
||||
// Verify Alice's delete only affected Alice's collections
|
||||
const aliceDeleteCall = mockPrisma.userCollection.updateMany.mock.calls[0][0];
|
||||
const aliceDeleteCall =
|
||||
mockPrisma.userCollection.updateMany.mock.calls[0][0];
|
||||
expect(aliceDeleteCall.where.userUid).toBe(alice.uid);
|
||||
expect(aliceDeleteCall.where.parentID).toBe(null);
|
||||
expect(aliceDeleteCall.where.orderIndex).toEqual({ gt: aliceCollection2.orderIndex });
|
||||
expect(aliceDeleteCall.where.orderIndex).toEqual({
|
||||
gt: aliceCollection2.orderIndex,
|
||||
});
|
||||
|
||||
// Reset mocks for Bob's operation
|
||||
mockReset(mockPrisma);
|
||||
|
|
@ -1831,7 +1845,8 @@ describe('FIX: updateMany queries now include userUid filter for root collection
|
|||
);
|
||||
|
||||
// Verify Bob's reorder only affected Bob's collections
|
||||
const bobReorderCall = mockPrisma.userCollection.updateMany.mock.calls[0][0];
|
||||
const bobReorderCall =
|
||||
mockPrisma.userCollection.updateMany.mock.calls[0][0];
|
||||
expect(bobReorderCall.where.userUid).toBe(bob.uid);
|
||||
expect(bobReorderCall.where.parentID).toBe(null);
|
||||
});
|
||||
|
|
@ -1873,7 +1888,8 @@ describe('FIX: updateMany queries now include userUid filter for root collection
|
|||
);
|
||||
|
||||
// Verify Alice's operation is scoped to Alice
|
||||
const aliceReorderCall = mockPrisma.userCollection.updateMany.mock.calls[0][0];
|
||||
const aliceReorderCall =
|
||||
mockPrisma.userCollection.updateMany.mock.calls[0][0];
|
||||
expect(aliceReorderCall.where.userUid).toBe(alice.uid);
|
||||
expect(aliceReorderCall.where.parentID).toBe(null);
|
||||
|
||||
|
|
@ -1903,7 +1919,8 @@ describe('FIX: updateMany queries now include userUid filter for root collection
|
|||
);
|
||||
|
||||
// Verify Bob's operation is scoped to Bob
|
||||
const bobReorderCall = mockPrisma.userCollection.updateMany.mock.calls[0][0];
|
||||
const bobReorderCall =
|
||||
mockPrisma.userCollection.updateMany.mock.calls[0][0];
|
||||
expect(bobReorderCall.where.userUid).toBe(bob.uid);
|
||||
expect(bobReorderCall.where.parentID).toBe(null);
|
||||
});
|
||||
|
|
@ -1937,7 +1954,9 @@ describe('FIX: updateMany queries now include userUid filter for root collection
|
|||
.mockResolvedValueOnce(E.right(aliceChildCollection));
|
||||
|
||||
mockPrisma.$transaction.mockImplementation(async (fn) => fn(mockPrisma));
|
||||
mockPrisma.userCollection.findFirst.mockResolvedValueOnce(aliceCollection3); // Last root
|
||||
mockPrisma.userCollection.findFirst.mockResolvedValueOnce(
|
||||
aliceCollection3,
|
||||
); // Last root
|
||||
mockPrisma.userCollection.update.mockResolvedValueOnce({
|
||||
...aliceChildCollection,
|
||||
parentID: null,
|
||||
|
|
@ -1952,7 +1971,8 @@ describe('FIX: updateMany queries now include userUid filter for root collection
|
|||
);
|
||||
|
||||
// Verify Alice's move-to-root only affects Alice's collections
|
||||
const aliceMoveCall = mockPrisma.userCollection.updateMany.mock.calls[0][0];
|
||||
const aliceMoveCall =
|
||||
mockPrisma.userCollection.updateMany.mock.calls[0][0];
|
||||
expect(aliceMoveCall.where.userUid).toBe(alice.uid);
|
||||
expect(aliceMoveCall.where.parentID).toBe(aliceChildCollection.parentID);
|
||||
|
||||
|
|
@ -1976,10 +1996,13 @@ describe('FIX: updateMany queries now include userUid filter for root collection
|
|||
);
|
||||
|
||||
// Verify Bob's delete only affects Bob's collections
|
||||
const bobDeleteCall = mockPrisma.userCollection.updateMany.mock.calls[0][0];
|
||||
const bobDeleteCall =
|
||||
mockPrisma.userCollection.updateMany.mock.calls[0][0];
|
||||
expect(bobDeleteCall.where.userUid).toBe(bob.uid);
|
||||
expect(bobDeleteCall.where.parentID).toBe(null);
|
||||
expect(bobDeleteCall.where.orderIndex).toEqual({ gt: bobCollection2.orderIndex });
|
||||
expect(bobDeleteCall.where.orderIndex).toEqual({
|
||||
gt: bobCollection2.orderIndex,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -2016,7 +2039,8 @@ describe('FIX: updateMany queries now include userUid filter for root collection
|
|||
);
|
||||
|
||||
expect(mockPrisma.userCollection.updateMany).toHaveBeenCalled();
|
||||
const updateManyCall = mockPrisma.userCollection.updateMany.mock.calls[0][0];
|
||||
const updateManyCall =
|
||||
mockPrisma.userCollection.updateMany.mock.calls[0][0];
|
||||
|
||||
// FIXED: The where clause now includes userUid to prevent cross-user data corruption
|
||||
expect(updateManyCall.where).toEqual({
|
||||
|
|
@ -2061,7 +2085,8 @@ describe('FIX: updateMany queries now include userUid filter for root collection
|
|||
user.uid,
|
||||
);
|
||||
|
||||
const updateManyCall = mockPrisma.userCollection.updateMany.mock.calls[0][0];
|
||||
const updateManyCall =
|
||||
mockPrisma.userCollection.updateMany.mock.calls[0][0];
|
||||
|
||||
// FIXED: Now includes userUid - only affects current user's root collections
|
||||
expect(updateManyCall.where).toEqual({
|
||||
|
|
@ -2117,7 +2142,8 @@ describe('FIX: updateMany queries now include userUid filter for root collection
|
|||
user.uid,
|
||||
);
|
||||
|
||||
const updateManyCall = mockPrisma.userCollection.updateMany.mock.calls[0][0];
|
||||
const updateManyCall =
|
||||
mockPrisma.userCollection.updateMany.mock.calls[0][0];
|
||||
|
||||
// FIXED: Now includes userUid - only affects current user's root collections
|
||||
expect(updateManyCall.where).toEqual({
|
||||
|
|
@ -2159,7 +2185,8 @@ describe('FIX: updateMany queries now include userUid filter for root collection
|
|||
user.uid,
|
||||
);
|
||||
|
||||
const updateManyCall = mockPrisma.userCollection.updateMany.mock.calls[0][0];
|
||||
const updateManyCall =
|
||||
mockPrisma.userCollection.updateMany.mock.calls[0][0];
|
||||
|
||||
// FIXED: Now includes userUid - only affects current user's root collections
|
||||
expect(updateManyCall.where).toEqual({
|
||||
|
|
|
|||
|
|
@ -175,11 +175,17 @@ export class UserCollectionService {
|
|||
* @param collectionID The collection ID
|
||||
* @returns An Either of the Collection details
|
||||
*/
|
||||
async getUserCollection(collectionID: string, tx: Prisma.TransactionClient | null = null) {
|
||||
async getUserCollection(
|
||||
collectionID: string,
|
||||
userUid: string,
|
||||
tx: Prisma.TransactionClient | null = null,
|
||||
) {
|
||||
try {
|
||||
const userCollection = await (tx || this.prisma).userCollection.findUniqueOrThrow(
|
||||
{ where: { id: collectionID } },
|
||||
);
|
||||
const userCollection = await (
|
||||
tx || this.prisma
|
||||
).userCollection.findUniqueOrThrow({
|
||||
where: { id: collectionID, userUid },
|
||||
});
|
||||
return E.right(userCollection);
|
||||
} catch (error) {
|
||||
return E.left(USER_COLL_NOT_FOUND);
|
||||
|
|
@ -212,21 +218,19 @@ export class UserCollectionService {
|
|||
data = jsonReq.right;
|
||||
}
|
||||
|
||||
|
||||
|
||||
let userCollection: UserCollection = null;
|
||||
try {
|
||||
userCollection = await this.prisma.$transaction(async (tx) => {
|
||||
try {
|
||||
// If creating a child collection
|
||||
if (parentID !== null) {
|
||||
const parentCollection = await this.getUserCollection(parentID, tx);
|
||||
const parentCollection = await this.getUserCollection(
|
||||
parentID,
|
||||
user.uid,
|
||||
tx,
|
||||
);
|
||||
if (E.isLeft(parentCollection)) throw Error(parentCollection.left);
|
||||
|
||||
// Check to see if parentUserCollectionID belongs to this User
|
||||
if (parentCollection.right.userUid !== user.uid)
|
||||
throw Error(USER_NOT_OWNER);
|
||||
|
||||
// Check to see if parent collection is of the same type of new collection being created
|
||||
if (parentCollection.right.type !== type)
|
||||
throw Error(USER_COLL_NOT_SAME_TYPE);
|
||||
|
|
@ -234,7 +238,6 @@ export class UserCollectionService {
|
|||
// lock the rows
|
||||
await this.prisma.lockUserCollectionByParent(tx, user.uid, parentID);
|
||||
|
||||
|
||||
// fetch last user collection
|
||||
const lastUserCollection = await tx.userCollection.findFirst({
|
||||
where: { userUid: user.uid, parentID },
|
||||
|
|
@ -391,12 +394,9 @@ export class UserCollectionService {
|
|||
*/
|
||||
async deleteUserCollection(collectionID: string, userID: string) {
|
||||
// Get collection details of collectionID
|
||||
const collection = await this.getUserCollection(collectionID);
|
||||
const collection = await this.getUserCollection(collectionID, userID);
|
||||
if (E.isLeft(collection)) return E.left(USER_COLL_NOT_FOUND);
|
||||
|
||||
// Check to see is the collection belongs to the user
|
||||
if (collection.right.userUid !== userID) return E.left(USER_NOT_OWNER);
|
||||
|
||||
// Delete all child collections and requests in the collection
|
||||
const isDeleted = await this.removeCollectionAndUpdateSiblingsOrderIndex(
|
||||
collection.right,
|
||||
|
|
@ -427,11 +427,10 @@ export class UserCollectionService {
|
|||
newParentID: string | null,
|
||||
) {
|
||||
// fetch last collection
|
||||
const lastCollectionUnderNewParent =
|
||||
await tx.userCollection.findFirst({
|
||||
where: { userUid: collection.userUid, parentID: newParentID },
|
||||
orderBy: { orderIndex: 'desc' },
|
||||
});
|
||||
const lastCollectionUnderNewParent = await tx.userCollection.findFirst({
|
||||
where: { userUid: collection.userUid, parentID: newParentID },
|
||||
orderBy: { orderIndex: 'desc' },
|
||||
});
|
||||
|
||||
// update collection's parentID and orderIndex
|
||||
const updatedCollection = await tx.userCollection.update({
|
||||
|
|
@ -483,6 +482,7 @@ export class UserCollectionService {
|
|||
// Get collection details of collection one step above in the tree i.e the parent collection
|
||||
const parentCollection = await this.getUserCollection(
|
||||
destCollection.parentID,
|
||||
destCollection.userUid,
|
||||
tx,
|
||||
);
|
||||
if (E.isLeft(parentCollection)) {
|
||||
|
|
@ -514,7 +514,11 @@ export class UserCollectionService {
|
|||
await this.prisma.$transaction(async (tx) => {
|
||||
try {
|
||||
// lock the rows
|
||||
await this.prisma.lockUserCollectionByParent(tx, userID, collection.parentID);
|
||||
await this.prisma.lockUserCollectionByParent(
|
||||
tx,
|
||||
userID,
|
||||
collection.parentID,
|
||||
);
|
||||
|
||||
const deletedCollection = await tx.userCollection.delete({
|
||||
where: { id: collection.id },
|
||||
|
|
@ -577,12 +581,13 @@ export class UserCollectionService {
|
|||
try {
|
||||
return await this.prisma.$transaction(async (tx) => {
|
||||
// Get collection details of collectionID
|
||||
const collection = await this.getUserCollection(userCollectionID, tx);
|
||||
const collection = await this.getUserCollection(
|
||||
userCollectionID,
|
||||
userID,
|
||||
tx,
|
||||
);
|
||||
if (E.isLeft(collection)) return E.left(USER_COLL_NOT_FOUND);
|
||||
|
||||
// Check to see is the collection belongs to the user
|
||||
if (collection.right.userUid !== userID) return E.left(USER_NOT_OWNER);
|
||||
|
||||
|
||||
// destCollectionID == null i.e move collection to root
|
||||
if (!destCollectionID) {
|
||||
if (!collection.right.parentID) {
|
||||
|
|
@ -590,7 +595,7 @@ export class UserCollectionService {
|
|||
// Throw error if collection is already a root collection
|
||||
return E.left(USER_COLL_ALREADY_ROOT);
|
||||
}
|
||||
|
||||
|
||||
// Change parent from child to root i.e child collection becomes a root collection
|
||||
// Move child collection into root and update orderIndexes for child userCollections
|
||||
const updatedCollection = await this.changeParentAndUpdateOrderIndex(
|
||||
|
|
@ -598,36 +603,36 @@ export class UserCollectionService {
|
|||
collection.right,
|
||||
null,
|
||||
);
|
||||
if (E.isLeft(updatedCollection)) return E.left(updatedCollection.left);
|
||||
|
||||
if (E.isLeft(updatedCollection))
|
||||
return E.left(updatedCollection.left);
|
||||
|
||||
this.pubsub.publish(
|
||||
`user_coll/${collection.right.userUid}/moved`,
|
||||
this.cast(updatedCollection.right),
|
||||
);
|
||||
|
||||
|
||||
return E.right(this.cast(updatedCollection.right));
|
||||
}
|
||||
|
||||
|
||||
// destCollectionID != null i.e move into another collection
|
||||
if (userCollectionID === destCollectionID) {
|
||||
// Throw error if collectionID and destCollectionID are the same
|
||||
return E.left(USER_COLL_DEST_SAME);
|
||||
}
|
||||
|
||||
|
||||
// Get collection details of destCollectionID
|
||||
const destCollection = await this.getUserCollection(destCollectionID, tx);
|
||||
const destCollection = await this.getUserCollection(
|
||||
destCollectionID,
|
||||
userID,
|
||||
tx,
|
||||
);
|
||||
if (E.isLeft(destCollection)) return E.left(USER_COLL_NOT_FOUND);
|
||||
|
||||
|
||||
// Check if collection and destCollection belong to the same collection type
|
||||
if (collection.right.type !== destCollection.right.type) {
|
||||
return E.left(USER_COLL_NOT_SAME_TYPE);
|
||||
}
|
||||
|
||||
// Check if collection and destCollection belong to the same user account
|
||||
if (collection.right.userUid !== destCollection.right.userUid) {
|
||||
return E.left(USER_COLL_NOT_SAME_USER);
|
||||
}
|
||||
|
||||
|
||||
// Check if collection is present on the parent tree for destCollection
|
||||
const checkIfParent = await this.isParent(
|
||||
collection.right,
|
||||
|
|
@ -637,7 +642,7 @@ export class UserCollectionService {
|
|||
if (O.isNone(checkIfParent)) {
|
||||
return E.left(USER_COLL_IS_PARENT_COLL);
|
||||
}
|
||||
|
||||
|
||||
// Change parent from null to teamCollection i.e collection becomes a child collection
|
||||
// Move root/child collection into another child collection and update orderIndexes of the previous parent
|
||||
const updatedCollection = await this.changeParentAndUpdateOrderIndex(
|
||||
|
|
@ -646,16 +651,15 @@ export class UserCollectionService {
|
|||
destCollection.right.id,
|
||||
);
|
||||
if (E.isLeft(updatedCollection)) return E.left(updatedCollection.left);
|
||||
|
||||
|
||||
this.pubsub.publish(
|
||||
`user_coll/${collection.right.userUid}/moved`,
|
||||
this.cast(updatedCollection.right),
|
||||
);
|
||||
|
||||
|
||||
return E.right(this.cast(updatedCollection.right));
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
console.error(
|
||||
'Error from UserCollectionService.moveUserCollection',
|
||||
error,
|
||||
|
|
@ -671,7 +675,11 @@ export class UserCollectionService {
|
|||
* @param userUid The User UID
|
||||
* @returns Number of collections
|
||||
*/
|
||||
getCollectionCount(collectionID: string, userUid: string, tx: Prisma.TransactionClient | null = null): Promise<number> {
|
||||
getCollectionCount(
|
||||
collectionID: string,
|
||||
userUid: string,
|
||||
tx: Prisma.TransactionClient | null = null,
|
||||
): Promise<number> {
|
||||
return (tx || this.prisma).userCollection.count({
|
||||
where: {
|
||||
parentID: collectionID,
|
||||
|
|
@ -698,19 +706,20 @@ export class UserCollectionService {
|
|||
return E.left(USER_COLL_SAME_NEXT_COLL);
|
||||
|
||||
// Get collection details of collectionID
|
||||
const collection = await this.getUserCollection(collectionID);
|
||||
const collection = await this.getUserCollection(collectionID, userID);
|
||||
if (E.isLeft(collection)) return E.left(USER_COLL_NOT_FOUND);
|
||||
|
||||
// Check to see is the collection belongs to the user
|
||||
if (collection.right.userUid !== userID) return E.left(USER_NOT_OWNER);
|
||||
|
||||
if (!nextCollectionID) {
|
||||
// nextCollectionID == null i.e move collection to the end of the list
|
||||
try {
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
try {
|
||||
// Step 0: lock the rows
|
||||
await this.prisma.lockUserCollectionByParent(tx, userID, collection.right.parentID);
|
||||
await this.prisma.lockUserCollectionByParent(
|
||||
tx,
|
||||
userID,
|
||||
collection.right.parentID,
|
||||
);
|
||||
|
||||
const collectionInTx = await tx.userCollection.findFirst({
|
||||
where: { id: collectionID },
|
||||
|
|
@ -719,7 +728,7 @@ export class UserCollectionService {
|
|||
|
||||
// if collection is found, update orderIndexes of siblings
|
||||
// if collection was deleted before the transaction started (race condition), do not update siblings orderIndexes
|
||||
if(collectionInTx) {
|
||||
if (collectionInTx) {
|
||||
// Step 1: Decrement orderIndex of all items that come after collection.orderIndex till end of list of items
|
||||
await tx.userCollection.updateMany({
|
||||
where: {
|
||||
|
|
@ -729,7 +738,7 @@ export class UserCollectionService {
|
|||
},
|
||||
data: { orderIndex: { decrement: 1 } },
|
||||
});
|
||||
|
||||
|
||||
// Step 2: Update orderIndex of collection to length of list
|
||||
await tx.userCollection.update({
|
||||
where: { id: collection.right.id },
|
||||
|
|
@ -742,7 +751,6 @@ export class UserCollectionService {
|
|||
},
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
throw new ConflictException(error);
|
||||
}
|
||||
|
|
@ -764,7 +772,10 @@ export class UserCollectionService {
|
|||
|
||||
// nextCollectionID != null i.e move to a certain position
|
||||
// Get collection details of nextCollectionID
|
||||
const subsequentCollection = await this.getUserCollection(nextCollectionID);
|
||||
const subsequentCollection = await this.getUserCollection(
|
||||
nextCollectionID,
|
||||
userID,
|
||||
);
|
||||
if (E.isLeft(subsequentCollection)) return E.left(USER_COLL_NOT_FOUND);
|
||||
|
||||
if (collection.right.userUid !== subsequentCollection.right.userUid)
|
||||
|
|
@ -779,7 +790,11 @@ export class UserCollectionService {
|
|||
await this.prisma.$transaction(async (tx) => {
|
||||
try {
|
||||
// Step 0: lock the rows
|
||||
await this.prisma.lockUserCollectionByParent(tx, userID, subsequentCollection.right.parentID);
|
||||
await this.prisma.lockUserCollectionByParent(
|
||||
tx,
|
||||
userID,
|
||||
subsequentCollection.right.parentID,
|
||||
);
|
||||
|
||||
// subsequentCollectionInTx and subsequentCollection are same, just to make sure, orderIndex value is concrete
|
||||
const collectionInTx = await tx.userCollection.findFirst({
|
||||
|
|
@ -793,20 +808,20 @@ export class UserCollectionService {
|
|||
|
||||
// if collection and subsequentCollection are found, update orderIndexes of siblings
|
||||
// if collection or subsequentCollection was deleted before the transaction started (race condition), do not update siblings orderIndexes
|
||||
if(collectionInTx && subsequentCollectionInTx) {
|
||||
if (collectionInTx && subsequentCollectionInTx) {
|
||||
// Step 1: Determine if we are moving collection up or down the list
|
||||
const isMovingUp =
|
||||
subsequentCollectionInTx.orderIndex < collectionInTx.orderIndex;
|
||||
|
||||
|
||||
// Step 2: Update OrderIndex of items in list depending on moving up or down
|
||||
const updateFrom = isMovingUp
|
||||
? subsequentCollectionInTx.orderIndex
|
||||
: collectionInTx.orderIndex + 1;
|
||||
|
||||
|
||||
const updateTo = isMovingUp
|
||||
? collectionInTx.orderIndex - 1
|
||||
: subsequentCollectionInTx.orderIndex - 1;
|
||||
|
||||
|
||||
await tx.userCollection.updateMany({
|
||||
where: {
|
||||
userUid: collection.right.userUid,
|
||||
|
|
@ -817,7 +832,7 @@ export class UserCollectionService {
|
|||
orderIndex: isMovingUp ? { increment: 1 } : { decrement: 1 },
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
// Step 3: Update OrderIndex of collection
|
||||
await tx.userCollection.update({
|
||||
where: { id: collection.right.id },
|
||||
|
|
@ -828,7 +843,6 @@ export class UserCollectionService {
|
|||
},
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
throw new ConflictException(error);
|
||||
}
|
||||
|
|
@ -860,7 +874,7 @@ export class UserCollectionService {
|
|||
collectionID: string,
|
||||
): Promise<E.Left<string> | E.Right<CollectionFolder>> {
|
||||
// Get Collection details
|
||||
const collection = await this.getUserCollection(collectionID);
|
||||
const collection = await this.getUserCollection(collectionID, userUID);
|
||||
if (E.isLeft(collection)) return E.left(collection.left);
|
||||
|
||||
const childrenCollectionObjects: CollectionFolder[] = [];
|
||||
|
|
@ -955,7 +969,10 @@ export class UserCollectionService {
|
|||
// If collectionID is not null, return JSON stringified data for specific collection
|
||||
if (collectionID) {
|
||||
// Get Details of collection
|
||||
const parentCollection = await this.getUserCollection(collectionID);
|
||||
const parentCollection = await this.getUserCollection(
|
||||
collectionID,
|
||||
userUID,
|
||||
);
|
||||
if (E.isLeft(parentCollection)) return E.left(parentCollection.left);
|
||||
|
||||
if (parentCollection.right.type !== reqType)
|
||||
|
|
@ -1083,13 +1100,12 @@ export class UserCollectionService {
|
|||
|
||||
// Check to see if destCollectionID belongs to this User
|
||||
if (destCollectionID) {
|
||||
const parentCollection = await this.getUserCollection(destCollectionID);
|
||||
const parentCollection = await this.getUserCollection(
|
||||
destCollectionID,
|
||||
userID,
|
||||
);
|
||||
if (E.isLeft(parentCollection)) return E.left(parentCollection.left);
|
||||
|
||||
// Check to see if parentUserCollectionID belongs to this User
|
||||
if (parentCollection.right.userUid !== userID)
|
||||
return E.left(USER_NOT_OWNER);
|
||||
|
||||
// Check to see if parent collection is of the same type of new collection being created
|
||||
if (parentCollection.right.type !== reqType)
|
||||
return E.left(USER_COLL_NOT_SAME_TYPE);
|
||||
|
|
@ -1101,7 +1117,11 @@ export class UserCollectionService {
|
|||
await this.prisma.$transaction(async (tx) => {
|
||||
try {
|
||||
// lock the rows
|
||||
await this.prisma.lockUserCollectionByParent(tx, userID, destCollectionID);
|
||||
await this.prisma.lockUserCollectionByParent(
|
||||
tx,
|
||||
userID,
|
||||
destCollectionID,
|
||||
);
|
||||
|
||||
// Get the last order index
|
||||
const lastCollection = await tx.userCollection.findFirst({
|
||||
|
|
@ -1147,6 +1167,7 @@ export class UserCollectionService {
|
|||
if (isCollectionDuplication) {
|
||||
const duplicatedCollectionData = await this.fetchCollectionData(
|
||||
userCollections[0].id,
|
||||
userID,
|
||||
);
|
||||
if (E.isRight(duplicatedCollectionData)) {
|
||||
this.pubsub.publish(
|
||||
|
|
@ -1233,10 +1254,9 @@ export class UserCollectionService {
|
|||
userID: string,
|
||||
reqType: DBReqType,
|
||||
) {
|
||||
const collection = await this.getUserCollection(collectionID);
|
||||
const collection = await this.getUserCollection(collectionID, userID);
|
||||
if (E.isLeft(collection)) return E.left(USER_COLL_NOT_FOUND);
|
||||
|
||||
if (collection.right.userUid !== userID) return E.left(USER_NOT_OWNER);
|
||||
if (collection.right.type !== reqType)
|
||||
return E.left(USER_COLL_NOT_SAME_TYPE);
|
||||
|
||||
|
|
@ -1272,8 +1292,9 @@ export class UserCollectionService {
|
|||
*/
|
||||
private async fetchCollectionData(
|
||||
collectionID: string,
|
||||
userID: string,
|
||||
): Promise<E.Left<string> | E.Right<UserCollectionDuplicatedData>> {
|
||||
const collection = await this.getUserCollection(collectionID);
|
||||
const collection = await this.getUserCollection(collectionID, userID);
|
||||
if (E.isLeft(collection)) return E.left(collection.left);
|
||||
|
||||
const { id, title, data, type, parentID, userUid } = collection.right;
|
||||
|
|
@ -1291,7 +1312,7 @@ export class UserCollectionService {
|
|||
]);
|
||||
|
||||
const childCollectionDataList = await Promise.all(
|
||||
childCollections.map(({ id }) => this.fetchCollectionData(id)),
|
||||
childCollections.map(({ id }) => this.fetchCollectionData(id, userID)),
|
||||
);
|
||||
|
||||
const failedChildData = childCollectionDataList.find(E.isLeft);
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ export class UserEnvironmentsResolver {
|
|||
id,
|
||||
name,
|
||||
variables,
|
||||
user
|
||||
user,
|
||||
);
|
||||
if (E.isLeft(userEnvironment)) throwErr(userEnvironment.left);
|
||||
return userEnvironment.right;
|
||||
|
|
|
|||
|
|
@ -849,6 +849,7 @@ describe('UserRequestService', () => {
|
|||
destCollID,
|
||||
userRequests[0],
|
||||
userRequests[1],
|
||||
user,
|
||||
);
|
||||
|
||||
expect(result).resolves.toEqualRight(true);
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import {
|
|||
UserRequest as DbUserRequest,
|
||||
} from 'src/generated/prisma/client';
|
||||
import {
|
||||
USER_COLLECTION_NOT_FOUND,
|
||||
USER_REQUEST_CREATION_FAILED,
|
||||
USER_REQUEST_INVALID_TYPE,
|
||||
USER_REQUEST_NOT_FOUND,
|
||||
|
|
@ -99,7 +98,10 @@ export class UserRequestService {
|
|||
* @param user User who owns the collection
|
||||
* @returns Number of requests in the collection
|
||||
*/
|
||||
getRequestsCountInCollection(collectionID: string, tx: Prisma.TransactionClient | null = null): Promise<number> {
|
||||
getRequestsCountInCollection(
|
||||
collectionID: string,
|
||||
tx: Prisma.TransactionClient | null = null,
|
||||
): Promise<number> {
|
||||
return (tx || this.prisma).userRequest.count({
|
||||
where: { collectionID },
|
||||
});
|
||||
|
|
@ -124,13 +126,12 @@ export class UserRequestService {
|
|||
const jsonRequest = stringToJson(request);
|
||||
if (E.isLeft(jsonRequest)) return E.left(jsonRequest.left);
|
||||
|
||||
const collection =
|
||||
await this.userCollectionService.getUserCollection(collectionID);
|
||||
const collection = await this.userCollectionService.getUserCollection(
|
||||
collectionID,
|
||||
user.uid,
|
||||
);
|
||||
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);
|
||||
|
||||
|
|
@ -139,7 +140,9 @@ export class UserRequestService {
|
|||
newRequest = await this.prisma.$transaction(async (tx) => {
|
||||
try {
|
||||
// lock the rows
|
||||
await this.prisma.lockUserRequestByCollections(tx, user.uid, [collectionID]);
|
||||
await this.prisma.lockUserRequestByCollections(tx, user.uid, [
|
||||
collectionID,
|
||||
]);
|
||||
|
||||
// fetch last user request
|
||||
const lastUserRequest = await tx.userRequest.findFirst({
|
||||
|
|
@ -235,13 +238,15 @@ export class UserRequestService {
|
|||
await this.prisma.$transaction(async (tx) => {
|
||||
try {
|
||||
// lock the rows
|
||||
await this.prisma.lockUserRequestByCollections(tx, user.uid, [request.collectionID]);
|
||||
await this.prisma.lockUserRequestByCollections(tx, user.uid, [
|
||||
request.collectionID,
|
||||
]);
|
||||
|
||||
const deletedRequest = await tx.userRequest.delete({ where: { id } });
|
||||
|
||||
// if request is found, update orderIndexes of siblings
|
||||
// if request was deleted before the transaction started (race condition), do not update siblings orderIndexes
|
||||
if(deletedRequest) {
|
||||
if (deletedRequest) {
|
||||
await tx.userRequest.updateMany({
|
||||
where: {
|
||||
collectionID: request.collectionID,
|
||||
|
|
@ -298,6 +303,7 @@ export class UserRequestService {
|
|||
destCollID,
|
||||
dbRequest,
|
||||
dbNextRequest,
|
||||
user,
|
||||
);
|
||||
if (E.isLeft(isTypeValidate)) return E.left(isTypeValidate.left);
|
||||
|
||||
|
|
@ -332,10 +338,11 @@ export class UserRequestService {
|
|||
destCollID,
|
||||
request,
|
||||
nextRequest,
|
||||
user: AuthUser,
|
||||
) {
|
||||
const collections = await Promise.all([
|
||||
this.userCollectionService.getUserCollection(srcCollID),
|
||||
this.userCollectionService.getUserCollection(destCollID),
|
||||
this.userCollectionService.getUserCollection(srcCollID, user.uid),
|
||||
this.userCollectionService.getUserCollection(destCollID, user.uid),
|
||||
]);
|
||||
|
||||
const srcColl = collections[0];
|
||||
|
|
@ -416,7 +423,10 @@ export class UserRequestService {
|
|||
E.Left<string> | E.Right<DbUserRequest>
|
||||
>(async (tx) => {
|
||||
// lock the rows
|
||||
await this.prisma.lockUserRequestByCollections(tx, request.userUid, [srcCollID, destCollID]);
|
||||
await this.prisma.lockUserRequestByCollections(tx, request.userUid, [
|
||||
srcCollID,
|
||||
destCollID,
|
||||
]);
|
||||
|
||||
request = await tx.userRequest.findUnique({
|
||||
where: { id: request.id },
|
||||
|
|
@ -429,22 +439,24 @@ export class UserRequestService {
|
|||
|
||||
// Check again if request is found in transaction, update orderIndexes of siblings
|
||||
// if request was deleted before the transaction started (race condition), do not update siblings orderIndexes
|
||||
if(request) {
|
||||
if (request) {
|
||||
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;
|
||||
|
||||
const updateTo = isMovingUp
|
||||
? request.orderIndex
|
||||
: nextReqOrderIndex;
|
||||
|
||||
await tx.userRequest.updateMany({
|
||||
where: {
|
||||
collectionID: srcCollID,
|
||||
|
|
@ -462,7 +474,7 @@ export class UserRequestService {
|
|||
},
|
||||
data: { orderIndex: { decrement: 1 } },
|
||||
});
|
||||
|
||||
|
||||
if (nextRequest) {
|
||||
await tx.userRequest.updateMany({
|
||||
where: {
|
||||
|
|
@ -473,15 +485,16 @@ export class UserRequestService {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Updating order index of the request
|
||||
let adjust: number;
|
||||
if (isSameCollection) adjust = nextRequest ? (isMovingUp ? 0 : -1) : 0;
|
||||
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 },
|
||||
|
|
@ -520,7 +533,9 @@ export class UserRequestService {
|
|||
|
||||
try {
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
await this.prisma.lockUserRequestByCollections(tx, userUid, [collectionID]);
|
||||
await this.prisma.lockUserRequestByCollections(tx, userUid, [
|
||||
collectionID,
|
||||
]);
|
||||
|
||||
const userRequests = await tx.userRequest.findMany({
|
||||
where: { userUid, collectionID },
|
||||
|
|
|
|||
Loading…
Reference in a new issue