diff --git a/apps/desktop/src/main/mutations/files/file-delete.ts b/apps/desktop/src/main/mutations/files/file-delete.ts index 815eb21a..f8de1f5c 100644 --- a/apps/desktop/src/main/mutations/files/file-delete.ts +++ b/apps/desktop/src/main/mutations/files/file-delete.ts @@ -1,9 +1,13 @@ -import { entryService } from '@/main/services/entry-service'; +import { DeleteFileMutationData, generateId, IdType } from '@colanode/core'; + +import { databaseService } from '@/main/data/database-service'; import { MutationHandler } from '@/main/types'; import { FileDeleteMutationInput, FileDeleteMutationOutput, } from '@/shared/mutations/files/file-delete'; +import { eventBus } from '@/shared/lib/event-bus'; +import { mapFile } from '@/main/utils'; export class FileDeleteMutationHandler implements MutationHandler @@ -11,7 +15,55 @@ export class FileDeleteMutationHandler async handleMutation( input: FileDeleteMutationInput ): Promise { - await entryService.deleteEntry(input.fileId, input.userId); + const workspaceDatabase = await databaseService.getWorkspaceDatabase( + input.userId + ); + + const file = await workspaceDatabase + .selectFrom('files') + .selectAll() + .where('id', '=', input.fileId) + .executeTakeFirst(); + + if (!file) { + return { + success: true, + }; + } + + const deletedAt = new Date().toISOString(); + const deleteFileMutationData: DeleteFileMutationData = { + id: input.fileId, + rootId: file.root_id, + deletedAt, + }; + + await workspaceDatabase.transaction().execute(async (tx) => { + await tx.deleteFrom('files').where('id', '=', input.fileId).execute(); + + await tx + .insertInto('mutations') + .values({ + id: generateId(IdType.Mutation), + type: 'delete_file', + node_id: input.fileId, + data: JSON.stringify(deleteFileMutationData), + created_at: deletedAt, + retries: 0, + }) + .execute(); + }); + + eventBus.publish({ + type: 'file_deleted', + userId: input.userId, + file: mapFile(file), + }); + + eventBus.publish({ + type: 'mutation_created', + userId: input.userId, + }); return { success: true, diff --git a/apps/desktop/src/main/mutations/messages/message-delete.ts b/apps/desktop/src/main/mutations/messages/message-delete.ts index fa27c9c0..62362621 100644 --- a/apps/desktop/src/main/mutations/messages/message-delete.ts +++ b/apps/desktop/src/main/mutations/messages/message-delete.ts @@ -1,9 +1,13 @@ -import { entryService } from '@/main/services/entry-service'; +import { DeleteMessageMutationData, generateId, IdType } from '@colanode/core'; + +import { databaseService } from '@/main/data/database-service'; import { MutationHandler } from '@/main/types'; import { MessageDeleteMutationInput, MessageDeleteMutationOutput, } from '@/shared/mutations/messages/message-delete'; +import { eventBus } from '@/shared/lib/event-bus'; +import { mapMessage } from '@/main/utils'; export class MessageDeleteMutationHandler implements MutationHandler @@ -11,7 +15,58 @@ export class MessageDeleteMutationHandler async handleMutation( input: MessageDeleteMutationInput ): Promise { - await entryService.deleteEntry(input.messageId, input.userId); + const workspaceDatabase = await databaseService.getWorkspaceDatabase( + input.userId + ); + + const message = await workspaceDatabase + .selectFrom('messages') + .selectAll() + .where('id', '=', input.messageId) + .executeTakeFirst(); + + if (!message) { + return { + success: true, + }; + } + + const deletedAt = new Date().toISOString(); + const deleteMessageMutationData: DeleteMessageMutationData = { + id: input.messageId, + rootId: message.root_id, + deletedAt, + }; + + await workspaceDatabase.transaction().execute(async (tx) => { + await tx + .deleteFrom('messages') + .where('id', '=', input.messageId) + .execute(); + + await tx + .insertInto('mutations') + .values({ + id: generateId(IdType.Mutation), + type: 'delete_message', + node_id: input.messageId, + data: JSON.stringify(deleteMessageMutationData), + created_at: deletedAt, + retries: 0, + }) + .execute(); + }); + + eventBus.publish({ + type: 'message_deleted', + userId: input.userId, + message: mapMessage(message), + }); + + eventBus.publish({ + type: 'mutation_created', + userId: input.userId, + }); return { success: true, diff --git a/apps/desktop/src/main/services/file-service.ts b/apps/desktop/src/main/services/file-service.ts index 8fc1cabe..d0a44381 100644 --- a/apps/desktop/src/main/services/file-service.ts +++ b/apps/desktop/src/main/services/file-service.ts @@ -442,6 +442,46 @@ class FileService { const workspaceDatabase = await databaseService.getWorkspaceDatabase(userId); + if (file.deletedAt) { + const deletedFile = await workspaceDatabase + .deleteFrom('files') + .returningAll() + .where('id', '=', file.id) + .executeTakeFirst(); + + if (!deletedFile) { + return; + } + + await workspaceDatabase + .deleteFrom('file_interactions') + .where('file_id', '=', file.id) + .execute(); + + await workspaceDatabase + .deleteFrom('file_states') + .where('file_id', '=', file.id) + .execute(); + + // if the file exists in the workspace, we need to delete it + const filePath = path.join( + getWorkspaceFilesDirectoryPath(userId), + `${file.id}${file.extension}` + ); + + if (fs.existsSync(filePath)) { + fs.rmSync(filePath, { force: true }); + } + + eventBus.publish({ + type: 'file_deleted', + userId, + file: mapFile(deletedFile), + }); + + return; + } + const existingFile = await workspaceDatabase .selectFrom('files') .selectAll() diff --git a/apps/desktop/src/main/services/message-service.ts b/apps/desktop/src/main/services/message-service.ts index 979b65ad..f43a6b98 100644 --- a/apps/desktop/src/main/services/message-service.ts +++ b/apps/desktop/src/main/services/message-service.ts @@ -21,6 +21,41 @@ class MessageService { const workspaceDatabase = await databaseService.getWorkspaceDatabase(userId); + if (message.deletedAt) { + const deletedMessage = await workspaceDatabase + .deleteFrom('messages') + .returningAll() + .where('id', '=', message.id) + .executeTakeFirst(); + + if (!deletedMessage) { + return; + } + + await workspaceDatabase + .deleteFrom('message_reactions') + .where('message_id', '=', message.id) + .execute(); + + await workspaceDatabase + .deleteFrom('message_interactions') + .where('message_id', '=', message.id) + .execute(); + + await workspaceDatabase + .deleteFrom('texts') + .where('id', '=', message.id) + .execute(); + + eventBus.publish({ + type: 'message_deleted', + userId, + message: mapMessage(deletedMessage), + }); + + return; + } + const existingMessage = await workspaceDatabase .selectFrom('messages') .selectAll() diff --git a/apps/server/src/controllers/client/workspaces/mutations/mutations-sync.ts b/apps/server/src/controllers/client/workspaces/mutations/mutations-sync.ts index d43c0233..911e6dcc 100644 --- a/apps/server/src/controllers/client/workspaces/mutations/mutations-sync.ts +++ b/apps/server/src/controllers/client/workspaces/mutations/mutations-sync.ts @@ -16,6 +16,8 @@ import { MarkMessageSeenMutation, MarkFileSeenMutation, MarkFileOpenedMutation, + DeleteFileMutation, + DeleteMessageMutation, } from '@colanode/core'; import { SelectUser } from '@/data/schema'; @@ -62,8 +64,12 @@ const handleMutation = async ( return await handleDeleteTransaction(user, mutation); } else if (mutation.type === 'create_file') { return await handleCreateFile(user, mutation); + } else if (mutation.type === 'delete_file') { + return await handleDeleteFile(user, mutation); } else if (mutation.type === 'create_message') { return await handleCreateMessage(user, mutation); + } else if (mutation.type === 'delete_message') { + return await handleDeleteMessage(user, mutation); } else if (mutation.type === 'create_message_reaction') { return await handleCreateMessageReaction(user, mutation); } else if (mutation.type === 'delete_message_reaction') { @@ -148,6 +154,14 @@ const handleCreateFile = async ( return output ? 'success' : 'error'; }; +const handleDeleteFile = async ( + user: SelectUser, + mutation: DeleteFileMutation +): Promise => { + const output = await fileService.deleteFile(user, mutation); + return output ? 'success' : 'error'; +}; + const handleCreateMessage = async ( user: SelectUser, mutation: CreateMessageMutation @@ -156,6 +170,14 @@ const handleCreateMessage = async ( return output ? 'success' : 'error'; }; +const handleDeleteMessage = async ( + user: SelectUser, + mutation: DeleteMessageMutation +): Promise => { + const output = await messageService.deleteMessage(user, mutation); + return output ? 'success' : 'error'; +}; + const handleCreateMessageReaction = async ( user: SelectUser, mutation: CreateMessageReactionMutation diff --git a/apps/server/src/services/file-service.ts b/apps/server/src/services/file-service.ts index ae1631e1..2f51cb12 100644 --- a/apps/server/src/services/file-service.ts +++ b/apps/server/src/services/file-service.ts @@ -1,5 +1,6 @@ import { CreateFileMutation, + DeleteFileMutation, extractEntryRole, FileStatus, hasCollaboratorAccess, @@ -78,6 +79,60 @@ class FileService { return true; } + public async deleteFile( + user: SelectUser, + mutation: DeleteFileMutation + ): Promise { + const file = await database + .selectFrom('files') + .selectAll() + .where('id', '=', mutation.data.id) + .executeTakeFirst(); + + if (!file) { + return true; + } + + const root = await database + .selectFrom('entries') + .selectAll() + .where('id', '=', mutation.data.rootId) + .executeTakeFirst(); + + if (!root) { + return false; + } + + const rootEntry = mapEntry(root); + const role = extractEntryRole(rootEntry, user.id); + if (!hasCollaboratorAccess(role)) { + return false; + } + + const deletedFile = await database + .updateTable('files') + .returningAll() + .set({ + deleted_at: new Date(mutation.data.deletedAt), + deleted_by: user.id, + }) + .where('id', '=', mutation.data.id) + .executeTakeFirst(); + + if (!deletedFile) { + return false; + } + + eventBus.publish({ + type: 'file_deleted', + fileId: deletedFile.id, + rootId: deletedFile.root_id, + workspaceId: deletedFile.workspace_id, + }); + + return true; + } + public async markFileAsSeen( user: SelectUser, mutation: MarkFileSeenMutation diff --git a/apps/server/src/services/message-service.ts b/apps/server/src/services/message-service.ts index ae45b924..a357783f 100644 --- a/apps/server/src/services/message-service.ts +++ b/apps/server/src/services/message-service.ts @@ -1,6 +1,7 @@ import { CreateMessageMutation, CreateMessageReactionMutation, + DeleteMessageMutation, DeleteMessageReactionMutation, extractEntryRole, hasCollaboratorAccess, @@ -73,6 +74,60 @@ class MessageService { return true; } + public async deleteMessage( + user: SelectUser, + mutation: DeleteMessageMutation + ): Promise { + const message = await database + .selectFrom('messages') + .select(['id', 'root_id', 'workspace_id']) + .where('id', '=', mutation.data.id) + .executeTakeFirst(); + + if (!message) { + return true; + } + + const root = await database + .selectFrom('entries') + .selectAll() + .where('id', '=', message.root_id) + .executeTakeFirst(); + + if (!root) { + return false; + } + + const rootEntry = mapEntry(root); + const role = extractEntryRole(rootEntry, user.id); + if (!hasCollaboratorAccess(role)) { + return false; + } + + const deletedMessage = await database + .updateTable('messages') + .returningAll() + .set({ + deleted_at: new Date(mutation.data.deletedAt), + deleted_by: user.id, + }) + .where('id', '=', mutation.data.id) + .executeTakeFirst(); + + if (!deletedMessage) { + return false; + } + + eventBus.publish({ + type: 'message_deleted', + messageId: deletedMessage.id, + rootId: deletedMessage.root_id, + workspaceId: deletedMessage.workspace_id, + }); + + return true; + } + public async createMessageReaction( user: SelectUser, mutation: CreateMessageReactionMutation diff --git a/packages/core/src/types/mutations.ts b/packages/core/src/types/mutations.ts index ba353f95..d034fed1 100644 --- a/packages/core/src/types/mutations.ts +++ b/packages/core/src/types/mutations.ts @@ -45,6 +45,17 @@ export type CreateFileMutation = MutationBase & { data: CreateFileMutationData; }; +export type DeleteFileMutationData = { + id: string; + rootId: string; + deletedAt: string; +}; + +export type DeleteFileMutation = MutationBase & { + type: 'delete_file'; + data: DeleteFileMutationData; +}; + export type ApplyCreateTransactionMutation = MutationBase & { type: 'apply_create_transaction'; data: LocalCreateTransaction; @@ -75,6 +86,17 @@ export type CreateMessageMutation = MutationBase & { data: CreateMessageMutationData; }; +export type DeleteMessageMutationData = { + id: string; + rootId: string; + deletedAt: string; +}; + +export type DeleteMessageMutation = MutationBase & { + type: 'delete_message'; + data: DeleteMessageMutationData; +}; + export type CreateMessageReactionMutationData = { messageId: string; reaction: string; @@ -156,10 +178,12 @@ export type MarkEntryOpenedMutation = MutationBase & { export type Mutation = | CreateFileMutation + | DeleteFileMutation | ApplyCreateTransactionMutation | ApplyUpdateTransactionMutation | ApplyDeleteTransactionMutation | CreateMessageMutation + | DeleteMessageMutation | CreateMessageReactionMutation | DeleteMessageReactionMutation | MarkMessageSeenMutation