Skip to content

Commit

Permalink
Delete should also be working now including a bit of a refactor or no…
Browse files Browse the repository at this point in the history
…te inputs and ids
  • Loading branch information
dejanvasic85 committed Dec 27, 2023
1 parent 555f290 commit 0037ef2
Show file tree
Hide file tree
Showing 8 changed files with 75 additions and 49 deletions.
7 changes: 4 additions & 3 deletions src/lib/server/db/notesDb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ServerError, Note> =>
Expand Down Expand Up @@ -34,12 +34,13 @@ export const deleteNote = ({ id }: { id: string }): TE.TaskEither<ServerError, N
withError('DatabaseError', 'Failed to delete note')
);

export const createNote = (noteInput: NoteCreateInput): TE.TaskEither<ServerError, Note> =>
export const createNote = (note: Note): TE.TaskEither<ServerError, Note> =>
TE.tryCatch(
() => {
return db.note.create({
data: {
...noteInput
...note,
boardId: note.boardId!
}
});
},
Expand Down
12 changes: 6 additions & 6 deletions src/lib/server/parseRequest.test.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -21,7 +21,7 @@ describe('parseRequest', () => {

const result = await parseRequest(
req as any,
NoteCreateInputSchema,
NoteSchema,
'Unable to parse note create input'
)();

Expand All @@ -39,7 +39,7 @@ describe('parseRequest', () => {

const result = await parseRequest(
req as any,
NoteCreateInputSchema,
NoteSchema,
'Unable to parse note create input'
)();

Expand All @@ -53,7 +53,7 @@ describe('parseRequest', () => {

const result = await parseRequest(
req as any,
NoteCreateInputSchema,
NoteSchema,
'Unable to parse note create input'
)();

Expand Down
17 changes: 16 additions & 1 deletion src/lib/server/services/userService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ export const isBoardOwner = <T extends IsBoardOwnerParams>({
);

interface IsNoteOwnerParams {
user: User;
note: Note;
user: User;
}

export const isNoteOwner = <T extends IsNoteOwnerParams>({
Expand Down Expand Up @@ -69,3 +69,18 @@ export const getOrCreateUserByAuth = ({
return TE.left(err);
})
);

export const getCurrentBoardForUserNote = ({
note,
user
}: {
note: Note;
user: User;
}): TE.TaskEither<ServerError, { note: Note; user: User; board: Board }> => {
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 });
};
12 changes: 1 addition & 11 deletions src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
});
Expand Down Expand Up @@ -70,16 +70,6 @@ export const AuthUserProfileSchema = z.object({

export type AuthUserProfile = z.infer<typeof AuthUserProfileSchema>;

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<typeof NoteCreateInputSchema>;

interface BaseError {
readonly message: string;
readonly originalError?: Error | string | unknown;
Expand Down
24 changes: 7 additions & 17 deletions src/routes/api/notes/+server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }))
)
),
Expand Down
21 changes: 17 additions & 4 deletions src/routes/api/notes/[id]/+server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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 }),
Expand Down
23 changes: 20 additions & 3 deletions src/routes/api/notes/[id]/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,26 @@ 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<typeof getUser>;
const mockGetNoteById = getNoteById as MockedFunction<typeof getNoteById>;
const mockDeleteNote = deleteNote as MockedFunction<typeof deleteNote>;
const mockUpdateNote = updateNote as MockedFunction<typeof updateNote>;
const mockIsNoteOwner = isNoteOwner as MockedFunction<typeof isNoteOwner>;
const mockUpdateBoard = updateBoard as MockedFunction<typeof updateBoard>;
const mockGetCurrentBoardForUserNote = getCurrentBoardForUserNote as MockedFunction<
typeof getCurrentBoardForUserNote
>;

const mockNote = {
id: 'nid_123',
Expand All @@ -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', () => {
Expand Down Expand Up @@ -137,17 +143,28 @@ 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,
params: { id: mockNote.id }
} as any);

expect(mockIsNoteOwner).toHaveBeenCalledWith({ user: mockUser, note: mockNote });
expect(mockUpdateBoard).toHaveBeenCalled();
expect(result.status).toBe(204);
});
});
8 changes: 4 additions & 4 deletions src/routes/api/notes/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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 () => {
Expand Down

0 comments on commit 0037ef2

Please sign in to comment.