Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/notes #64

Merged
merged 7 commits into from
Aug 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* @typedef SingleGetNoteListDto
* @property {number} id - id of the event
* @property {string} value - value of the note
*/
/**
* @typedef GetNoteListDto
* @property {Array.<SingleGetNoteListDto>} notes - array of notes dto data
*/

export type SingleGetNoteListDto = {
id: number;
value: string;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* @typedef NoteInsertDto
* @property {string} value - string value of the note
*/

import * as Joi from 'joi';

export type NoteInsertDto = {
value: string;
};

export const noteInsertDtoSchema = Joi.object<NoteInsertDto>({
value: Joi.string().required(),
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* @typedef NoteUpdateDto
* @property {string} value - string value of the note
*/

import * as Joi from 'joi';

export type NoteUpdateDto = {
value: string;
};

export const noteUpdateDtoSchema = Joi.object<NoteUpdateDto>({
value: Joi.string().required(),
});
153 changes: 153 additions & 0 deletions backend/src/Application/Controller/NotesController/NotesController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { Request, Response, Router } from 'express';
import asyncHandler from 'express-async-handler';
import { Note } from '../../../Domain/User/Note/Note';
import {
deleteNoteById,
findUserNoteById,
findUserNotes,
saveNote,
} from '../../../Infrastracture/Entity/Note/NoteRepository';
import { authorization } from '../../Middleware/Auth/Authorization';
import { extractUserFromCookies } from '../../Util/Authorization';
import { IController } from '../IController';
import { SingleGetNoteListDto } from './Dto/GetNoteListDto';
import { NoteInsertDto, noteInsertDtoSchema } from './Dto/NoteInsertDto';
import { NoteUpdateDto, noteUpdateDtoSchema } from './Dto/NoteUpdateDto';

export enum NoteRoutes {
List = '/list',
}

export class NotesController implements IController {
private router: Router;

constructor() {
this.router = Router();
this.declareRoutes();
}

private declareRoutes = (): void => {
this.router.get(NoteRoutes.List, authorization, asyncHandler(this.getNotesListHandler));
this.router.patch('/:id', authorization, asyncHandler(this.updateNoteHandler));
this.router.post('/', authorization, asyncHandler(this.insertNoteHandler));
this.router.delete('/:id', authorization, asyncHandler(this.deleteNoteHandler));
this.router.get('/:id', authorization, asyncHandler(this.getSingleNoteHandler));
};

/**
* @route GET /note/:id
* @group note - Operations related to notes data
* @returns {SingleGetNoteListDto.model} 200 - Success
* @returns {EmptyResponse.model} 400 - Wrong data format
* @returns {EmptyResponse.model} 404 - Note not found
* @security cookieAuth
*/
private getSingleNoteHandler = async (req: Request, res: Response): Promise<void> => {
const id = Number(req.params.id);
if (!this.isIdValid(id)) {
res.status(400).json({});
return;
}
const user = await extractUserFromCookies(req.cookies);
const note = await findUserNoteById(id, user.id!);
if (!note) {
res.status(404).json({});
return;
}
res.status(200).json(this.mapToReturnNoteDto(note));
};

/**
* @route GET /note/list/
* @group note - Operations related to notes data
* @returns {GetNoteListDto.model} 200 - Success
* @returns {EmptyResponse.model} 400 - Wrong data format
* @returns {EmptyResponse.model} 404 - Schema or campaign not found
* @security cookieAuth
*/
private getNotesListHandler = async (req: Request, res: Response): Promise<void> => {
const user = await extractUserFromCookies(req.cookies);
const notes = await findUserNotes(user.id!);
res.status(200).json({ notes: notes.map(item => this.mapToReturnNoteDto(item)) });
};

/**
* @route DELETE /note/{id}
* @group note - Operations related to notes data
* @returns {EmptyResponse.model} 200 - Note successfully deleted
* @returns {EmptyResponse.model} 400 - Bad Request
* @returns {EmptyResponse.model} 404 - Note not found
* @security cookieAuth
*/
private deleteNoteHandler = async (req: Request, res: Response): Promise<void> => {
const id = Number(req.params.id);
if (!this.isIdValid(id)) {
res.status(400).json();
return;
}
const user = await extractUserFromCookies(req.cookies);
const note = await findUserNoteById(id, user.id!);
if (!note) {
res.status(404).json({});
return;
}
await deleteNoteById(id);
res.status(200).json({});
};

/**
* @route PATCH /note/{id}
* @group note - Operations related to notes data
* @param {NoteUpdateDto.model} data.body.required - note update data
* @returns {EmptyResponse.model} 200 - Note successfully saved
* @returns {EmptyResponse.model} 400 - Data in wrong format
* @returns {EmptyResponse.model} 404 - Note not found
* @security cookieAuth
*/
private updateNoteHandler = async (req: Request, res: Response): Promise<void> => {
const id = Number(req.params.id);
const { error, value } = noteUpdateDtoSchema.validate(req.body);
if (!this.isIdValid(id) || error) {
res.status(400).json({});
return;
}
const user = await extractUserFromCookies(req.cookies);
const note = await findUserNoteById(id, user.id!);
if (!note) {
res.status(404).json({});
return;
}
const dto = value as NoteUpdateDto;
await saveNote({ ...note, value: dto.value });
res.status(200).json({});
};

/**
* @route POST /note
* @group note - Operations related to notes data
* @param {NoteInsertDto.model} data.body.required - note insert data
* @returns {SingleGetNoteListDto.model} 200 - Note successfully saved
* @returns {EmptyResponse.model} 400 - Data in wrong format
* @security cookieAuth
*/
private insertNoteHandler = async (req: Request, res: Response): Promise<void> => {
const { error, value } = noteInsertDtoSchema.validate(req.body);
if (error) {
res.status(400).json({});
return;
}
const dto = value as NoteInsertDto;
const user = await extractUserFromCookies(req.cookies);
const saved = await saveNote({ user, value: dto.value });
res.status(200).json(this.mapToReturnNoteDto(saved));
};

private isIdValid = (id: number) => !isNaN(id) && id > 0;

private mapToReturnNoteDto = (note: Note): SingleGetNoteListDto => ({
id: note.id,
value: note.value,
});

getRouter = (): Router => this.router;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { dropDb, initDb } from '../../../../Common/Test/Database';
import { insertMockedUser } from '../../../../Common/Test/Token';
import { deleteNote, insertNote, readNoteById, readNotesList, updateNote } from './common';

describe('NotesController', () => {
beforeEach(async () => {
await initDb();
await insertMockedUser();
});
afterEach(async () => await dropDb());

it('should insert note', async () => {
const response = await insertNote({ value: 'test' });

expect(response.status).toEqual(200);
expect(response.body).toStrictEqual({ id: expect.any(Number), value: 'test' });
});

it('should read note by id', async () => {
const note = await insertNote({ value: 'test' });
const response = await readNoteById(note.body.id);

expect(response.status).toEqual(200);
expect(response.body).toStrictEqual({ id: expect.any(Number), value: 'test' });
});

it('should read notes list', async () => {
await insertNote({ value: 'test1' });
await insertNote({ value: 'test2' });
await insertNote({ value: 'test3' });
const response = await readNotesList();

expect(response.body.notes).toHaveLength(3);
expect(response.body.notes).toEqual(
expect.arrayContaining([
{ id: expect.any(Number), value: 'test1' },
{ id: expect.any(Number), value: 'test2' },
{ id: expect.any(Number), value: 'test3' },
])
);
});

it('should delete note by id', async () => {
const note = await insertNote({ value: 'test' });
const listBefore = await readNotesList();
expect(listBefore.body.notes).toHaveLength(1);

const response = await deleteNote(note.body.id);
expect(response.status).toBe(200);
const list = await readNotesList();
expect(list.body.notes).toHaveLength(0);
});

it('should update note', async () => {
const note = await insertNote({ value: 'test' });
const updated = await updateNote(note.body.id, { value: 'updated' });
expect(updated.status).toBe(200);

const response = await readNoteById(note.body.id);
expect(response.status).toBe(200);
expect(response.body).toEqual({ id: expect.any(Number), value: 'updated' });
});
});
23 changes: 23 additions & 0 deletions backend/src/Application/Controller/NotesController/Test/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { makeAuthorizedTestRequest } from '../../../../Common/Test/Request';
import { App } from '../../../App/App';
import { NoteInsertDto } from '../Dto/NoteInsertDto';
import { NoteUpdateDto } from '../Dto/NoteUpdateDto';
import { NoteRoutes, NotesController } from '../NotesController';

export const BASE_ENDPOINT = '/api/note';
export const testApp = new App([['/note', new NotesController()]]);

export const readNoteById = async (id: number) =>
await makeAuthorizedTestRequest(testApp, `${BASE_ENDPOINT}/${id}`, 'get');

export const readNotesList = async () =>
await makeAuthorizedTestRequest(testApp, `${BASE_ENDPOINT}/${NoteRoutes.List}`, 'get');

export const insertNote = async (note: NoteInsertDto) =>
await makeAuthorizedTestRequest(testApp, BASE_ENDPOINT, 'post').send(note);

export const deleteNote = async (id: number) =>
await makeAuthorizedTestRequest(testApp, `${BASE_ENDPOINT}/${id}`, 'delete');

export const updateNote = async (id: number, note: NoteUpdateDto) =>
await makeAuthorizedTestRequest(testApp, `${BASE_ENDPOINT}/${id}`, 'patch').send(note);
7 changes: 7 additions & 0 deletions backend/src/Domain/User/Note/Note.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { User } from '../User';

export type Note = {
id: number;
value: string;
user: User;
};
2 changes: 2 additions & 0 deletions backend/src/Index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { App, ControllerWithRoute } from './Application/App/App';
import { AuthController } from './Application/Controller/AuthController/AuthController';
import { CampaignController } from './Application/Controller/CampaignController/CampaignController';
import { EventController } from './Application/Controller/EventController/EventController';
import { NotesController } from './Application/Controller/NotesController/NotesController';
import { ObjectController } from './Application/Controller/ObjectController/ObjectController';
import { OpsController } from './Application/Controller/OpsController/OpsController';
import { SchemaContoller } from './Application/Controller/SchemaController/SchemaController';
Expand All @@ -18,6 +19,7 @@ const controllers: ControllerWithRoute[] = [
['/schema', new SchemaContoller()],
['/object', new ObjectController()],
['/event', new EventController()],
['/note', new NotesController()],
];

const app = new App(controllers);
Expand Down
8 changes: 8 additions & 0 deletions backend/src/Infrastracture/Entity/Note/Mapping.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Note } from '../../../Domain/User/Note/Note';
import { NoteEntity } from './NoteEntity';

export const mapEntityToDomainObject = (entity: NoteEntity): Note => ({
id: entity.id,
value: entity.value,
user: entity.user,
});
19 changes: 19 additions & 0 deletions backend/src/Infrastracture/Entity/Note/NoteEntity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
import { DbAwareColumn } from '../../../Common/Decorator/DbAwareColumn';
import { UserEntity } from '../User/UserEntity';

@Entity({ name: 'note' })
export class NoteEntity {
@PrimaryGeneratedColumn()
id!: number;

@DbAwareColumn({ length: 'max' })
value!: string;

@ManyToOne(() => UserEntity, user => user.notes, {
nullable: false,
eager: true,
onDelete: 'CASCADE',
})
user!: UserEntity;
}
28 changes: 28 additions & 0 deletions backend/src/Infrastracture/Entity/Note/NoteRepository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { getRepository } from 'typeorm';
import { Note } from '../../../Domain/User/Note/Note';
import { mapEntityToDomainObject } from './Mapping';
import { NoteEntity } from './NoteEntity';

export const findNoteById = async (id: number): Promise<Note | null> => {
const entity = await getRepository(NoteEntity).findOne({ where: { id } });
return entity ? mapEntityToDomainObject(entity) : null;
};

export const findUserNoteById = async (id: number, userId: number): Promise<Note | null> => {
const entity = await getRepository(NoteEntity).findOne({ where: { id, user: { id: userId } } });
return entity ? mapEntityToDomainObject(entity) : null;
};

export const deleteNoteById = async (id: number): Promise<void> => {
await getRepository(NoteEntity).delete({ id });
};

export const saveNote = async (note: Omit<Note, 'id'> & { id?: number }): Promise<Note> => {
const entity = await getRepository(NoteEntity).save(note);
return mapEntityToDomainObject(entity);
};

export const findUserNotes = async (userId: number): Promise<Note[]> => {
const entities = await getRepository(NoteEntity).find({ user: { id: userId } });
return entities.map(mapEntityToDomainObject);
};
4 changes: 4 additions & 0 deletions backend/src/Infrastracture/Entity/User/UserEntity.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Column, Entity, Index, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
import { DbAwareColumn } from '../../../Common/Decorator/DbAwareColumn';
import { CampaignEntity } from '../Campaign/CampaignEntity';
import { NoteEntity } from '../Note/NoteEntity';

@Entity({ name: 'user' })
export class UserEntity {
Expand All @@ -23,4 +24,7 @@ export class UserEntity {

@OneToMany(() => CampaignEntity, campaign => campaign.user, { onDelete: 'CASCADE' })
campaigns!: CampaignEntity[];

@OneToMany(() => NoteEntity, note => note.user, { onDelete: 'CASCADE' })
notes!: NoteEntity[];
}
Loading