From df398a8b440439dd5a61cb23a022847b5cf68965 Mon Sep 17 00:00:00 2001 From: Artem Belyavsky Date: Sun, 21 Apr 2024 18:21:33 +0300 Subject: [PATCH] s5-1/feat: move all models to postgre --- .../20240421134316_init/migration.sql | 29 ++++ .../20240421135001_init/migration.sql | 38 +++++ .../20240421142255_init/migration.sql | 41 +++++ .../20240421145436_init/migration.sql | 34 ++++ .../20240421145507_init/migration.sql | 2 + .../20240421150856_init/migration.sql | 8 + .../20240421151034_init/migration.sql | 5 + backend/prisma/schema.postgresql.dev.prisma | 52 ++++++ .../src/modules/auth/services/auth.service.ts | 3 +- .../file-structure.service.spec.ts | 22 ++- .../file-structure/file-structure.service.ts | 158 +++++++++--------- .../file-structure/file-structure.type.ts | 45 +++++ .../file-structure/postgres-connection.ts | 14 ++ backend/src/modules/files/file.service.ts | 23 +-- .../modules/files/files.controller.spec.ts | 4 + backend/src/modules/files/files.controller.ts | 43 +++-- backend/src/types/api/request.ts | 5 +- 17 files changed, 402 insertions(+), 124 deletions(-) create mode 100644 backend/prisma/migrations/20240421134316_init/migration.sql create mode 100644 backend/prisma/migrations/20240421135001_init/migration.sql create mode 100644 backend/prisma/migrations/20240421142255_init/migration.sql create mode 100644 backend/prisma/migrations/20240421145436_init/migration.sql create mode 100644 backend/prisma/migrations/20240421145507_init/migration.sql create mode 100644 backend/prisma/migrations/20240421150856_init/migration.sql create mode 100644 backend/prisma/migrations/20240421151034_init/migration.sql create mode 100644 backend/src/modules/file-structure/file-structure.type.ts create mode 100644 backend/src/modules/file-structure/postgres-connection.ts diff --git a/backend/prisma/migrations/20240421134316_init/migration.sql b/backend/prisma/migrations/20240421134316_init/migration.sql new file mode 100644 index 0000000..be13f3b --- /dev/null +++ b/backend/prisma/migrations/20240421134316_init/migration.sql @@ -0,0 +1,29 @@ +-- CreateTable +CREATE TABLE "Folder" ( + "dr_id" TEXT NOT NULL, + "dr_name" TEXT, + "dr_owner" TEXT NOT NULL, + "dr_path" TEXT[], + "dr_removed" BOOLEAN NOT NULL DEFAULT false, + "dr_parent_dir_id" TEXT NOT NULL, + + CONSTRAINT "Folder_pkey" PRIMARY KEY ("dr_id") +); + +-- CreateTable +CREATE TABLE "File" ( + "fl_id" TEXT NOT NULL, + "fl_name" TEXT NOT NULL, + "fl_extension" TEXT NOT NULL, + "fl_owner" TEXT NOT NULL, + "fl_removed" BOOLEAN NOT NULL DEFAULT false, + "folderId" TEXT NOT NULL, + + CONSTRAINT "File_pkey" PRIMARY KEY ("fl_id") +); + +-- AddForeignKey +ALTER TABLE "Folder" ADD CONSTRAINT "Folder_dr_parent_dir_id_fkey" FOREIGN KEY ("dr_parent_dir_id") REFERENCES "Folder"("dr_id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "File" ADD CONSTRAINT "File_folderId_fkey" FOREIGN KEY ("folderId") REFERENCES "Folder"("dr_id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/backend/prisma/migrations/20240421135001_init/migration.sql b/backend/prisma/migrations/20240421135001_init/migration.sql new file mode 100644 index 0000000..d1fcc29 --- /dev/null +++ b/backend/prisma/migrations/20240421135001_init/migration.sql @@ -0,0 +1,38 @@ +/* + Warnings: + + - The primary key for the `File` table will be changed. If it partially fails, the table could be left without primary key constraint. + - The `fl_id` column on the `File` table would be dropped and recreated. This will lead to data loss if there is data in the column. + - The primary key for the `Folder` table will be changed. If it partially fails, the table could be left without primary key constraint. + - The `dr_id` column on the `Folder` table would be dropped and recreated. This will lead to data loss if there is data in the column. + - Changed the type of `folderId` on the `File` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required. + - Changed the type of `dr_parent_dir_id` on the `Folder` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required. + +*/ +-- DropForeignKey +ALTER TABLE "File" DROP CONSTRAINT "File_folderId_fkey"; + +-- DropForeignKey +ALTER TABLE "Folder" DROP CONSTRAINT "Folder_dr_parent_dir_id_fkey"; + +-- AlterTable +ALTER TABLE "File" DROP CONSTRAINT "File_pkey", +DROP COLUMN "fl_id", +ADD COLUMN "fl_id" SERIAL NOT NULL, +DROP COLUMN "folderId", +ADD COLUMN "folderId" INTEGER NOT NULL, +ADD CONSTRAINT "File_pkey" PRIMARY KEY ("fl_id"); + +-- AlterTable +ALTER TABLE "Folder" DROP CONSTRAINT "Folder_pkey", +DROP COLUMN "dr_id", +ADD COLUMN "dr_id" SERIAL NOT NULL, +DROP COLUMN "dr_parent_dir_id", +ADD COLUMN "dr_parent_dir_id" INTEGER NOT NULL, +ADD CONSTRAINT "Folder_pkey" PRIMARY KEY ("dr_id"); + +-- AddForeignKey +ALTER TABLE "Folder" ADD CONSTRAINT "Folder_dr_parent_dir_id_fkey" FOREIGN KEY ("dr_parent_dir_id") REFERENCES "Folder"("dr_id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "File" ADD CONSTRAINT "File_folderId_fkey" FOREIGN KEY ("folderId") REFERENCES "Folder"("dr_id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/backend/prisma/migrations/20240421142255_init/migration.sql b/backend/prisma/migrations/20240421142255_init/migration.sql new file mode 100644 index 0000000..4432675 --- /dev/null +++ b/backend/prisma/migrations/20240421142255_init/migration.sql @@ -0,0 +1,41 @@ +/* + Warnings: + + - The primary key for the `Folder` table will be changed. If it partially fails, the table could be left without primary key constraint. + - You are about to drop the column `dr_id` on the `Folder` table. All the data in the column will be lost. + - You are about to drop the column `dr_name` on the `Folder` table. All the data in the column will be lost. + - You are about to drop the column `dr_owner` on the `Folder` table. All the data in the column will be lost. + - You are about to drop the column `dr_parent_dir_id` on the `Folder` table. All the data in the column will be lost. + - You are about to drop the column `dr_path` on the `Folder` table. All the data in the column will be lost. + - You are about to drop the column `dr_removed` on the `Folder` table. All the data in the column will be lost. + - Added the required column `fd_owner` to the `Folder` table without a default value. This is not possible if the table is not empty. + - Added the required column `fd_parent_folder_id` to the `Folder` table without a default value. This is not possible if the table is not empty. + +*/ +-- DropForeignKey +ALTER TABLE "File" DROP CONSTRAINT "File_folderId_fkey"; + +-- DropForeignKey +ALTER TABLE "Folder" DROP CONSTRAINT "Folder_dr_parent_dir_id_fkey"; + +-- AlterTable +ALTER TABLE "Folder" DROP CONSTRAINT "Folder_pkey", +DROP COLUMN "dr_id", +DROP COLUMN "dr_name", +DROP COLUMN "dr_owner", +DROP COLUMN "dr_parent_dir_id", +DROP COLUMN "dr_path", +DROP COLUMN "dr_removed", +ADD COLUMN "fd_id" SERIAL NOT NULL, +ADD COLUMN "fd_name" TEXT, +ADD COLUMN "fd_owner" TEXT NOT NULL, +ADD COLUMN "fd_parent_folder_id" INTEGER NOT NULL, +ADD COLUMN "fd_path" INTEGER[], +ADD COLUMN "fd_removed" BOOLEAN NOT NULL DEFAULT false, +ADD CONSTRAINT "Folder_pkey" PRIMARY KEY ("fd_id"); + +-- AddForeignKey +ALTER TABLE "Folder" ADD CONSTRAINT "Folder_fd_parent_folder_id_fkey" FOREIGN KEY ("fd_parent_folder_id") REFERENCES "Folder"("fd_id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "File" ADD CONSTRAINT "File_folderId_fkey" FOREIGN KEY ("folderId") REFERENCES "Folder"("fd_id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/backend/prisma/migrations/20240421145436_init/migration.sql b/backend/prisma/migrations/20240421145436_init/migration.sql new file mode 100644 index 0000000..539be95 --- /dev/null +++ b/backend/prisma/migrations/20240421145436_init/migration.sql @@ -0,0 +1,34 @@ +/* + Warnings: + + - The primary key for the `File` table will be changed. If it partially fails, the table could be left without primary key constraint. + - The primary key for the `Folder` table will be changed. If it partially fails, the table could be left without primary key constraint. + +*/ +-- DropForeignKey +ALTER TABLE "File" DROP CONSTRAINT "File_folderId_fkey"; + +-- DropForeignKey +ALTER TABLE "Folder" DROP CONSTRAINT "Folder_fd_parent_folder_id_fkey"; + +-- AlterTable +ALTER TABLE "File" DROP CONSTRAINT "File_pkey", +ALTER COLUMN "fl_id" DROP DEFAULT, +ALTER COLUMN "fl_id" SET DATA TYPE TEXT, +ALTER COLUMN "folderId" SET DATA TYPE TEXT, +ADD CONSTRAINT "File_pkey" PRIMARY KEY ("fl_id"); +DROP SEQUENCE "File_fl_id_seq"; + +-- AlterTable +ALTER TABLE "Folder" DROP CONSTRAINT "Folder_pkey", +ALTER COLUMN "fd_id" DROP DEFAULT, +ALTER COLUMN "fd_id" SET DATA TYPE TEXT, +ALTER COLUMN "fd_parent_folder_id" SET DATA TYPE TEXT, +ADD CONSTRAINT "Folder_pkey" PRIMARY KEY ("fd_id"); +DROP SEQUENCE "Folder_fd_id_seq"; + +-- AddForeignKey +ALTER TABLE "Folder" ADD CONSTRAINT "Folder_fd_parent_folder_id_fkey" FOREIGN KEY ("fd_parent_folder_id") REFERENCES "Folder"("fd_id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "File" ADD CONSTRAINT "File_folderId_fkey" FOREIGN KEY ("folderId") REFERENCES "Folder"("fd_id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/backend/prisma/migrations/20240421145507_init/migration.sql b/backend/prisma/migrations/20240421145507_init/migration.sql new file mode 100644 index 0000000..e682cca --- /dev/null +++ b/backend/prisma/migrations/20240421145507_init/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Folder" ALTER COLUMN "fd_path" SET DATA TYPE TEXT[]; diff --git a/backend/prisma/migrations/20240421150856_init/migration.sql b/backend/prisma/migrations/20240421150856_init/migration.sql new file mode 100644 index 0000000..e3352de --- /dev/null +++ b/backend/prisma/migrations/20240421150856_init/migration.sql @@ -0,0 +1,8 @@ +-- DropForeignKey +ALTER TABLE "Folder" DROP CONSTRAINT "Folder_fd_parent_folder_id_fkey"; + +-- AlterTable +ALTER TABLE "Folder" ALTER COLUMN "fd_parent_folder_id" DROP NOT NULL; + +-- AddForeignKey +ALTER TABLE "Folder" ADD CONSTRAINT "Folder_fd_parent_folder_id_fkey" FOREIGN KEY ("fd_parent_folder_id") REFERENCES "Folder"("fd_id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/backend/prisma/migrations/20240421151034_init/migration.sql b/backend/prisma/migrations/20240421151034_init/migration.sql new file mode 100644 index 0000000..76afeac --- /dev/null +++ b/backend/prisma/migrations/20240421151034_init/migration.sql @@ -0,0 +1,5 @@ +-- DropForeignKey +ALTER TABLE "Folder" DROP CONSTRAINT "Folder_fd_parent_folder_id_fkey"; + +-- AddForeignKey +ALTER TABLE "Folder" ADD CONSTRAINT "Folder_fd_parent_folder_id_fkey" FOREIGN KEY ("fd_parent_folder_id") REFERENCES "Folder"("fd_id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/backend/prisma/schema.postgresql.dev.prisma b/backend/prisma/schema.postgresql.dev.prisma index f602e0f..f6f5432 100644 --- a/backend/prisma/schema.postgresql.dev.prisma +++ b/backend/prisma/schema.postgresql.dev.prisma @@ -18,3 +18,55 @@ model User { username String @unique password String? } + +model Folder { + id String @id @default(uuid()) @map("fd_id") + name String? @map("fd_name") + owner String @map("fd_owner") + path String[] @map("fd_path") + removed Boolean @default(false) @map("fd_removed") + + parentFolder Folder? @relation("FolderParents", fields: [parentFolderId], references: [id], onDelete: Cascade, onUpdate: Cascade) + childrenFolders Folder[] @relation("FolderParents") + parentFolderId String? @map("fd_parent_folder_id") + childrenFiles File[] +} + +model File { + id String @id @default(uuid()) @map("fl_id") + name String @map("fl_name") + extension String @map("fl_extension") + owner String @map("fl_owner") + removed Boolean @default(false) @map("fl_removed") + + folder Folder @relation(fields: [folderId], references: [id]) + folderId String +} + +// model FileAccessList { +// id String @id +// file File +// fileId +// user User @relation(fields: [userId], references: [id]) +// userId String +// rightListId + +// @@map("file_access_list") +// } + +// model FileAccessRightsListToUsers { +// id String @id +// listId +// userId +// } + +// model FileAccessRightsList { +// id String @id +// rightId +// } + +// model FileAccessRights { +// id String @id +// value String +// description String +// } diff --git a/backend/src/modules/auth/services/auth.service.ts b/backend/src/modules/auth/services/auth.service.ts index 9d72056..4779e71 100644 --- a/backend/src/modules/auth/services/auth.service.ts +++ b/backend/src/modules/auth/services/auth.service.ts @@ -5,7 +5,8 @@ import { DbUserUniqueConstraintError } from '@src/modules/errors/ErrorUniqueCons import { UserRegisterError } from '@src/modules/errors/ErrorUserRegister'; import { CannotFullfillRequestError } from '@src/modules/errors/logic/CannotFullfillRequest'; import { GenericServerError } from '@src/modules/errors/logic/GenericServerError'; -import { FileStructureRepository, IFileStructureRepository } from '@src/modules/file-structure/file-structure.service'; +import { FileStructureRepository } from '@src/modules/file-structure/file-structure.service'; +import { IFileStructureRepository } from '@src/modules/file-structure/file-structure.type'; import { FileSystemService, IFileSystemService } from '@src/modules/file-system/file-system.service'; export interface IAuthService { diff --git a/backend/src/modules/file-structure/file-structure.service.spec.ts b/backend/src/modules/file-structure/file-structure.service.spec.ts index 98d347a..f3f5ca6 100644 --- a/backend/src/modules/file-structure/file-structure.service.spec.ts +++ b/backend/src/modules/file-structure/file-structure.service.spec.ts @@ -1,14 +1,10 @@ import { Test, TestingModule } from '@nestjs/testing'; import { User } from '../user/models/User'; -import { - CreateUserRootFolderStructure, - FileStructureRepository, - IFileStructureRepository, - TFileRepository, -} from './file-structure.service'; +import { FileStructureRepository } from './file-structure.service'; import { DbFileRecordDoesNotExist } from '../errors/db/DbFileRecordDoesNotExistError'; import { ObjectId } from 'bson'; import { TestConfigService } from '../config/test.config.service'; +import { IFileStructureRepository, CreateUserRootFolderStructure, TFileRepository } from './file-structure.type'; describe('FileStructureRepository: ', () => { let service: IFileStructureRepository; @@ -39,7 +35,8 @@ describe('FileStructureRepository: ', () => { const result = await service.createUserRootFolder(user.id); expect(result).toMatchObject>({ - parentId: '', + // parentId: , + parentFolderId: null, name: user.id, }); }); @@ -58,7 +55,7 @@ describe('FileStructureRepository: ', () => { const nestedFolder1 = await service.createFolder(user.id, nestedFolder1Name, parentFolder.id); expect(nestedFolder1).toMatchObject>({ name: nestedFolder1Name, - parentId: parentFolder.id, + parentFolderId: parentFolder.id, }); const children = await service.getChildrenFolders(parentFolder.id); @@ -68,7 +65,8 @@ describe('FileStructureRepository: ', () => { const nestedFolder2 = await service.createFolder(user.id, nestedFolder2Name, parentFolder.id); expect(nestedFolder2).toMatchObject>({ name: nestedFolder2Name, - parentId: parentFolder.id, + parentFolderId: parentFolder.id, + }); const children2 = await service.getChildrenFolders(parentFolder.id); @@ -78,7 +76,7 @@ describe('FileStructureRepository: ', () => { const nestedFolder11 = await service.createFolder(user.id, nestedFolder11Name, nestedFolder1.id); expect(nestedFolder11).toMatchObject>({ name: nestedFolder11Name, - parentId: nestedFolder1.id, + parentFolderId: nestedFolder1.id, }); const children3 = await service.getChildrenFolders(nestedFolder1.id); @@ -147,13 +145,13 @@ describe('FileStructureRepository: ', () => { /** Create root user folder */ try { - await service.removeFile('unknown', false); + await service.removeFile('unkown', false); } catch (e: unknown) { expect(e).toBeInstanceOf(DbFileRecordDoesNotExist); } try { - await service.removeFile(new ObjectId().toString(), false); + await service.removeFile('', false); } catch (e: unknown) { expect(e).toBeInstanceOf(DbFileRecordDoesNotExist); } diff --git a/backend/src/modules/file-structure/file-structure.service.ts b/backend/src/modules/file-structure/file-structure.service.ts index 14ac769..ba6f4cb 100644 --- a/backend/src/modules/file-structure/file-structure.service.ts +++ b/backend/src/modules/file-structure/file-structure.service.ts @@ -1,61 +1,27 @@ import { Injectable } from '@nestjs/common'; -import { PrismaClient as MongoClient, Prisma } from '@prsm/generated/prisma-mongo-client-js'; +import { PrismaClient as PostgresClient, Prisma } from '@prsm/generated/prisma-postgres-client-js'; import { DbFileRecordDoesNotExist } from '../errors/db/DbFileRecordDoesNotExistError'; import { NoFolderWithThisIdError } from '../errors/logic/NoFolderWithThisIdError'; -import { MongoConnection } from './mongo-connection'; - -export type CreateUserRootFolderStructure = { - parentId: string; - name: string; - id: string; -}; - -export type TFolder = { - id: string; - name: string; - parentId: string; - owner: string; - path: string[]; -}; -export type TFileStructureRemoveAllData = Prisma.BatchPayload; - -export type TFolderRepository = Prisma.Result; -export type TFileRepository = Prisma.Result; - -export interface IFileStructureRepository { - /** - * For testing purpose only! - * TODO: remove from production code. - */ - removeAllData(): Promise; - createUserRootFolder(userId: string): Promise; - createFolder(userId: string, folderName: string, parentFolderId: string): Promise; - getChildrenFiles(folderId: string): Promise; - getChildrenFolders(folderId: string): Promise; - createFile(name: string, extension: string, folderId: string, userId: string): Promise; - removeFile(fileId: string, softDelete: boolean): Promise>; - getUserRootFolder(userId: string): Promise; - getFolderById(folderId: string): Promise; - renameFolder(newFolderName: string, folderId: string): Promise; - deleteFolder(folderId: string): Promise; - getFolderPath(folderId: string): Promise<{ name: string; id: string }[]>; - changeFolderRemovedState(folderId: string, removedState: boolean): Promise; - getFileById(fileId: string): Promise; -} +import { PostgresConnection } from './postgres-connection'; +import { CreateUserRootFolderStructure, IFileStructureRepository, TFileId, TFileRepository, TFileStructureRemoveAllData, TFolder, TFolderId, TFolderRepository } from './file-structure.type'; + @Injectable() export class FileStructureRepository implements IFileStructureRepository { - private connection: MongoClient; + private connection: PostgresClient; constructor() { - this.connection = new MongoConnection().Connection; + this.connection = new PostgresConnection().Connection; } async removeAllData(): Promise { - return Promise.all([this.connection.node.deleteMany(), this.connection.file.deleteMany()]); + return Promise.all([ + this.connection.file.deleteMany(), + this.connection.folder.deleteMany() + ]); } - async getFolderPath(folderId: string): Promise<{ name: string; id: string }[]> { + async getFolderPath(folderId: TFolderId): Promise[]> { try { const folder = await this.getFolderById(folderId); if (folder === null) { @@ -66,7 +32,7 @@ export class FileStructureRepository implements IFileStructureRepository { const ancestorFoldersIds = folder.path.slice(1); console.log(ancestorFoldersIds); - const names = await this.connection.node.findMany({ + const names = await this.connection.folder.findMany({ where: { id: { in: ancestorFoldersIds, @@ -116,7 +82,7 @@ export class FileStructureRepository implements IFileStructureRepository { } } - async getChildrenFiles(folderId: string): Promise { + async getChildrenFiles(folderId: TFolderId): Promise { return this.connection.file.findMany({ where: { folderId, @@ -124,18 +90,31 @@ export class FileStructureRepository implements IFileStructureRepository { }); } - async createFile(name: string, extension: string, folderId: string, userId: string): Promise { + async createFile(name: string, extension: string, folderId: TFolderId, userId: string): Promise { + + const folder = this.connection.folder.findUnique({ + where: { + id: folderId + } + }); + return this.connection.file.create({ data: { extension, - folderId, + // folderId, name, owner: userId, + folder: { + connect: { + id: folderId + } + }, + // id: 'asd' }, }); } - async getFileById(fileId: string): Promise { + async getFileById(fileId: TFileId): Promise { try { return this.connection.file.findUnique({ where: { @@ -153,7 +132,7 @@ export class FileStructureRepository implements IFileStructureRepository { } } - async removeFile(fileId: string, softDelete: boolean): Promise> { + async removeFile(fileId: TFileId, softDelete: boolean): Promise> { try { if (softDelete) { return await this.connection.file.update({ @@ -197,35 +176,46 @@ export class FileStructureRepository implements IFileStructureRepository { /** TODO Need to disallow some username symbols. */ /** TODO Remake tests to pass userId */ async createUserRootFolder(userId: string): Promise { - return this.connection.node.create({ - data: { - parentId: '', - name: userId, - owner: userId, - path: [userId], - }, - }); + try { + + return this.connection.folder.create({ + data: { + // parentId: '', + name: userId, + owner: userId, + path: [], + // parentFolderId: + // parentFolder: { + // create: { + // } + // } + }, + }); + } catch (e: unknown) { + console.log(e) + } + } async getUserRootFolder(userId: string): Promise { - return this.connection.node.findFirst({ + return this.connection.folder.findFirst({ where: { name: userId, }, }); } - async getChildrenFolders(folderId: string): Promise { - return this.connection.node.findMany({ + async getChildrenFolders(folderId: TFolderId): Promise { + return this.connection.folder.findMany({ where: { - parentId: folderId, + parentFolderId: folderId, }, }); } - async getFolderById(folderId: string): Promise { + async getFolderById(folderId: TFolderId): Promise { try { - const folder = await this.connection.node.findFirst({ + const folder = await this.connection.folder.findFirst({ where: { id: folderId, }, @@ -233,14 +223,16 @@ export class FileStructureRepository implements IFileStructureRepository { id: true, name: true, owner: true, - parentId: true, + // parentId: true, + parentFolder: true, path: true, }, }); console.log(`folder ${folder?.id}`); if (folder !== null) { - return folder; + /** TODO fix */ + return folder as any; } return null; @@ -249,8 +241,8 @@ export class FileStructureRepository implements IFileStructureRepository { } } - async renameFolder(newFolderName: string, folderId: string): Promise { - const folder = await this.connection.node.update({ + async renameFolder(newFolderName: string, folderId: TFolderId): Promise { + const folder = await this.connection.folder.update({ where: { id: folderId, }, @@ -262,39 +254,39 @@ export class FileStructureRepository implements IFileStructureRepository { return folder; } - async changeFolderRemovedState(folderId: string, removedState: boolean): Promise { - const folder = await this.connection.node.update({ - where: { - id: folderId, - }, - data: { - removed: removedState, - }, + async changeFolderRemovedState(folderId: TFolderId, removedState: boolean): Promise { + const folder = await this.connection.folder.update({ + where: { id: folderId }, + data: { removed: removedState }, }); return folder; } - async deleteFolder(folderId: string): Promise { - const folder = await this.connection.node.delete({ + async deleteFolder(folderId: TFolderId): Promise { + const folder = await this.connection.folder.delete({ where: { id: folderId, }, + // include: { + // parentFolder: true + // } }); return folder; } - async createFolder(userId: string, folderName: string, parentFolderId: string): Promise { - const parentFolder = await this.connection.node.findUnique({ + async createFolder(userId: string, folderName: string, parentFolderId: TFolderId): Promise { + const parentFolder = await this.connection.folder.findUnique({ where: { id: parentFolderId, }, }); - return this.connection.node.create({ + return this.connection.folder.create({ data: { - parentId: parentFolderId, + // parentId: parentFolderId, + parentFolderId: parentFolderId, name: folderName, owner: userId, path: [...parentFolder.path, parentFolderId], diff --git a/backend/src/modules/file-structure/file-structure.type.ts b/backend/src/modules/file-structure/file-structure.type.ts new file mode 100644 index 0000000..4d7136f --- /dev/null +++ b/backend/src/modules/file-structure/file-structure.type.ts @@ -0,0 +1,45 @@ +import { Prisma } from "@prsm/generated/prisma-postgres-client-js"; + +export type CreateUserRootFolderStructure = { + parentFolderId: TFolderId; + name: string; + id: TFolderId; +}; + +export type TFolderId = string; + +export type TFileId = string; + +export type TFolder = { + id: TFolderId; + name: string; + // parentFolder: string; + parentFolder: Omit; + owner: string; + path: TFolderId[]; +}; +export type TFileStructureRemoveAllData = Prisma.BatchPayload; + +export type TFolderRepository = Prisma.Result; +export type TFileRepository = Prisma.Result; + +export interface IFileStructureRepository { + /** + * For testing purpose only! + * TODO: remove from production code. + */ + removeAllData(): Promise; + createUserRootFolder(userId: string): Promise; + createFolder(userId: string, folderName: string, parentFolderId: TFolderId): Promise; + getChildrenFiles(folderId: TFolderId): Promise; + getChildrenFolders(folderId: TFolderId): Promise; + createFile(name: string, extension: string, folderId: TFolderId, userId: string): Promise; + removeFile(fileId: TFileId, softDelete: boolean): Promise>; + getUserRootFolder(userId: string): Promise; + getFolderById(folderId: TFolderId): Promise; + renameFolder(newFolderName: string, folderId: TFolderId): Promise; + deleteFolder(folderId: TFolderId): Promise; + getFolderPath(folderId: TFolderId): Promise[]>; + changeFolderRemovedState(folderId: TFolderId, removedState: boolean): Promise; + getFileById(fileId: TFileId): Promise; +} \ No newline at end of file diff --git a/backend/src/modules/file-structure/postgres-connection.ts b/backend/src/modules/file-structure/postgres-connection.ts new file mode 100644 index 0000000..1efd7ff --- /dev/null +++ b/backend/src/modules/file-structure/postgres-connection.ts @@ -0,0 +1,14 @@ +import { PrismaClient as PostgresClient, Prisma } from '@prsm/generated/prisma-postgres-client-js'; + +export class PostgresConnection { + /** client is one for each app instance/cluster */ + private client: PostgresClient; + + constructor() { + this.client = new PostgresClient(); + } + + get Connection() { + return this.client; + } +} diff --git a/backend/src/modules/files/file.service.ts b/backend/src/modules/files/file.service.ts index d72610e..7047b42 100644 --- a/backend/src/modules/files/file.service.ts +++ b/backend/src/modules/files/file.service.ts @@ -3,16 +3,17 @@ import path from 'node:path'; import { IConfigService } from '../config/dev.config.service'; import { FileStructureRepository, - IFileStructureRepository, - TFileRepository, + // IFileStructureRepository, + // TFileRepository, } from '../file-structure/file-structure.service'; import { IFileSystemService } from '../file-system/file-system.service'; +import { IFileStructureRepository, TFileId, TFileRepository, TFolderId } from '../file-structure/file-structure.type'; export interface IFileService {} export type TRemoveFileResult = { - folderId: string; - fileId: string; + folderId: TFolderId; + fileId: TFileId; }; export type TGetFileResult = { @@ -38,17 +39,17 @@ export class FileService implements IFileService { async saveFileToFolder( userId: string, fileBuffer: Buffer, - folderId: string, + folderId: TFolderId, fileName: string, fileExtension: string, ): Promise { const createdFile = await this.fileStructureService.createFile(fileName, fileExtension, folderId, userId); const realFolderPath = this.getRealPath(userId); - const realFilePath = path.join(realFolderPath, createdFile.id); + const realFilePath = path.join(realFolderPath, createdFile.id.toString()); this.fileSystem.writeFile(fileBuffer, realFilePath); } - async getFileStreamById(fileId: string, userId: string): Promise { + async getFileStreamById(fileId: TFileId, userId: string): Promise { try { /** * TODO @@ -56,14 +57,14 @@ export class FileService implements IFileService { */ const file = await this.fileStructureService.getFileById(fileId); const realFolderPath = this.getRealPath(userId); - const realFilePath = path.join(realFolderPath, file.id); + const realFilePath = path.join(realFolderPath, file.id.toString()); const fileStream = this.fileSystem.getFileStream(realFilePath); return fileStream; } catch (e: unknown) {} } - async getFileInfoById(fileId: string, userId: string): Promise { + async getFileInfoById(fileId: TFileId, userId: string): Promise { try { /** * TODO @@ -74,7 +75,7 @@ export class FileService implements IFileService { } catch (e: unknown) {} } - async removeFile(fileId: string, userId: string): Promise { + async removeFile(fileId: TFileId, userId: string): Promise { try { /** * TODO @@ -82,7 +83,7 @@ export class FileService implements IFileService { */ const { folderId, id } = await this.fileStructureService.removeFile(fileId, false); const realFolderPath = this.getRealPath(userId); - const realFilePath = path.join(realFolderPath, id); + const realFilePath = path.join(realFolderPath, id.toString()); await this.fileSystem.removeFile(realFilePath); return { diff --git a/backend/src/modules/files/files.controller.spec.ts b/backend/src/modules/files/files.controller.spec.ts index 3add25c..8d55b89 100644 --- a/backend/src/modules/files/files.controller.spec.ts +++ b/backend/src/modules/files/files.controller.spec.ts @@ -41,4 +41,8 @@ describe('FilesController', () => { it('Should be defined', () => { expect(controller).toBeDefined(); }); + + it('Should return file access rights info', async () => { + + }) }); diff --git a/backend/src/modules/files/files.controller.ts b/backend/src/modules/files/files.controller.ts index 61b64d1..8984d6d 100644 --- a/backend/src/modules/files/files.controller.ts +++ b/backend/src/modules/files/files.controller.ts @@ -14,17 +14,18 @@ import { } from '@nestjs/common'; import { FilesInterceptor } from '@nestjs/platform-express'; import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; -import { CreateFolderDTO, DeleteFolderParamsDTO, RenameFolderDTO } from '@src/types/api/request'; +import { CreateFolderDTO, DeleteFolderParamsDTO, RenameFolderDTO, TApiFileId, TApiFolderId } from '@src/types/api/request'; import { CreateFolderResult, GetFolderResult200Decl } from '@src/types/api/response'; import { NoFolderWithThisIdError } from '../errors/logic/NoFolderWithThisIdError'; -import { FileStructureRepository, IFileStructureRepository, TFolder } from '../file-structure/file-structure.service'; +import { FileStructureRepository } from '../file-structure/file-structure.service'; import { IFileSystemService } from '../file-system/file-system.service'; import { IUserService } from '../user/services/users.service'; import { FileService } from './file.service'; +import { IFileStructureRepository, TFolder, TFolderRepository } from '../file-structure/file-structure.type'; // export class TFol export class TUploadFileDTO { - folderId: string; + folderId: TApiFolderId; } @Controller({ @@ -37,7 +38,7 @@ export class FilesController { @Inject(Symbol.for('IFileSystemService')) private fileSystem: IFileSystemService, @Inject(FileService) private fileService: FileService, @Inject(Symbol.for('IUserService')) private usersService: IUserService, - ) {} + ) { } @ApiResponse({ status: 200, type: CreateFolderResult }) @ApiOperation({ @@ -52,7 +53,7 @@ export class FilesController { const createdFolder = await this.fileStructureRepository.createFolder(userId, folderName, parentFolderId); - const children = await this.fileStructureRepository.getChildrenFolders(createdFolder.parentId); + const children = await this.fileStructureRepository.getChildrenFolders(createdFolder.parentFolderId); const parentFolder = await this.fileStructureRepository.getFolderById(parentFolderId); return { @@ -66,7 +67,7 @@ export class FilesController { async renameFolder( @Body() renameFolderDTO: RenameFolderDTO, @Req() request: any, - @Param('folderId') folderId: string, + @Param('folderId') folderId: TApiFolderId, ) { const { newFolderName } = renameFolderDTO; const userId = request.user.sub; @@ -83,19 +84,19 @@ export class FilesController { async deleteFolder( @Body() deleteFolderDTO: DeleteFolderParamsDTO, @Req() request: any, - @Param('folderId') folderId: string, - ): Promise<{ folder: TFolder; softDelete: boolean }> { + @Param('folderId') folderId: TApiFolderId, + ): Promise<{ folder: TFolder; softDelete: boolean; }> { const { softDelete } = deleteFolderDTO; const userId = request.user.sub; let deletedFolder: TFolder; if (!softDelete) { + /** TODO fix!!! */ /** We should delete all nested subfolders */ - deletedFolder = await this.fileStructureRepository.deleteFolder(folderId); - const deletedFolderFS = await this.fileSystem.removeFolder(folderId); + deletedFolder = await this.fileSystem.removeFolder(folderId.toString()) as any; } else { /** We should change this flag for all files in nodes subtree */ - deletedFolder = await this.fileStructureRepository.changeFolderRemovedState(folderId, true); + deletedFolder = await this.fileStructureRepository.changeFolderRemovedState(folderId, true) as any; // const deletedFolderFS = await this.fileSystem.removeFolder(folderId) } @@ -116,9 +117,9 @@ export class FilesController { }) @ApiTags('files') @Get('folder/:id') - async getFolderChildren(@Req() request: any, @Param('id') id: string) { + async getFolderChildren(@Req() request: any, @Param('id') parentFolderId: TApiFolderId) { try { - const parentFolderId = id; + // const parentFolderId = parentFolderId; const userId = request.user.sub; const user = await this.usersService.getUserProfile(userId); @@ -152,6 +153,16 @@ export class FilesController { folders: children, files: folderFiles, currentPath: namesPath, + folderAccess: { + downloadZip: true, + downloadNestedFiles: false, + editFolderInfo: false, + editNestedFiles: true, + removeFolder: false, + removeNestedFiles: false, + moveFolder: true, + moveNestedFiles: true + } }; } catch (e: unknown) { if (e instanceof NoFolderWithThisIdError) { @@ -223,7 +234,7 @@ export class FilesController { }) @ApiTags('files') @Post('remove/:fileId') - async removeFile(@Req() request: any, @Param('fileId') fileId: string) { + async removeFile(@Req() request: any, @Param('fileId') fileId: TApiFileId) { const userId = request.user.sub; const { fileId: removedFileId, folderId } = await this.fileService.removeFile(fileId, userId); @@ -244,7 +255,7 @@ export class FilesController { }) @ApiTags('files') @Get('download/:fileId') - async getFile(@Req() request: any, @Param('fileId') fileId: string): Promise { + async getFile(@Req() request: any, @Param('fileId') fileId: TApiFileId): Promise { const userId = request.user.sub; const fileStream = await this.fileService.getFileStreamById(fileId, userId); @@ -260,7 +271,7 @@ export class FilesController { }) @ApiTags('files') @Get('info/:fileId') - async getFileInfo(@Req() request: any, @Param('fileId') fileId: string): Promise { + async getFileInfo(@Req() request: any, @Param('fileId') fileId: TApiFileId): Promise { const userId = request.user.sub; const fileInfo = await this.fileService.getFileInfoById(fileId, userId); return fileInfo; diff --git a/backend/src/types/api/request.ts b/backend/src/types/api/request.ts index f746afc..e6ad597 100644 --- a/backend/src/types/api/request.ts +++ b/backend/src/types/api/request.ts @@ -8,13 +8,16 @@ export class SignInDTO { password: string; } +export type TApiFolderId = string +export type TApiFileId = string + /** TODO Share somehow types between fe and be */ export class CreateFolderDTO { @ApiProperty() folderName: string; @ApiProperty() - parentFolderId: string; + parentFolderId: TApiFolderId; } export class RenameFolderDTO {