fix(backend): prevent IDOR in user collection and request endpoints (#5902)

This commit is contained in:
Mir Arif Hasan 2026-02-25 00:02:43 +06:00 committed by GitHub
parent 02b3dbcf5c
commit 57be05cdcb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 329 additions and 230 deletions

View file

@ -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

View file

@ -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({

View file

@ -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 },

View file

@ -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,

View file

@ -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>{

View file

@ -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({

View file

@ -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);

View file

@ -104,7 +104,7 @@ export class UserEnvironmentsResolver {
id,
name,
variables,
user
user,
);
if (E.isLeft(userEnvironment)) throwErr(userEnvironment.left);
return userEnvironment.right;

View file

@ -849,6 +849,7 @@ describe('UserRequestService', () => {
destCollID,
userRequests[0],
userRequests[1],
user,
);
expect(result).resolves.toEqualRight(true);

View file

@ -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 },