From 0037ef245b9c072c1314d2b840a7dbf79044e9e1 Mon Sep 17 00:00:00 2001 From: Dejan Vasic Date: Wed, 27 Dec 2023 22:35:47 +1100 Subject: [PATCH] Delete should also be working now including a bit of a refactor or note inputs and ids --- src/lib/server/db/notesDb.ts | 7 ++++--- src/lib/server/parseRequest.test.ts | 12 ++++++------ src/lib/server/services/userService.ts | 17 ++++++++++++++++- src/lib/types.ts | 12 +----------- src/routes/api/notes/+server.ts | 24 +++++++----------------- src/routes/api/notes/[id]/+server.ts | 21 +++++++++++++++++---- src/routes/api/notes/[id]/server.test.ts | 23 ++++++++++++++++++++--- src/routes/api/notes/server.test.ts | 8 ++++---- 8 files changed, 75 insertions(+), 49 deletions(-) diff --git a/src/lib/server/db/notesDb.ts b/src/lib/server/db/notesDb.ts index 86d3cb9..4acd68b 100644 --- a/src/lib/server/db/notesDb.ts +++ b/src/lib/server/db/notesDb.ts @@ -3,7 +3,7 @@ import { pipe } from 'fp-ts/lib/function'; import db from '$lib/server/db'; import { withError } from '$lib/server/createError'; -import type { ServerError, Note, IdParams, NoteCreateInput } from '$lib/types'; +import type { ServerError, Note, IdParams } from '$lib/types'; import { fromNullableRecord, tryDbTask } from './utils'; export const getNoteById = ({ id }: IdParams): TE.TaskEither => @@ -34,12 +34,13 @@ export const deleteNote = ({ id }: { id: string }): TE.TaskEither => +export const createNote = (note: Note): TE.TaskEither => TE.tryCatch( () => { return db.note.create({ data: { - ...noteInput + ...note, + boardId: note.boardId! } }); }, diff --git a/src/lib/server/parseRequest.test.ts b/src/lib/server/parseRequest.test.ts index 21fecc8..f9fa7fb 100644 --- a/src/lib/server/parseRequest.test.ts +++ b/src/lib/server/parseRequest.test.ts @@ -1,13 +1,13 @@ import { describe, expect, it, vi } from 'vitest'; -import type { NoteCreateInput } from '$lib/types'; -import { NoteCreateInputSchema } from '$lib/types'; +import type { Note } from '$lib/types'; +import { NoteSchema } from '$lib/types'; import { parseRequest } from './parseRequest'; describe('parseRequest', () => { it('should return a NoteCreateInput when parsing succeeds', async () => { - const noteCreateInput: NoteCreateInput = { + const noteCreateInput: Note = { boardId: 'boardId', colour: 'colour', id: 'id', @@ -21,7 +21,7 @@ describe('parseRequest', () => { const result = await parseRequest( req as any, - NoteCreateInputSchema, + NoteSchema, 'Unable to parse note create input' )(); @@ -39,7 +39,7 @@ describe('parseRequest', () => { const result = await parseRequest( req as any, - NoteCreateInputSchema, + NoteSchema, 'Unable to parse note create input' )(); @@ -53,7 +53,7 @@ describe('parseRequest', () => { const result = await parseRequest( req as any, - NoteCreateInputSchema, + NoteSchema, 'Unable to parse note create input' )(); diff --git a/src/lib/server/services/userService.ts b/src/lib/server/services/userService.ts index f4ec322..00985f6 100644 --- a/src/lib/server/services/userService.ts +++ b/src/lib/server/services/userService.ts @@ -23,8 +23,8 @@ export const isBoardOwner = ({ ); interface IsNoteOwnerParams { - user: User; note: Note; + user: User; } export const isNoteOwner = ({ @@ -69,3 +69,18 @@ export const getOrCreateUserByAuth = ({ return TE.left(err); }) ); + +export const getCurrentBoardForUserNote = ({ + note, + user +}: { + note: Note; + user: User; +}): TE.TaskEither => { + const boardId = note.boardId; + const board = user.boards.find((b) => b.id === boardId); + if (!board) { + return TE.left(createError('RecordNotFound', `Board ${boardId} not found`)); + } + return TE.right({ note, user, board }); +}; diff --git a/src/lib/types.ts b/src/lib/types.ts index b704203..3f5d956 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -3,7 +3,7 @@ import z from 'zod'; export type { Colour } from '$lib/colours'; export const EntitySchema = z.object({ - id: z.string().optional(), + id: z.string(), createdAt: z.date().optional(), updatedAt: z.date().optional() }); @@ -70,16 +70,6 @@ export const AuthUserProfileSchema = z.object({ export type AuthUserProfile = z.infer; -export const NoteCreateInputSchema = z.object({ - id: z.string(), - boardId: z.string(), - colour: z.string().nullable(), - text: z.string(), - textPlain: z.string() -}); - -export type NoteCreateInput = z.infer; - interface BaseError { readonly message: string; readonly originalError?: Error | string | unknown; diff --git a/src/routes/api/notes/+server.ts b/src/routes/api/notes/+server.ts index 3bc826b..1688a23 100644 --- a/src/routes/api/notes/+server.ts +++ b/src/routes/api/notes/+server.ts @@ -6,31 +6,21 @@ import { taskEither as TE } from 'fp-ts/lib'; import { updateBoard } from '$lib/server/db/boardDb'; import { getUser } from '$lib/server/db/userDb'; import { createNote } from '$lib/server/db/notesDb'; -import { isNoteOwner } from '$lib/server/services/userService'; +import { isNoteOwner, getCurrentBoardForUserNote } from '$lib/server/services/userService'; import { parseRequest } from '$lib/server/parseRequest'; import { mapToApiError } from '$lib/server/mapApi'; -import { createError } from '$lib/server/createError'; -import { NoteCreateInputSchema } from '$lib/types'; +import { NoteSchema } from '$lib/types'; export const POST: RequestHandler = async ({ locals, request }) => { return pipe( TE.Do, - TE.bind('noteInput', () => - parseRequest(request, NoteCreateInputSchema, 'Unable to parse NoteCreateInputSchema') - ), + TE.bind('note', () => parseRequest(request, NoteSchema, 'Unable to parse Note')), TE.bind('user', () => getUser({ id: locals.user.id!, includeBoards: true })), - TE.flatMap(({ noteInput, user }) => isNoteOwner({ user, note: noteInput })), - TE.flatMap(({ note, user }) => { - const boardId = note.boardId; - const currentBoard = user.boards.find((b) => b.id === boardId); - if (!currentBoard) { - return TE.left(createError('RecordNotFound', `Board ${boardId} not found`)); - } - return TE.right({ note, user, currentBoard }); - }), - TE.flatMap(({ currentBoard, note, user }) => + TE.flatMap(({ note, user }) => isNoteOwner({ user, note })), + TE.flatMap(({ note, user }) => getCurrentBoardForUserNote({ user, note })), + TE.flatMap(({ board, note, user }) => pipe( - updateBoard({ ...currentBoard, noteOrder: [...currentBoard.noteOrder, note.id!] }), + updateBoard({ ...board, noteOrder: [...board.noteOrder, note.id!] }), TE.map(() => ({ note, user })) ) ), diff --git a/src/routes/api/notes/[id]/+server.ts b/src/routes/api/notes/[id]/+server.ts index 27854ca..3592795 100644 --- a/src/routes/api/notes/[id]/+server.ts +++ b/src/routes/api/notes/[id]/+server.ts @@ -4,9 +4,10 @@ import { taskEither as TE } from 'fp-ts'; import { pipe } from 'fp-ts/lib/function'; import { mapToApiError } from '$lib/server/mapApi'; +import { updateBoard } from '$lib/server/db/boardDb'; import { getNoteById, updateNote, deleteNote } from '$lib/server/db/notesDb'; import { getUser } from '$lib/server/db/userDb'; -import { isNoteOwner } from '$lib/server/services/userService'; +import { getCurrentBoardForUserNote, isNoteOwner } from '$lib/server/services/userService'; import { parseRequest } from '$lib/server/parseRequest'; import { NotePatchInputSchema } from '$lib/types'; @@ -46,9 +47,21 @@ export const DELETE: RequestHandler = async ({ locals, params }) => { return pipe( TE.Do, TE.bind('note', () => getNoteById({ id: params.id! })), - TE.bind('user', () => getUser({ id: locals.user.id! })), - TE.flatMap((params) => isNoteOwner(params)), - TE.flatMap(({ note }) => deleteNote({ id: note.id! })), + TE.bind('user', () => getUser({ id: locals.user.id!, includeBoards: true })), + TE.flatMap((p) => isNoteOwner(p)), + TE.flatMap((p) => getCurrentBoardForUserNote(p)), + TE.flatMap(({ board, note, user }) => { + return pipe( + updateBoard({ ...board, noteOrder: board.noteOrder.filter((id) => id !== note.id) }), + TE.map(() => ({ board, note, user })) + ); + }), + TE.flatMap(({ note, user }) => + pipe( + deleteNote({ id: note.id! }), + TE.map(() => ({ note, user })) + ) + ), TE.mapLeft(mapToApiError), TE.match( (err) => json({ message: err.message }, { status: err.status }), diff --git a/src/routes/api/notes/[id]/server.test.ts b/src/routes/api/notes/[id]/server.test.ts index b34c96d..dedeef1 100644 --- a/src/routes/api/notes/[id]/server.test.ts +++ b/src/routes/api/notes/[id]/server.test.ts @@ -3,13 +3,15 @@ import { taskEither as TE } from 'fp-ts'; import { getNoteById, updateNote, deleteNote } from '$lib/server/db/notesDb'; import { getUser } from '$lib/server/db/userDb'; -import { isNoteOwner } from '$lib/server/services/userService'; +import { updateBoard } from '$lib/server/db/boardDb'; +import { isNoteOwner, getCurrentBoardForUserNote } from '$lib/server/services/userService'; import type { AuthorizationError, NotePatchInput } from '$lib/types'; import { GET, PATCH, DELETE } from './+server'; vi.mock('$lib/server/db/notesDb'); vi.mock('$lib/server/db/userDb'); +vi.mock('$lib/server/db/boardDb'); vi.mock('$lib/server/services/userService'); const mockGetUser = getUser as MockedFunction; @@ -17,6 +19,10 @@ const mockGetNoteById = getNoteById as MockedFunction; const mockDeleteNote = deleteNote as MockedFunction; const mockUpdateNote = updateNote as MockedFunction; const mockIsNoteOwner = isNoteOwner as MockedFunction; +const mockUpdateBoard = updateBoard as MockedFunction; +const mockGetCurrentBoardForUserNote = getCurrentBoardForUserNote as MockedFunction< + typeof getCurrentBoardForUserNote +>; const mockNote = { id: 'nid_123', @@ -27,7 +33,7 @@ const mockNote = { const mockUser = { id: 'uid_123', username: 'testuser', - boards: [{ id: 'bid_123', name: 'Test board', ownerId: 'uid_123' }] + boards: [{ id: 'bid_123', name: 'Test board', ownerId: 'uid_123', noteOrder: [] }] }; describe('GET', () => { @@ -137,10 +143,20 @@ describe('PATCH', () => { describe('DELETE', () => { it('should return 204 and call the repository to delete the note successfully', async () => { const locals = { user: { id: 'uid_123' } }; - mockGetUser.mockReturnValue(TE.right(mockUser) as any); + const mockBoard = { ...mockUser.boards[0], noteOrder: [mockNote.id] }; + mockGetUser.mockReturnValue( + TE.right({ + ...mockUser, + boards: [mockBoard] + }) as any + ); mockGetNoteById.mockReturnValue(TE.right(mockNote) as any); mockIsNoteOwner.mockReturnValue(TE.right({ user: mockUser, note: mockNote }) as any); mockDeleteNote.mockReturnValue(TE.right(mockNote) as any); + mockGetCurrentBoardForUserNote.mockReturnValue( + TE.right({ user: mockUser, note: mockNote, board: mockBoard } as any) + ); + mockUpdateBoard.mockReturnValue(TE.right({} as any)); const result = await DELETE({ locals, @@ -148,6 +164,7 @@ describe('DELETE', () => { } as any); expect(mockIsNoteOwner).toHaveBeenCalledWith({ user: mockUser, note: mockNote }); + expect(mockUpdateBoard).toHaveBeenCalled(); expect(result.status).toBe(204); }); }); diff --git a/src/routes/api/notes/server.test.ts b/src/routes/api/notes/server.test.ts index afeba4c..e552913 100644 --- a/src/routes/api/notes/server.test.ts +++ b/src/routes/api/notes/server.test.ts @@ -5,16 +5,16 @@ import { taskEither as TE } from 'fp-ts'; import { updateBoard } from '$lib/server/db/boardDb'; import { createNote } from '$lib/server/db/notesDb'; import { getUser } from '$lib/server/db/userDb'; -import type { NoteCreateInput } from '$lib/types'; +import { createError } from '$lib/server/createError'; +import type { Note } from '$lib/types'; import { POST } from './+server'; -import { createError } from '$lib/server/createError'; vi.mock('$lib/server/db/boardDb'); vi.mock('$lib/server/db/notesDb'); vi.mock('$lib/server/db/userDb'); -const mockNoteInput: NoteCreateInput = { +const mockNoteInput: Note = { boardId: 'board_123', text: 'This is a note', textPlain: 'This is a note', @@ -75,7 +75,7 @@ describe('POST', () => { expect(resp.status).toBe(400); const data = await resp.json(); - expect(data).toEqual({ message: 'Unable to parse NoteCreateInputSchema', status: 400 }); + expect(data).toEqual({ message: 'Unable to parse Note', status: 400 }); }); it('should return a 403 when the user is not the owner of the note', async () => {