diff --git a/src/modules/item/__test__/item.service.test.ts b/src/modules/item/__test__/item.service.test.ts index 34c5a5e..2bae2f2 100644 --- a/src/modules/item/__test__/item.service.test.ts +++ b/src/modules/item/__test__/item.service.test.ts @@ -21,7 +21,7 @@ describe('ItemService', () => { itemService = new ItemService(); userService = new UserService(); folderService = new FolderService(); - sharingService = new SharingService(); + sharingService = new SharingService(itemService); blobService = new BlobService(); shortcutService = new ShortcutService(); @@ -137,10 +137,13 @@ describe('ItemService', () => { parentId: null, }); - await sharingService.createSharing({ - itemId: folder.id, - userId: otherUser.id, - }); + await sharingService.createSharing( + { + itemId: folder.id, + userId: otherUser.id, + }, + user.id, + ); const blob1 = await blobService.createBlob({ mimeType: 'text/plain', @@ -177,20 +180,29 @@ describe('ItemService', () => { parentId: folder.id, }); - await sharingService.createSharing({ - itemId: blob1.id, - userId: otherUser.id, - }); + await sharingService.createSharing( + { + itemId: blob1.id, + userId: otherUser.id, + }, + user.id, + ); - await sharingService.createSharing({ - itemId: folder1.id, - userId: otherUser.id, - }); + await sharingService.createSharing( + { + itemId: folder1.id, + userId: otherUser.id, + }, + user.id, + ); - await sharingService.createSharing({ - itemId: folder3.id, - userId: otherUser.id, - }); + await sharingService.createSharing( + { + itemId: folder3.id, + userId: otherUser.id, + }, + user.id, + ); const shortcut1 = await shortcutService.createShortcut({ name: 'Shortcut1', @@ -206,10 +218,13 @@ describe('ItemService', () => { parentId: folder.id, }); - await sharingService.createSharing({ - itemId: shortcut1.id, - userId: otherUser.id, - }); + await sharingService.createSharing( + { + itemId: shortcut1.id, + userId: otherUser.id, + }, + user.id, + ); const itemsOwner = await itemService.getAllOwnedAndSharredItemsByParentIdAndUserId( user.id, diff --git a/src/modules/item/blob/blob.route.ts b/src/modules/item/blob/blob.route.ts index 84ae08a..c68e31e 100644 --- a/src/modules/item/blob/blob.route.ts +++ b/src/modules/item/blob/blob.route.ts @@ -6,9 +6,10 @@ import ItemService from '../item.service'; import AccessService from '../sharing/access.service'; export default async (fastify: FastifyInstance) => { + const itemService = new ItemService(); const blobController = new BlobController( new BlobService(), - new AccessService(new ItemService(), new SharingService()), + new AccessService(itemService, new SharingService(itemService)), ); fastify.get( diff --git a/src/modules/item/docs/docs.route.ts b/src/modules/item/docs/docs.route.ts index d3e4df8..042b411 100644 --- a/src/modules/item/docs/docs.route.ts +++ b/src/modules/item/docs/docs.route.ts @@ -6,10 +6,11 @@ import ItemService from '../item.service'; import SharingService from '../sharing/sharing.service'; export default async (fastify: FastifyInstance) => { + const itemService = new ItemService(); const docsService = new DocsService(); const docsController = new DocsController( docsService, - new AccessService(new ItemService(), new SharingService()), + new AccessService(itemService, new SharingService(itemService)), ); fastify.get( diff --git a/src/modules/item/folder/folder.route.ts b/src/modules/item/folder/folder.route.ts index dfa5d24..422d88d 100644 --- a/src/modules/item/folder/folder.route.ts +++ b/src/modules/item/folder/folder.route.ts @@ -6,10 +6,11 @@ import ItemService from '../item.service'; import SharingService from '../sharing/sharing.service'; export default async (fastify: FastifyInstance) => { + const itemService = new ItemService(); const folderService = new FolderService(); const folderController = new FolderController( folderService, - new AccessService(new ItemService(), new SharingService()), + new AccessService(itemService, new SharingService(itemService)), ); fastify.get( diff --git a/src/modules/item/item.route.ts b/src/modules/item/item.route.ts index cf7e2ea..2587426 100644 --- a/src/modules/item/item.route.ts +++ b/src/modules/item/item.route.ts @@ -8,7 +8,7 @@ export default async (fastify: FastifyInstance) => { const itemService = new ItemService(); const itemController = new ItemController( itemService, - new AccessService(itemService, new SharingService()), + new AccessService(itemService, new SharingService(itemService)), ); fastify.get( diff --git a/src/modules/item/item.service.ts b/src/modules/item/item.service.ts index b4f447f..f051388 100644 --- a/src/modules/item/item.service.ts +++ b/src/modules/item/item.service.ts @@ -49,6 +49,32 @@ export default class ItemService { return this.formatItems(items); } + public async getAllOwnedAndSharredItemsByParentIdAndUserIdRecursively( + userId: number, + parentId: number | null, + ): Promise { + const returnItems = []; + + const items = await this.getAllOwnedAndSharredItemsByParentIdAndUserId(userId, parentId); + returnItems.push(...items); + + await Promise.all( + items.map(async (item) => { + if (item.mimeType !== 'application/vnd.cloudstore.folder') { + return; + } + + const childItems = await this.getAllOwnedAndSharredItemsByParentIdAndUserId( + userId, + item.id, + ); + returnItems.push(...childItems); + }), + ); + + return returnItems; + } + public async getAllOwnedAndSharredItemsByParentIdAndUserId( userId: number, parentId: number | null, diff --git a/src/modules/item/sharing/__test__/access.service.test.ts b/src/modules/item/sharing/__test__/access.service.test.ts index 36c17a4..03d08ba 100644 --- a/src/modules/item/sharing/__test__/access.service.test.ts +++ b/src/modules/item/sharing/__test__/access.service.test.ts @@ -16,7 +16,7 @@ describe('ItemService', () => { beforeAll(async () => { itemService = new ItemService(); userService = new UserService(); - sharingService = new SharingService(); + sharingService = new SharingService(itemService); accessService = new AccessService(itemService, sharingService); user = await userService.createUser({ @@ -52,10 +52,13 @@ describe('ItemService', () => { parentId: null, mimeType: 'text/plain', }); - await sharingService.createSharing({ - itemId: createdItem.id, - userId: user.id, - }); + await sharingService.createSharing( + { + itemId: createdItem.id, + userId: user.id, + }, + otherUser.id, + ); const hasAccessToItem = await accessService.hasAccessToItem(createdItem.id, user.id); diff --git a/src/modules/item/sharing/__test__/add.test.ts b/src/modules/item/sharing/__test__/add.test.ts index 34928d5..28bd9bf 100644 --- a/src/modules/item/sharing/__test__/add.test.ts +++ b/src/modules/item/sharing/__test__/add.test.ts @@ -3,11 +3,13 @@ import UserService from '../../../auth/user.service'; import AuthService from '../../../auth/auth.service'; import ItemService from '../../item.service'; import SharingService from '../sharing.service'; +import FolderService from '../../folder/folder.service'; describe('POST /api/sharing', () => { let userService: UserService; let authService: AuthService; let itemService: ItemService; + let folderService: FolderService; let sharingService: SharingService; let user: User; @@ -17,7 +19,8 @@ describe('POST /api/sharing', () => { authService = new AuthService(); userService = new UserService(); itemService = new ItemService(); - sharingService = new SharingService(); + folderService = new FolderService(); + sharingService = new SharingService(itemService); user = await userService.createUser({ name: 'Joe Biden the 1st', @@ -63,6 +66,81 @@ describe('POST /api/sharing', () => { }); }); + it('should return status 200, return the created sharing and recursively create sharings on all child items', async () => { + const { accessToken } = await authService.createTokens(user.id); + + const folder = await folderService.createFolder({ + name: 'root', + ownerId: user.id, + parentId: null, + color: 'red', + }); + + const item1 = await itemService.createItem({ + name: 'test1.txt', + ownerId: user.id, + parentId: folder.id, + mimeType: 'text/plain', + }); + + const item2 = await itemService.createItem({ + name: 'test2.txt', + ownerId: user.id, + parentId: folder.id, + mimeType: 'text/plain', + }); + + const subFolder = await folderService.createFolder({ + name: 'sub', + ownerId: user.id, + parentId: folder.id, + color: 'red', + }); + + const item3 = await itemService.createItem({ + name: 'test3.txt', + ownerId: user.id, + parentId: subFolder.id, + mimeType: 'text/plain', + }); + + const response = await global.fastify.inject({ + method: 'POST', + url: '/api/sharing', + headers: { + authorization: 'Bearer ' + accessToken, + }, + payload: { + itemId: folder.id, + userId: otherUser.id, + }, + }); + + expect(response.statusCode).toBe(200); + expect(response.json()).toEqual({ + id: expect.any(Number), + itemId: folder.id, + userId: otherUser.id, + createdAt: expect.any(String), + updatedAt: expect.any(String), + }); + await expect( + sharingService.getByItemIdAndUserId(folder.id, otherUser.id), + ).resolves.toBeDefined(); + await expect( + sharingService.getByItemIdAndUserId(item1.id, otherUser.id), + ).resolves.toBeDefined(); + await expect( + sharingService.getByItemIdAndUserId(item2.id, otherUser.id), + ).resolves.toBeDefined(); + await expect( + sharingService.getByItemIdAndUserId(subFolder.id, otherUser.id), + ).resolves.toBeDefined(); + await expect( + sharingService.getByItemIdAndUserId(item3.id, otherUser.id), + ).resolves.toBeDefined(); + }); + it('should return status 401, when unauthorized', async () => { const item = await itemService.createItem({ name: 'test.txt', @@ -135,10 +213,13 @@ describe('POST /api/sharing', () => { mimeType: 'text/plain', }); - await sharingService.createSharing({ - itemId: item.id, - userId: user.id, - }); + await sharingService.createSharing( + { + itemId: item.id, + userId: user.id, + }, + otherUser.id, + ); const response = await global.fastify.inject({ method: 'POST', diff --git a/src/modules/item/sharing/__test__/delete.test.ts b/src/modules/item/sharing/__test__/delete.test.ts index 35241c0..82a5e69 100644 --- a/src/modules/item/sharing/__test__/delete.test.ts +++ b/src/modules/item/sharing/__test__/delete.test.ts @@ -3,11 +3,13 @@ import UserService from '../../../auth/user.service'; import AuthService from '../../../auth/auth.service'; import ItemService from '../../item.service'; import SharingService from '../sharing.service'; +import FolderService from '../../folder/folder.service'; describe('DELETE /api/sharing/:id', () => { let userService: UserService; let authService: AuthService; let itemService: ItemService; + let folderService: FolderService; let sharingService: SharingService; let user: User; @@ -17,7 +19,8 @@ describe('DELETE /api/sharing/:id', () => { authService = new AuthService(); userService = new UserService(); itemService = new ItemService(); - sharingService = new SharingService(); + folderService = new FolderService(); + sharingService = new SharingService(itemService); user = await userService.createUser({ name: 'Joe Biden the 1st', @@ -41,10 +44,13 @@ describe('DELETE /api/sharing/:id', () => { mimeType: 'text/plain', }); - const sharing = await sharingService.createSharing({ - itemId: item.id, - userId: user.id, - }); + const sharing = await sharingService.createSharing( + { + itemId: item.id, + userId: user.id, + }, + user.id, + ); const response = await global.fastify.inject({ method: 'DELETE', @@ -59,6 +65,97 @@ describe('DELETE /api/sharing/:id', () => { await expect(sharingService.getById(sharing.id)).rejects.toThrow(); }); + it('should return status 204 and delete sharings on child items', async () => { + const { accessToken } = await authService.createTokens(user.id); + + const folder = await folderService.createFolder({ + name: 'root', + ownerId: user.id, + parentId: null, + color: 'red', + }); + const sharing = await sharingService.createSharing( + { + itemId: folder.id, + userId: user.id, + }, + user.id, + ); + + const item1 = await itemService.createItem({ + name: 'test1.txt', + ownerId: user.id, + parentId: folder.id, + mimeType: 'text/plain', + }); + await sharingService.createSharing( + { + itemId: item1.id, + userId: user.id, + }, + user.id, + ); + + const item2 = await itemService.createItem({ + name: 'test2.txt', + ownerId: user.id, + parentId: folder.id, + mimeType: 'text/plain', + }); + await sharingService.createSharing( + { + itemId: item2.id, + userId: user.id, + }, + user.id, + ); + + const subFolder = await folderService.createFolder({ + name: 'sub', + ownerId: user.id, + parentId: folder.id, + color: 'red', + }); + await sharingService.createSharing( + { + itemId: subFolder.id, + userId: user.id, + }, + user.id, + ); + + const item3 = await itemService.createItem({ + name: 'test3.txt', + ownerId: user.id, + parentId: subFolder.id, + mimeType: 'text/plain', + }); + await sharingService.createSharing( + { + itemId: item3.id, + userId: user.id, + }, + user.id, + ); + + const response = await global.fastify.inject({ + method: 'DELETE', + url: '/api/sharing/' + sharing.id, + headers: { + authorization: 'Bearer ' + accessToken, + }, + }); + + expect(response.statusCode).toBe(204); + expect(response.body).toEqual(''); + + await expect(sharingService.getByItemIdAndUserId(folder.id, otherUser.id)).rejects.toThrow(); + await expect(sharingService.getByItemIdAndUserId(item1.id, otherUser.id)).rejects.toThrow(); + await expect(sharingService.getByItemIdAndUserId(item2.id, otherUser.id)).rejects.toThrow(); + await expect(sharingService.getByItemIdAndUserId(subFolder.id, otherUser.id)).rejects.toThrow(); + await expect(sharingService.getByItemIdAndUserId(item3.id, otherUser.id)).rejects.toThrow(); + }); + it('should return status 401, when unauthorized', async () => { const item = await itemService.createItem({ name: 'test.txt', @@ -67,10 +164,13 @@ describe('DELETE /api/sharing/:id', () => { mimeType: 'text/plain', }); - const sharing = await sharingService.createSharing({ - itemId: item.id, - userId: user.id, - }); + const sharing = await sharingService.createSharing( + { + itemId: item.id, + userId: user.id, + }, + user.id, + ); const response = await global.fastify.inject({ method: 'DELETE', @@ -100,10 +200,13 @@ describe('DELETE /api/sharing/:id', () => { mimeType: 'text/plain', }); - const sharing = await sharingService.createSharing({ - itemId: item.id, - userId: otherUser.id, - }); + const sharing = await sharingService.createSharing( + { + itemId: item.id, + userId: otherUser.id, + }, + otherUser.id, + ); const response = await global.fastify.inject({ method: 'DELETE', diff --git a/src/modules/item/sharing/__test__/edit.test.ts b/src/modules/item/sharing/__test__/edit.test.ts index 969f3e4..3a4fb46 100644 --- a/src/modules/item/sharing/__test__/edit.test.ts +++ b/src/modules/item/sharing/__test__/edit.test.ts @@ -17,7 +17,7 @@ describe('PUT /api/sharing', () => { authService = new AuthService(); userService = new UserService(); itemService = new ItemService(); - sharingService = new SharingService(); + sharingService = new SharingService(itemService); user = await userService.createUser({ name: 'Joe Biden the 1st', @@ -41,10 +41,13 @@ describe('PUT /api/sharing', () => { mimeType: 'text/plain', }); - const sharing = await sharingService.createSharing({ - itemId: item.id, - userId: user.id, - }); + const sharing = await sharingService.createSharing( + { + itemId: item.id, + userId: user.id, + }, + user.id, + ); const response = await global.fastify.inject({ method: 'PUT', @@ -75,10 +78,13 @@ describe('PUT /api/sharing', () => { mimeType: 'text/plain', }); - const sharing = await sharingService.createSharing({ - itemId: item.id, - userId: user.id, - }); + const sharing = await sharingService.createSharing( + { + itemId: item.id, + userId: user.id, + }, + user.id, + ); const response = await global.fastify.inject({ method: 'PUT', @@ -113,10 +119,13 @@ describe('PUT /api/sharing', () => { mimeType: 'text/plain', }); - const sharing = await sharingService.createSharing({ - itemId: item.id, - userId: otherUser.id, - }); + const sharing = await sharingService.createSharing( + { + itemId: item.id, + userId: otherUser.id, + }, + otherUser.id, + ); const response = await global.fastify.inject({ method: 'PUT', @@ -151,10 +160,13 @@ describe('PUT /api/sharing', () => { mimeType: 'text/plain', }); - const sharing = await sharingService.createSharing({ - itemId: item.id, - userId: user.id, - }); + const sharing = await sharingService.createSharing( + { + itemId: item.id, + userId: user.id, + }, + user.id, + ); const response = await global.fastify.inject({ method: 'PUT', @@ -188,10 +200,13 @@ describe('PUT /api/sharing', () => { mimeType: 'text/plain', }); - const sharing = await sharingService.createSharing({ - itemId: item.id, - userId: user.id, - }); + const sharing = await sharingService.createSharing( + { + itemId: item.id, + userId: user.id, + }, + user.id, + ); const response = await global.fastify.inject({ method: 'PUT', @@ -226,10 +241,13 @@ describe('PUT /api/sharing', () => { mimeType: 'text/plain', }); - const sharing = await sharingService.createSharing({ - itemId: item.id, - userId: user.id, - }); + const sharing = await sharingService.createSharing( + { + itemId: item.id, + userId: user.id, + }, + user.id, + ); const response = await global.fastify.inject({ method: 'PUT', @@ -263,10 +281,13 @@ describe('PUT /api/sharing', () => { mimeType: 'text/plain', }); - const sharing = await sharingService.createSharing({ - itemId: item.id, - userId: user.id, - }); + const sharing = await sharingService.createSharing( + { + itemId: item.id, + userId: user.id, + }, + user.id, + ); const response = await global.fastify.inject({ method: 'PUT', @@ -301,10 +322,13 @@ describe('PUT /api/sharing', () => { mimeType: 'text/plain', }); - const sharing = await sharingService.createSharing({ - itemId: item.id, - userId: user.id, - }); + const sharing = await sharingService.createSharing( + { + itemId: item.id, + userId: user.id, + }, + user.id, + ); const response = await global.fastify.inject({ method: 'PUT', @@ -338,10 +362,13 @@ describe('PUT /api/sharing', () => { mimeType: 'text/plain', }); - const sharing = await sharingService.createSharing({ - itemId: item.id, - userId: user.id, - }); + const sharing = await sharingService.createSharing( + { + itemId: item.id, + userId: user.id, + }, + user.id, + ); const response = await global.fastify.inject({ method: 'PUT', diff --git a/src/modules/item/sharing/__test__/read.test.ts b/src/modules/item/sharing/__test__/read.test.ts index d346350..b28fc87 100644 --- a/src/modules/item/sharing/__test__/read.test.ts +++ b/src/modules/item/sharing/__test__/read.test.ts @@ -17,7 +17,7 @@ describe('GET /api/sharing/:id', () => { authService = new AuthService(); userService = new UserService(); itemService = new ItemService(); - sharingService = new SharingService(); + sharingService = new SharingService(itemService); user = await userService.createUser({ name: 'Joe Biden the 1st', @@ -41,10 +41,13 @@ describe('GET /api/sharing/:id', () => { mimeType: 'text/plain', }); - const sharing = await sharingService.createSharing({ - itemId: item.id, - userId: user.id, - }); + const sharing = await sharingService.createSharing( + { + itemId: item.id, + userId: user.id, + }, + user.id, + ); const response = await global.fastify.inject({ method: 'GET', @@ -70,10 +73,13 @@ describe('GET /api/sharing/:id', () => { mimeType: 'text/plain', }); - const sharing = await sharingService.createSharing({ - itemId: item.id, - userId: user.id, - }); + const sharing = await sharingService.createSharing( + { + itemId: item.id, + userId: user.id, + }, + user.id, + ); const response = await global.fastify.inject({ method: 'GET', @@ -103,10 +109,13 @@ describe('GET /api/sharing/:id', () => { mimeType: 'text/plain', }); - const sharing = await sharingService.createSharing({ - itemId: item.id, - userId: otherUser.id, - }); + const sharing = await sharingService.createSharing( + { + itemId: item.id, + userId: otherUser.id, + }, + otherUser.id, + ); const response = await global.fastify.inject({ method: 'GET', diff --git a/src/modules/item/sharing/sharing.controller.ts b/src/modules/item/sharing/sharing.controller.ts index 6f54540..28ee7e4 100644 --- a/src/modules/item/sharing/sharing.controller.ts +++ b/src/modules/item/sharing/sharing.controller.ts @@ -73,7 +73,7 @@ export default class SharingController { return reply.unauthorized(); } - const sharing = await this.sharingService.createSharing(request.body); + const sharing = await this.sharingService.createSharing(request.body, request.user.sub); return reply.code(200).send(sharing); } catch (e) { @@ -99,7 +99,7 @@ export default class SharingController { return reply.unauthorized(); } - await this.sharingService.deleteSharingById(request.params.id); + await this.sharingService.deleteSharingByIdAndUserId(request.params.id, request.user.sub); return reply.code(204).send(); } catch (e) { diff --git a/src/modules/item/sharing/sharing.route.ts b/src/modules/item/sharing/sharing.route.ts index ddc389e..44492d1 100644 --- a/src/modules/item/sharing/sharing.route.ts +++ b/src/modules/item/sharing/sharing.route.ts @@ -5,10 +5,11 @@ import AccessService from './access.service'; import ItemService from '../item.service'; export default async (fastify: FastifyInstance) => { - const sharingService = new SharingService(); + const itemService = new ItemService(); + const sharingService = new SharingService(itemService); const sharingController = new SharingController( sharingService, - new AccessService(new ItemService(), sharingService), + new AccessService(itemService, sharingService), ); fastify.get( diff --git a/src/modules/item/sharing/sharing.service.ts b/src/modules/item/sharing/sharing.service.ts index f3f59d9..0adff34 100644 --- a/src/modules/item/sharing/sharing.service.ts +++ b/src/modules/item/sharing/sharing.service.ts @@ -1,7 +1,14 @@ import { prisma } from '../../../plugins/prisma'; +import ItemService from '../item.service'; import { Sharing, CreateSharing, UpdateSharing } from './sharing.schema'; export default class SharingService { + private itemService: ItemService; + + constructor(itemService: ItemService) { + this.itemService = itemService; + } + public async getById(id: number): Promise { const itemSharing = await prisma.itemSharing.findUnique({ where: { @@ -33,7 +40,23 @@ export default class SharingService { return itemSharing; } - public async createSharing(input: CreateSharing): Promise { + public async createSharing(input: CreateSharing, userId: number): Promise { + const accessableItems = + await this.itemService.getAllOwnedAndSharredItemsByParentIdAndUserIdRecursively( + userId, + input.itemId, + ); + + await prisma.itemSharing.createMany({ + data: accessableItems.map((item) => { + return { + itemId: item.id, + userId: input.userId, + }; + }), + skipDuplicates: true, + }); + try { await this.getByItemIdAndUserId(input.itemId, input.userId); @@ -70,11 +93,36 @@ export default class SharingService { return itemSharing; } - public async deleteSharingById(id: number): Promise { - await prisma.itemSharing.delete({ - where: { - id: id, - }, - }); + public async deleteSharingByIdAndUserId(id: number, userId: number): Promise { + try { + const itemSharing = await prisma.itemSharing.findUniqueOrThrow({ + where: { + id: id, + }, + }); + + const accessableItems = + await this.itemService.getAllOwnedAndSharredItemsByParentIdAndUserIdRecursively( + userId, + itemSharing.itemId, + ); + + await prisma.itemSharing.deleteMany({ + where: { + itemId: { + in: accessableItems.map((item) => item.id), + }, + userId: itemSharing.userId, + }, + }); + + await prisma.itemSharing.delete({ + where: { + id: id, + }, + }); + } catch (e) { + // Nothing to do here + } } } diff --git a/src/modules/item/shortcut/shortcut.route.ts b/src/modules/item/shortcut/shortcut.route.ts index 511e4d4..a4e8fc3 100644 --- a/src/modules/item/shortcut/shortcut.route.ts +++ b/src/modules/item/shortcut/shortcut.route.ts @@ -6,10 +6,11 @@ import ItemService from '../item.service'; import SharingService from '../sharing/sharing.service'; export default async (fastify: FastifyInstance) => { + const itemService = new ItemService(); const shortcutService = new ShortcutService(); const shortcutController = new ShortcutController( shortcutService, - new AccessService(new ItemService(), new SharingService()), + new AccessService(itemService, new SharingService(itemService)), ); fastify.get(