From 22cebc5a4b029329134b4fd2fd63369e3b8f7be0 Mon Sep 17 00:00:00 2001 From: Kristian Binau Date: Sat, 7 Oct 2023 18:16:53 +0200 Subject: [PATCH] #22 - Added Docs CRUD --- .../20231007154157_item_docs/migration.sql | 14 ++ prisma/schema.prisma | 15 +- src/locales/da.json | 20 ++ src/locales/en.json | 20 ++ src/modules/item/docs/__test__/add.test.ts | 192 ++++++++++++++ src/modules/item/docs/__test__/delete.test.ts | 154 ++++++++++++ src/modules/item/docs/__test__/edit.test.ts | 205 +++++++++++++++ src/modules/item/docs/__test__/read.test.ts | 153 +++++++++++ src/modules/item/docs/docs.controller.ts | 118 +++++++++ src/modules/item/docs/docs.route.ts | 91 +++++++ src/modules/item/docs/docs.schema.ts | 238 ++++++++++++++++++ src/modules/item/docs/docs.service.ts | 79 ++++++ src/modules/item/docs/index.ts | 13 + src/modules/item/index.ts | 6 +- 14 files changed, 1313 insertions(+), 5 deletions(-) create mode 100644 prisma/migrations/20231007154157_item_docs/migration.sql create mode 100644 src/modules/item/docs/__test__/add.test.ts create mode 100644 src/modules/item/docs/__test__/delete.test.ts create mode 100644 src/modules/item/docs/__test__/edit.test.ts create mode 100644 src/modules/item/docs/__test__/read.test.ts create mode 100644 src/modules/item/docs/docs.controller.ts create mode 100644 src/modules/item/docs/docs.route.ts create mode 100644 src/modules/item/docs/docs.schema.ts create mode 100644 src/modules/item/docs/docs.service.ts create mode 100644 src/modules/item/docs/index.ts diff --git a/prisma/migrations/20231007154157_item_docs/migration.sql b/prisma/migrations/20231007154157_item_docs/migration.sql new file mode 100644 index 0000000..92bc15a --- /dev/null +++ b/prisma/migrations/20231007154157_item_docs/migration.sql @@ -0,0 +1,14 @@ +-- CreateTable +CREATE TABLE "ItemDocs" ( + "id" SERIAL NOT NULL, + "text" TEXT NOT NULL, + "itemId" INTEGER NOT NULL, + + CONSTRAINT "ItemDocs_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "ItemDocs_itemId_key" ON "ItemDocs"("itemId"); + +-- AddForeignKey +ALTER TABLE "ItemDocs" ADD CONSTRAINT "ItemDocs_itemId_fkey" FOREIGN KEY ("itemId") REFERENCES "Item"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 2db0ecd..3637c60 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -53,6 +53,7 @@ model Item { Items Item[] @relation("ItemToItem") ItemFolder ItemFolder? ItemBlob ItemBlob? + ItemDocs ItemDocs? ItemSharing ItemSharing[] createdAt DateTime @default(now()) @@ -66,9 +67,9 @@ model Item { } model ItemFolder { - id Int @id @default(autoincrement()) - color String @db.VarChar(255) - itemId Int @unique + id Int @id @default(autoincrement()) + color String @db.VarChar(255) + itemId Int @unique item Item @relation(fields: [itemId], references: [id], onDelete: Cascade) } @@ -81,6 +82,14 @@ model ItemBlob { item Item @relation(fields: [itemId], references: [id], onDelete: Cascade) } +model ItemDocs { + id Int @id @default(autoincrement()) + text String @db.Text + itemId Int @unique + + item Item @relation(fields: [itemId], references: [id], onDelete: Cascade) +} + model ItemSharing { id Int @id @default(autoincrement()) itemId Int diff --git a/src/locales/da.json b/src/locales/da.json index e6a13ce..8ea73e2 100644 --- a/src/locales/da.json +++ b/src/locales/da.json @@ -105,6 +105,9 @@ "folder": { "notFound": "Mappen blev ikke fundet" }, + "docs": { + "notFound": "Dokumentet blev ikke fundet" + }, "notFound": "Item ikke fundet" }, "folder": { @@ -123,5 +126,22 @@ "parentId": { "type": "Parent id skal være et tal" } + }, + "docs": { + "id": { + "required": "id er påkrævet", + "type": "id skal være et tal" + }, + "name": { + "required": "Navn er påkrævet", + "type": "Navn skal være en tekst" + }, + "text": { + "required": "Tekst er påkrævet", + "type": "Tekst skal være en tekst" + }, + "parentId": { + "type": "Parent id skal være et tal" + } } } diff --git a/src/locales/en.json b/src/locales/en.json index a921d5a..b9ba1cb 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -105,6 +105,9 @@ "folder": { "notFound": "Folder not found" }, + "docs": { + "notFound": "Docs not found" + }, "notFound": "Item not found" }, "folder": { @@ -123,5 +126,22 @@ "parentId": { "type": "Parent id must be a number" } + }, + "docs": { + "id": { + "required": "id is required", + "type": "id must be a number" + }, + "name": { + "required": "Name is required", + "type": "Name must be a string" + }, + "text": { + "required": "Text is required", + "type": "Text must be a string" + }, + "parentId": { + "type": "Parent id must be a number" + } } } diff --git a/src/modules/item/docs/__test__/add.test.ts b/src/modules/item/docs/__test__/add.test.ts new file mode 100644 index 0000000..f5c0959 --- /dev/null +++ b/src/modules/item/docs/__test__/add.test.ts @@ -0,0 +1,192 @@ +import { User } from '@prisma/client'; +import UserService from '../../../auth/user.service'; +import AuthService from '../../../auth/auth.service'; +import DocsService from '../docs.service'; + +describe('POST /api/docs', () => { + let userService: UserService; + let authService: AuthService; + let docsService: DocsService; + + let user: User; + let otherUser: User; + + beforeAll(async () => { + authService = new AuthService(); + userService = new UserService(); + docsService = new DocsService(); + + user = await userService.createUser({ + name: 'Joe Biden the 1st', + email: 'joe@biden.com', + password: '1234', + }); + otherUser = await userService.createUser({ + name: 'Joe Biden the 2nd', + email: 'joe2@biden.com', + password: '4321', + }); + }); + + it('should return status 200 and docs', async () => { + const { accessToken } = await authService.createTokens(user.id); + + const response = await global.fastify.inject({ + method: 'POST', + url: '/api/docs', + headers: { + authorization: 'Bearer ' + accessToken, + }, + payload: { + name: 'Docs Name', + text: 'Hej mit navn er test 123', + }, + }); + + expect(response.statusCode).toBe(200); + expect(response.json()).toEqual({ + id: expect.any(Number), + name: 'Docs Name', + text: 'Hej mit navn er test 123', + parentId: null, + ownerId: user.id, + mimeType: 'application/vnd.cloudstore.docs', + createdAt: expect.any(String), + deletedAt: null, + updatedAt: expect.any(String), + }); + }); + + it('should return status 401, when unauthorized', async () => { + const response = await global.fastify.inject({ + method: 'POST', + url: '/api/docs', + headers: { + authorization: 'invalid_access_token!!!', + }, + payload: { + name: 'Docs Name', + text: 'Hej mit navn er test 123', + parentId: null, + }, + }); + + expect(response.statusCode).toBe(401); + expect(response.json()).toEqual({ + error: 'UnauthorizedError', + errors: { + _: ['Unauthorized'], + }, + statusCode: 401, + }); + }); + + it('should return status 401, when parent id is provided, but no access to parent', async () => { + const { accessToken } = await authService.createTokens(user.id); + + const docs = await docsService.createDocs({ + name: 'Docs1', + ownerId: otherUser.id, + parentId: null, + text: 'Hej mit navn er test 123', + }); + + const response = await global.fastify.inject({ + method: 'POST', + url: '/api/docs', + headers: { + authorization: 'Bearer ' + accessToken, + }, + payload: { + name: 'Docs Name', + text: 'Hej mit navn er test 123', + parentId: docs.id, + }, + }); + + expect(response.statusCode).toBe(401); + expect(response.json()).toEqual({ + error: 'UnauthorizedError', + errors: { + _: ['Unauthorized'], + }, + statusCode: 401, + }); + }); + + it('should return status 401, when docs name is not provided', async () => { + const { accessToken } = await authService.createTokens(user.id); + + const response = await global.fastify.inject({ + method: 'POST', + url: '/api/docs', + headers: { + authorization: 'Bearer ' + accessToken, + }, + payload: { + text: 'Hej mit navn er test 123', + parentId: null, + }, + }); + + expect(response.statusCode).toBe(400); + expect(response.json()).toEqual({ + error: 'ValidationError', + errors: { + _: ['Name is required'], + }, + statusCode: 400, + }); + }); + + it('should return status 401, when docs text is not provided', async () => { + const { accessToken } = await authService.createTokens(user.id); + + const response = await global.fastify.inject({ + method: 'POST', + url: '/api/docs', + headers: { + authorization: 'Bearer ' + accessToken, + }, + payload: { + name: 'Docs name', + parentId: null, + }, + }); + + expect(response.statusCode).toBe(400); + expect(response.json()).toEqual({ + error: 'ValidationError', + errors: { + _: ['Text is required'], + }, + statusCode: 400, + }); + }); + + it("should return status 400, when parent id isn't a number", async () => { + const { accessToken } = await authService.createTokens(user.id); + + const response = await global.fastify.inject({ + method: 'POST', + url: '/api/docs', + headers: { + authorization: 'Bearer ' + accessToken, + }, + payload: { + name: 'Docs Name', + text: 'Hej mit navn er test 123', + parentId: 'invalid_id', + }, + }); + + expect(response.statusCode).toBe(400); + expect(response.json()).toEqual({ + error: 'ValidationError', + errors: { + parentId: ['Parent id must be a number'], + }, + statusCode: 400, + }); + }); +}); diff --git a/src/modules/item/docs/__test__/delete.test.ts b/src/modules/item/docs/__test__/delete.test.ts new file mode 100644 index 0000000..93a32a8 --- /dev/null +++ b/src/modules/item/docs/__test__/delete.test.ts @@ -0,0 +1,154 @@ +import { User } from '@prisma/client'; +import UserService from '../../../auth/user.service'; +import AuthService from '../../../auth/auth.service'; +import DocsService from '../docs.service'; +import ItemService from '../../item.service'; + +describe('DELETE /api/docs/:id', () => { + let userService: UserService; + let authService: AuthService; + let docsService: DocsService; + let itemService: ItemService; + + let user: User; + let otherUser: User; + + beforeAll(async () => { + authService = new AuthService(); + userService = new UserService(); + docsService = new DocsService(); + itemService = new ItemService(); + + user = await userService.createUser({ + name: 'Joe Biden the 1st', + email: 'joe@biden.com', + password: '1234', + }); + otherUser = await userService.createUser({ + name: 'Joe Biden the 2nd', + email: 'joe2@biden.com', + password: '4321', + }); + }); + + it('should delete docs AND item', async () => { + const { accessToken } = await authService.createTokens(user.id); + + const docs = await docsService.createDocs({ + name: 'Docs1', + ownerId: user.id, + parentId: null, + text: 'Hej mit navn er test 123', + }); + + const response = await global.fastify.inject({ + method: 'DELETE', + url: '/api/docs/' + docs.id, + headers: { + authorization: 'Bearer ' + accessToken, + }, + }); + + expect(response.statusCode).toBe(204); + expect(response.body).toBe(''); + + await expect(docsService.getByItemId(docs.id)).rejects.toThrowError(); + await expect(itemService.getById(docs.id)).rejects.toThrowError(); + }); + + it('should return status 401, when unauthorized', async () => { + const docs = await docsService.createDocs({ + name: 'Docs1', + ownerId: user.id, + parentId: null, + text: 'Hej mit navn er test 123', + }); + + const response = await global.fastify.inject({ + method: 'DELETE', + url: '/api/docs/' + docs.id, + headers: { + authorization: 'invalid_access_token!!!', + }, + }); + + expect(response.statusCode).toBe(401); + expect(response.json()).toEqual({ + error: 'UnauthorizedError', + errors: { + _: ['Unauthorized'], + }, + statusCode: 401, + }); + }); + + it('should return status 401, when docs id is not accessible to you', async () => { + const { accessToken } = await authService.createTokens(user.id); + + const docs = await docsService.createDocs({ + name: 'Docs1', + ownerId: otherUser.id, + parentId: null, + text: 'Hej mit navn er test 123', + }); + + const response = await global.fastify.inject({ + method: 'DELETE', + url: '/api/docs/' + docs.id, + headers: { + authorization: 'Bearer ' + accessToken, + }, + }); + + expect(response.statusCode).toBe(401); + expect(response.json()).toEqual({ + error: 'UnauthorizedError', + errors: { + _: ['Unauthorized'], + }, + statusCode: 401, + }); + }); + + it("should return status 400, when docs id isn't a number", async () => { + const { accessToken } = await authService.createTokens(user.id); + + const response = await global.fastify.inject({ + method: 'DELETE', + url: '/api/docs/invalid_id', + headers: { + authorization: 'Bearer ' + accessToken, + }, + }); + + expect(response.statusCode).toBe(400); + expect(response.json()).toEqual({ + error: 'ValidationError', + errors: { + id: ['id must be a number'], + }, + statusCode: 400, + }); + }); + + it("should return status 400, when docs with id doesn't exist", async () => { + const { accessToken } = await authService.createTokens(user.id); + + const response = await global.fastify.inject({ + method: 'DELETE', + url: '/api/docs/1234', + headers: { + authorization: 'Bearer ' + accessToken, + }, + }); + + expect(response.statusCode).toBe(400); + expect(response.json()).toEqual({ + error: 'BadRequestError', + errors: { + _: ['Docs not found'], + }, + statusCode: 400, + }); + }); +}); diff --git a/src/modules/item/docs/__test__/edit.test.ts b/src/modules/item/docs/__test__/edit.test.ts new file mode 100644 index 0000000..3938b1c --- /dev/null +++ b/src/modules/item/docs/__test__/edit.test.ts @@ -0,0 +1,205 @@ +import { User } from '@prisma/client'; +import UserService from '../../../auth/user.service'; +import AuthService from '../../../auth/auth.service'; +import DocsService from '../docs.service'; + +describe('PUT /api/docs', () => { + let userService: UserService; + let authService: AuthService; + let docsService: DocsService; + + let user: User; + let otherUser: User; + + beforeAll(async () => { + authService = new AuthService(); + userService = new UserService(); + docsService = new DocsService(); + + user = await userService.createUser({ + name: 'Joe Biden the 1st', + email: 'joe@biden.com', + password: '1234', + }); + otherUser = await userService.createUser({ + name: 'Joe Biden the 2nd', + email: 'joe2@biden.com', + password: '4321', + }); + }); + + it('should return status 200 and docs', async () => { + const { accessToken } = await authService.createTokens(user.id); + + const docs = await docsService.createDocs({ + name: 'Docs1', + ownerId: user.id, + parentId: null, + text: 'Hej mit navn er test 123', + }); + + const response = await global.fastify.inject({ + method: 'PUT', + url: '/api/docs', + headers: { + authorization: 'Bearer ' + accessToken, + }, + payload: { + id: docs.id, + name: docs.name + ' updated', + text: 'Hej mit navn er test 123', + }, + }); + + expect(response.statusCode).toBe(200); + expect(response.json()).toEqual({ + ...docs, + name: docs.name + ' updated', + text: 'Hej mit navn er test 123', + createdAt: docs.createdAt.toISOString(), + updatedAt: expect.any(String), + deletedAt: docs.deletedAt?.toISOString() ?? null, + }); + }); + + it('should return status 401, when unauthorized', async () => { + const docs = await docsService.createDocs({ + name: 'Docs1', + ownerId: user.id, + parentId: null, + text: 'Hej mit navn er test 123', + }); + + const response = await global.fastify.inject({ + method: 'PUT', + url: '/api/docs', + headers: { + authorization: 'invalid_access_token!!!', + }, + payload: { + id: docs.id, + name: docs.name + ' updated', + text: 'Hej mit navn er test 123', + }, + }); + + expect(response.statusCode).toBe(401); + expect(response.json()).toEqual({ + error: 'UnauthorizedError', + errors: { + _: ['Unauthorized'], + }, + statusCode: 401, + }); + }); + + it('should return status 401, when docs id is not accessible to you', async () => { + const { accessToken } = await authService.createTokens(user.id); + + const docs = await docsService.createDocs({ + name: 'Docs1', + ownerId: otherUser.id, + parentId: null, + text: 'Hej mit navn er test 123', + }); + + const response = await global.fastify.inject({ + method: 'PUT', + url: '/api/docs', + headers: { + authorization: 'Bearer ' + accessToken, + }, + payload: { + id: docs.id, + name: docs.name + ' updated', + text: 'Hej mit navn er test 123', + }, + }); + + expect(response.statusCode).toBe(401); + expect(response.json()).toEqual({ + error: 'UnauthorizedError', + errors: { + _: ['Unauthorized'], + }, + statusCode: 401, + }); + }); + + it("should return status 400, when docs id isn't a number", async () => { + const { accessToken } = await authService.createTokens(user.id); + + const response = await global.fastify.inject({ + method: 'PUT', + url: '/api/docs', + headers: { + authorization: 'Bearer ' + accessToken, + }, + payload: { + id: 'invalid_id', + name: 'updated', + text: 'Hej mit navn er test 123', + }, + }); + + expect(response.statusCode).toBe(400); + expect(response.json()).toEqual({ + error: 'ValidationError', + errors: { + id: ['id must be a number'], + }, + statusCode: 400, + }); + }); + + it("should return status 400, when docs id isn't given", async () => { + const { accessToken } = await authService.createTokens(user.id); + + const response = await global.fastify.inject({ + method: 'PUT', + url: '/api/docs', + headers: { + authorization: 'Bearer ' + accessToken, + }, + payload: { + name: 'updated', + text: 'Hej mit navn er test 123', + }, + }); + + expect(response.statusCode).toBe(400); + expect(response.json()).toEqual({ + error: 'ValidationError', + errors: { + _: ['id is required'], + }, + statusCode: 400, + }); + }); + + it("should return status 400, when docs with id doesn't exist", async () => { + const { accessToken } = await authService.createTokens(user.id); + + const response = await global.fastify.inject({ + method: 'PUT', + url: '/api/docs', + headers: { + authorization: 'Bearer ' + accessToken, + }, + payload: { + id: 1234, + name: 'updated', + text: 'Hej mit navn er test 123', + }, + }); + + expect(response.statusCode).toBe(400); + expect(response.json()).toEqual({ + error: 'BadRequestError', + errors: { + _: ['Docs not found'], + }, + statusCode: 400, + }); + }); +}); diff --git a/src/modules/item/docs/__test__/read.test.ts b/src/modules/item/docs/__test__/read.test.ts new file mode 100644 index 0000000..7676680 --- /dev/null +++ b/src/modules/item/docs/__test__/read.test.ts @@ -0,0 +1,153 @@ +import { User } from '@prisma/client'; +import UserService from '../../../auth/user.service'; +import AuthService from '../../../auth/auth.service'; +import DocsService from '../docs.service'; + +describe('GET /api/docs/:id', () => { + let userService: UserService; + let authService: AuthService; + let docsService: DocsService; + + let user: User; + let otherUser: User; + + beforeAll(async () => { + authService = new AuthService(); + userService = new UserService(); + docsService = new DocsService(); + + user = await userService.createUser({ + name: 'Joe Biden the 1st', + email: 'joe@biden.com', + password: '1234', + }); + otherUser = await userService.createUser({ + name: 'Joe Biden the 2nd', + email: 'joe2@biden.com', + password: '4321', + }); + }); + + it('should return status 200 and docs', async () => { + const { accessToken } = await authService.createTokens(user.id); + + const docs = await docsService.createDocs({ + name: 'Docs1', + ownerId: user.id, + parentId: null, + text: 'Hej mit navn er test 123', + }); + + const response = await global.fastify.inject({ + method: 'GET', + url: '/api/docs/' + docs.id, + headers: { + authorization: 'Bearer ' + accessToken, + }, + }); + + expect(response.statusCode).toBe(200); + expect(response.json()).toEqual({ + ...docs, + createdAt: docs.createdAt.toISOString(), + updatedAt: docs.updatedAt.toISOString(), + deletedAt: docs.deletedAt?.toISOString() ?? null, + }); + }); + + it('should return status 401, when unauthorized', async () => { + const docs = await docsService.createDocs({ + name: 'Docs1', + ownerId: user.id, + parentId: null, + text: 'Hej mit navn er test 123', + }); + + const response = await global.fastify.inject({ + method: 'GET', + url: '/api/docs/' + docs.id, + headers: { + authorization: 'invalid_access_token!!!', + }, + }); + + expect(response.statusCode).toBe(401); + expect(response.json()).toEqual({ + error: 'UnauthorizedError', + errors: { + _: ['Unauthorized'], + }, + statusCode: 401, + }); + }); + + it('should return status 401, when docs id is not accessible to you', async () => { + const { accessToken } = await authService.createTokens(user.id); + + const docs = await docsService.createDocs({ + name: 'Docs1', + ownerId: otherUser.id, + parentId: null, + text: 'Hej mit navn er test 123', + }); + + const response = await global.fastify.inject({ + method: 'GET', + url: '/api/docs/' + docs.id, + headers: { + authorization: 'Bearer ' + accessToken, + }, + }); + + expect(response.statusCode).toBe(401); + expect(response.json()).toEqual({ + error: 'UnauthorizedError', + errors: { + _: ['Unauthorized'], + }, + statusCode: 401, + }); + }); + + it("should return status 400, when docs id isn't a number", async () => { + const { accessToken } = await authService.createTokens(user.id); + + const response = await global.fastify.inject({ + method: 'GET', + url: '/api/docs/invalid_id', + headers: { + authorization: 'Bearer ' + accessToken, + }, + }); + + expect(response.statusCode).toBe(400); + expect(response.json()).toEqual({ + error: 'ValidationError', + errors: { + id: ['id must be a number'], + }, + statusCode: 400, + }); + }); + + it("should return status 400, when docs with id doesn't exist", async () => { + const { accessToken } = await authService.createTokens(user.id); + + const response = await global.fastify.inject({ + method: 'GET', + url: '/api/docs/1234', + headers: { + authorization: 'Bearer ' + accessToken, + }, + }); + + expect(response.statusCode).toBe(400); + expect(response.json()).toEqual({ + error: 'BadRequestError', + errors: { + _: ['Docs not found'], + }, + statusCode: 400, + }); + }); +}); diff --git a/src/modules/item/docs/docs.controller.ts b/src/modules/item/docs/docs.controller.ts new file mode 100644 index 0000000..74abebd --- /dev/null +++ b/src/modules/item/docs/docs.controller.ts @@ -0,0 +1,118 @@ +import { FastifyReply, FastifyRequest } from 'fastify'; +import { ReadInput, EditInput, AddInput, DeleteInput } from './docs.schema'; +import DocsService from './docs.service'; +import AccessService from '../sharing/access.service'; + +export default class DocsController { + private docsService: DocsService; + private accessService: AccessService; + + constructor(docsService: DocsService, accessService: AccessService) { + this.docsService = docsService; + this.accessService = accessService; + } + + public async readHandler( + request: FastifyRequest<{ + Params: ReadInput; + }>, + reply: FastifyReply, + ) { + try { + const docs = await this.docsService.getByItemId(request.params.id); + + if (!(await this.accessService.hasAccessToItem(docs.id, request.user.sub))) { + return reply.unauthorized(); + } + + return reply.code(200).send(docs); + } catch (e) { + if (e instanceof Error) { + return reply.badRequest(request.i18n.t(e.message)); + } + + /* istanbul ignore next */ + return reply.badRequest(); + } + } + + public async editHandler( + request: FastifyRequest<{ + Body: EditInput; + }>, + reply: FastifyReply, + ) { + try { + const docs = await this.docsService.getByItemId(request.body.id); + + if (!(await this.accessService.hasAccessToItem(docs.id, request.user.sub))) { + return reply.unauthorized(); + } + + const updatedDocs = await this.docsService.updateDocs(request.body); + + return reply.code(200).send(updatedDocs); + } catch (e) { + if (e instanceof Error) { + return reply.badRequest(request.i18n.t(e.message)); + } + + /* istanbul ignore next */ + return reply.badRequest(); + } + } + + public async addHandler( + request: FastifyRequest<{ + Body: AddInput; + }>, + reply: FastifyReply, + ) { + try { + if ( + request.body.parentId !== null && + request.body.parentId !== undefined && + !(await this.accessService.hasAccessToItem(request.body.parentId, request.user.sub)) + ) { + return reply.unauthorized(); + } + + const docs = await this.docsService.createDocs({ + name: request.body.name, + text: request.body.text, + ownerId: request.user.sub, + parentId: request.body.parentId ?? null, + }); + + return reply.code(200).send(docs); + } catch (e) { + /* istanbul ignore next */ + return reply.badRequest(); + } + } + + public async deleteHandler( + request: FastifyRequest<{ + Params: DeleteInput; + }>, + reply: FastifyReply, + ) { + try { + const docs = await this.docsService.getByItemId(request.params.id); + + if (!(await this.accessService.hasAccessToItem(docs.id, request.user.sub))) { + return reply.unauthorized(); + } + + await this.docsService.deleteDocsByItemId(docs.id); + return reply.code(204).send(); + } catch (e) { + if (e instanceof Error) { + return reply.badRequest(request.i18n.t(e.message)); + } + + /* istanbul ignore next */ + return reply.badRequest(); + } + } +} diff --git a/src/modules/item/docs/docs.route.ts b/src/modules/item/docs/docs.route.ts new file mode 100644 index 0000000..d3e4df8 --- /dev/null +++ b/src/modules/item/docs/docs.route.ts @@ -0,0 +1,91 @@ +import { FastifyInstance } from 'fastify'; +import DocsController from './docs.controller'; +import DocsService from './docs.service'; +import AccessService from '../sharing/access.service'; +import ItemService from '../item.service'; +import SharingService from '../sharing/sharing.service'; + +export default async (fastify: FastifyInstance) => { + const docsService = new DocsService(); + const docsController = new DocsController( + docsService, + new AccessService(new ItemService(), new SharingService()), + ); + + fastify.get( + '/:id', + { + schema: { + tags: ['Docs'], + params: { $ref: 'readDocsSchema' }, + response: { + 200: { $ref: 'readDocsResponseSchema' }, + }, + security: [ + { + bearerAuth: [], + }, + ], + }, + onRequest: [fastify.authenticate], + }, + docsController.readHandler.bind(docsController), + ); + + fastify.put( + '/', + { + schema: { + tags: ['Docs'], + body: { $ref: 'editDocsSchema' }, + response: { + 200: { $ref: 'editDocsResponseSchema' }, + }, + security: [ + { + bearerAuth: [], + }, + ], + }, + onRequest: [fastify.authenticate], + }, + docsController.editHandler.bind(docsController), + ); + + fastify.post( + '/', + { + schema: { + tags: ['Docs'], + body: { $ref: 'addDocsSchema' }, + response: { + 200: { $ref: 'addDocsResponseSchema' }, + }, + security: [ + { + bearerAuth: [], + }, + ], + }, + onRequest: [fastify.authenticate], + }, + docsController.addHandler.bind(docsController), + ); + + fastify.delete( + '/:id', + { + schema: { + tags: ['Docs'], + params: { $ref: 'deleteDocsSchema' }, + security: [ + { + bearerAuth: [], + }, + ], + }, + onRequest: [fastify.authenticate], + }, + docsController.deleteHandler.bind(docsController), + ); +}; diff --git a/src/modules/item/docs/docs.schema.ts b/src/modules/item/docs/docs.schema.ts new file mode 100644 index 0000000..f4980fa --- /dev/null +++ b/src/modules/item/docs/docs.schema.ts @@ -0,0 +1,238 @@ +import { FromSchema } from 'json-schema-to-ts'; +import { Item, UpdateItem } from '../item.schema'; +import { ItemDocs as prismaItemDocsType } from '@prisma/client'; + +const readDocsSchema = { + $id: 'readDocsSchema', + type: 'object', + properties: { + id: { + type: 'number', + errorMessage: { + type: 'docs.id.type', + }, + }, + }, + required: ['id'], + errorMessage: { + required: { + id: 'docs.id.required', + }, + }, +} as const; + +const readDocsResponseSchema = { + $id: 'readDocsResponseSchema', + type: 'object', + properties: { + id: { + type: 'number', + }, + text: { + type: 'string', + }, + parentId: { + type: ['number', 'null'], + }, + name: { + type: 'string', + }, + mimeType: { + type: 'string', + }, + ownerId: { + type: 'number', + }, + deletedAt: { + type: ['string', 'null'], + }, + createdAt: { + type: 'string', + }, + updatedAt: { + type: 'string', + }, + }, +} as const; + +const editDocsSchema = { + $id: 'editDocsSchema', + type: 'object', + properties: { + id: { + type: 'number', + errorMessage: { + type: 'docs.id.type', + }, + }, + name: { + type: 'string', + errorMessage: { + type: 'docs.name.type', + }, + }, + text: { + type: 'string', + errorMessage: { + type: 'docs.text.type', + }, + }, + parentId: { + type: ['number', 'null'], + errorMessage: { + type: 'docs.parentId.type', + }, + }, + }, + required: ['id'], + errorMessage: { + required: { + id: 'docs.id.required', + }, + }, +} as const; + +const editDocsResponseSchema = { + $id: 'editDocsResponseSchema', + type: 'object', + properties: { + id: { + type: 'number', + }, + text: { + type: 'string', + }, + parentId: { + type: ['number', 'null'], + }, + name: { + type: 'string', + }, + mimeType: { + type: 'string', + }, + ownerId: { + type: 'number', + }, + deletedAt: { + type: ['string', 'null'], + }, + createdAt: { + type: 'string', + }, + updatedAt: { + type: 'string', + }, + }, +} as const; + +const addDocsSchema = { + $id: 'addDocsSchema', + type: 'object', + properties: { + name: { + type: 'string', + errorMessage: { + type: 'docs.name.type', + }, + }, + text: { + type: 'string', + errorMessage: { + type: 'docs.text.type', + }, + }, + parentId: { + type: ['number', 'null'], + errorMessage: { + type: 'docs.parentId.type', + }, + }, + }, + required: ['name', 'text'], + errorMessage: { + required: { + name: 'docs.name.required', + text: 'docs.text.required', + }, + }, +} as const; + +const addDocsResponseSchema = { + $id: 'addDocsResponseSchema', + type: 'object', + properties: { + id: { + type: 'number', + }, + name: { + type: 'string', + }, + text: { + type: 'string', + }, + parentId: { + type: ['number', 'null'], + }, + mimeType: { + type: 'string', + }, + ownerId: { + type: 'number', + }, + deletedAt: { + type: ['string', 'null'], + }, + createdAt: { + type: 'string', + }, + updatedAt: { + type: 'string', + }, + }, +} as const; + +const deleteDocsSchema = { + $id: 'deleteDocsSchema', + type: 'object', + properties: { + id: { + type: 'number', + errorMessage: { + type: 'docs.id.type', + }, + }, + }, + required: ['id'], + errorMessage: { + required: { + itemId: 'docs.id.required', + }, + }, +} as const; + +export type AddInput = FromSchema; +export type ReadInput = FromSchema; +export type EditInput = FromSchema; +export type DeleteInput = FromSchema; + +export type AddDocs = { + name: string; + text: string; + ownerId: number; + parentId: number | null; +}; + +export type ItemDocs = prismaItemDocsType & { item: Item }; +export type Docs = Omit & Item; +export type UpdateDocs = { id: number } & Partial & Omit; + +export const docsSchemas = [ + addDocsSchema, + addDocsResponseSchema, + readDocsSchema, + readDocsResponseSchema, + editDocsSchema, + editDocsResponseSchema, + deleteDocsSchema, +]; diff --git a/src/modules/item/docs/docs.service.ts b/src/modules/item/docs/docs.service.ts new file mode 100644 index 0000000..d65fbef --- /dev/null +++ b/src/modules/item/docs/docs.service.ts @@ -0,0 +1,79 @@ +import { prisma } from '../../../plugins/prisma'; +import { Docs, AddDocs, UpdateDocs, ItemDocs } from './docs.schema'; + +export default class DocsService { + public async createDocs(input: AddDocs): Promise { + const itemDocs = await prisma.itemDocs.create({ + data: { + text: input.text, + item: { + create: { + name: input.name, + mimeType: 'application/vnd.cloudstore.docs', + ownerId: input.ownerId, + parentId: input.parentId, + }, + }, + }, + include: { + item: true, + }, + }); + + return this.formatitemDocs(itemDocs); + } + + public async getByItemId(itemId: number): Promise { + const itemDocs = await prisma.itemDocs.findUnique({ + where: { + itemId, + }, + include: { + item: true, + }, + }); + + if (!itemDocs) { + throw new Error('item.docs.notFound'); + } + + return this.formatitemDocs(itemDocs); + } + + public async updateDocs(input: UpdateDocs): Promise { + const itemDocs = await prisma.itemDocs.update({ + data: { + text: input.text, + item: { + update: { + name: input.name, + parentId: input.parentId, + }, + }, + }, + where: { + itemId: input.id, + }, + include: { + item: true, + }, + }); + + return this.formatitemDocs(itemDocs); + } + + public async deleteDocsByItemId(itemId: number): Promise { + await prisma.item.delete({ + where: { + id: itemId, + }, + }); + } + + private formatitemDocs(itemDocs: ItemDocs): Docs { + return { + text: itemDocs.text, + ...itemDocs.item, + }; + } +} diff --git a/src/modules/item/docs/index.ts b/src/modules/item/docs/index.ts new file mode 100644 index 0000000..5072659 --- /dev/null +++ b/src/modules/item/docs/index.ts @@ -0,0 +1,13 @@ +import { FastifyInstance, FastifyPluginOptions } from 'fastify'; + +import fastifyPlugin from 'fastify-plugin'; +import { docsSchemas } from './docs.schema'; +import docsRoute from './docs.route'; + +export default fastifyPlugin(async (fastify: FastifyInstance, options: FastifyPluginOptions) => { + for (const schema of docsSchemas) { + fastify.addSchema(schema); + } + + await fastify.register(docsRoute, options); +}); diff --git a/src/modules/item/index.ts b/src/modules/item/index.ts index 58c9575..cfbb51c 100644 --- a/src/modules/item/index.ts +++ b/src/modules/item/index.ts @@ -1,14 +1,16 @@ import { FastifyInstance, FastifyPluginOptions } from 'fastify'; import fastifyPlugin from 'fastify-plugin'; -import folder from './folder'; import { getOptionsWithPrefix } from '..'; import blob from './blob'; +import docs from './docs'; +import folder from './folder'; import sharing from './sharing'; -import { itemSchemas } from './item.schema'; import itemRoute from './item.route'; +import { itemSchemas } from './item.schema'; export default fastifyPlugin(async (fastify: FastifyInstance, options: FastifyPluginOptions) => { await fastify.register(blob, getOptionsWithPrefix(options, '/blob')); + await fastify.register(docs, getOptionsWithPrefix(options, '/docs')); await fastify.register(folder, getOptionsWithPrefix(options, '/folder')); await fastify.register(sharing, getOptionsWithPrefix(options, '/sharing')); await fastify.register(itemRoute, getOptionsWithPrefix(options, '/item'));