From 174eef83cd44fe6bfba6eaaf2891638c6bb9a618 Mon Sep 17 00:00:00 2001 From: Peter Savchenko Date: Mon, 4 Sep 2023 00:38:14 +0300 Subject: [PATCH 1/3] PATCH /note route added --- .eslintrc | 3 +- src/domain/entities/note.ts | 14 +++++- src/domain/service/note.ts | 18 ++++++++ .../http/middlewares/authRequired.ts | 5 +-- src/presentation/http/middlewares/withUser.ts | 9 ++-- src/presentation/http/router/note.ts | 44 +++++++++++++++++++ src/repository/note.repository.ts | 11 +++++ .../storage/postgres/orm/sequelize/note.ts | 36 +++++++++++++++ 8 files changed, 128 insertions(+), 12 deletions(-) diff --git a/.eslintrc b/.eslintrc index c937e8c3..bd37a813 100644 --- a/.eslintrc +++ b/.eslintrc @@ -14,6 +14,7 @@ "match": false } } - ] + ], + "jsdoc/require-returns-type": "off" } } diff --git a/src/domain/entities/note.ts b/src/domain/entities/note.ts index c00d3e35..a9a1f79b 100644 --- a/src/domain/entities/note.ts +++ b/src/domain/entities/note.ts @@ -31,10 +31,20 @@ export interface Note { * Note creator */ creatorId: number; + + /** + * When note was created + */ + createdAt: string; + + /** + * Last time note was updated + */ + updatedAt: string; } /** - * Notes creation attributes, omitting id, because it's generated by database + * Part of note entity used to create new note */ -export type NoteCreationAttributes = Omit; +export type NoteCreationAttributes = Pick; diff --git a/src/domain/service/note.ts b/src/domain/service/note.ts index b36355c5..6863b435 100644 --- a/src/domain/service/note.ts +++ b/src/domain/service/note.ts @@ -36,6 +36,24 @@ export default class NoteService { }); } + /** + * Updates note + * + * @param id - note id + * @param content - new content + * @returns updated note + * @throws { Error } if note was not updated + */ + public async updateNoteContentByPublicId(id: NotePublicId, content: Note['content']): Promise { + const updatedNote = await this.repository.updateNoteContentByPublicId(id, content); + + if (updatedNote === null) { + throw new Error(`Note with id ${id} was not updated`); + } + + return updatedNote; + } + /** * Gets note by id * diff --git a/src/presentation/http/middlewares/authRequired.ts b/src/presentation/http/middlewares/authRequired.ts index e6636c59..5c327656 100644 --- a/src/presentation/http/middlewares/authRequired.ts +++ b/src/presentation/http/middlewares/authRequired.ts @@ -15,9 +15,8 @@ export default (authService: AuthService): preHandlerHookHandler => { * * @param request - request object * @param reply - reply object - * @param done - done callback */ - return async (request, reply, done) => { + return async (request, reply) => { const authorizationHeader = request.headers.authorization; /** @@ -39,7 +38,7 @@ export default (authService: AuthService): preHandlerHookHandler => { try { await authService.verifyAccessToken(token); - done(); + return reply; } catch (error) { return reply .code(StatusCodes.UNAUTHORIZED) diff --git a/src/presentation/http/middlewares/withUser.ts b/src/presentation/http/middlewares/withUser.ts index 0b5f4c4b..9e5b6ce6 100644 --- a/src/presentation/http/middlewares/withUser.ts +++ b/src/presentation/http/middlewares/withUser.ts @@ -14,18 +14,15 @@ export default (authService: AuthService): preHandlerHookHandler => { * * @param request - request object * @param reply - reply object - * @param done - done callback */ - return async (request, reply, done) => { + return async (request, reply) => { const authorizationHeader = request.headers.authorization; /** * If authorization header is not present, return unauthorized response */ if (!notEmpty(authorizationHeader)) { - done(); - - return; + return reply; } /** @@ -43,7 +40,7 @@ export default (authService: AuthService): preHandlerHookHandler => { auth: tokenPayload, }; } finally { - done(); + return reply; } }; }; diff --git a/src/presentation/http/router/note.ts b/src/presentation/http/router/note.ts index 6f078228..561010a8 100644 --- a/src/presentation/http/router/note.ts +++ b/src/presentation/http/router/note.ts @@ -27,6 +27,22 @@ interface AddNoteOptions { content: JSON; } +/** + * Payload for update note request + */ +interface UpdateNoteOptions { + /** + * Note public id + */ + id: NotePublicId; + + /** + * New content + */ + content: JSON; +} + + /** * Interface for the note router. */ @@ -185,6 +201,34 @@ const NoteRouter: FastifyPluginCallback = (fastify, opts, don }); }); + /** + * Updates note by id. + */ + fastify.patch<{ + Body: UpdateNoteOptions, + Reply: { + updatedAt: Note['updatedAt'], + } + }>('/', { + preHandler: [ + opts.middlewares.authRequired, + opts.middlewares.withUser, + ], + }, async (request, reply) => { + /** + * @todo Validate request params + * @todo Check user access right + */ + const { id, content } = request.body; + + const note = await noteService.updateNoteContentByPublicId(id, content); + + return reply.send({ + updatedAt: note.updatedAt, + }); + }); + + /** * Get note by custom hostname */ diff --git a/src/repository/note.repository.ts b/src/repository/note.repository.ts index bfc04832..7ff01ee1 100644 --- a/src/repository/note.repository.ts +++ b/src/repository/note.repository.ts @@ -31,6 +31,17 @@ export default class NoteRepository { return await this.storage.createNote(options); } + /** + * Update note content in a store using note public id + * + * @param publicId - note public id + * @param content - new content + * @returns Note on success, null on failure + */ + public async updateNoteContentByPublicId(publicId: NotePublicId, content: Note['content'] ): Promise { + return await this.storage.updateNoteContentByPublicId(publicId, content); + } + /** * Gets note by id * diff --git a/src/repository/storage/postgres/orm/sequelize/note.ts b/src/repository/storage/postgres/orm/sequelize/note.ts index f52380a3..730dc4c7 100644 --- a/src/repository/storage/postgres/orm/sequelize/note.ts +++ b/src/repository/storage/postgres/orm/sequelize/note.ts @@ -33,6 +33,16 @@ export class NoteModel extends Model, InferCreationAt * Note creator, user identifier, who created this note */ public declare creatorId: Note['creatorId']; + + /** + * Time when note was created + */ + public declare createdAt: CreationOptional; + + /** + * Last time when note was updated + */ + public declare updatedAt: CreationOptional; } @@ -95,6 +105,8 @@ export default class NoteSequelizeStorage { key: 'id', }, }, + createdAt: DataTypes.DATE, + updatedAt: DataTypes.DATE, }, { tableName: this.tableName, sequelize: this.database, @@ -154,6 +166,30 @@ export default class NoteSequelizeStorage { return createdNote; } + /** + * Update note content by public id + * + * @param publicId - note public id + * @param content - new content + * @returns Note on success, null on failure + */ + public async updateNoteContentByPublicId(publicId: NotePublicId, content: Note['content']): Promise { + const [affectedRowsCount, affectedRows] = await this.model.update({ + content, + }, { + where: { + publicId, + }, + returning: true, + }); + + if (affectedRowsCount !== 1) { + return null; + } + + return affectedRows[0]; + } + /** * Gets note by id * From ea7303dcb7337227f8b518a0da2853aba3a58727 Mon Sep 17 00:00:00 2001 From: Peter Savchenko Date: Mon, 4 Sep 2023 00:47:48 +0300 Subject: [PATCH 2/3] fix middlewares --- src/presentation/http/middlewares/authRequired.ts | 5 +++-- src/presentation/http/middlewares/withUser.ts | 7 ++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/presentation/http/middlewares/authRequired.ts b/src/presentation/http/middlewares/authRequired.ts index 5c327656..e6636c59 100644 --- a/src/presentation/http/middlewares/authRequired.ts +++ b/src/presentation/http/middlewares/authRequired.ts @@ -15,8 +15,9 @@ export default (authService: AuthService): preHandlerHookHandler => { * * @param request - request object * @param reply - reply object + * @param done - done callback */ - return async (request, reply) => { + return async (request, reply, done) => { const authorizationHeader = request.headers.authorization; /** @@ -38,7 +39,7 @@ export default (authService: AuthService): preHandlerHookHandler => { try { await authService.verifyAccessToken(token); - return reply; + done(); } catch (error) { return reply .code(StatusCodes.UNAUTHORIZED) diff --git a/src/presentation/http/middlewares/withUser.ts b/src/presentation/http/middlewares/withUser.ts index 9e5b6ce6..db438706 100644 --- a/src/presentation/http/middlewares/withUser.ts +++ b/src/presentation/http/middlewares/withUser.ts @@ -14,14 +14,17 @@ export default (authService: AuthService): preHandlerHookHandler => { * * @param request - request object * @param reply - reply object + * @param done - done callback */ - return async (request, reply) => { + return async (request, reply, done) => { const authorizationHeader = request.headers.authorization; /** * If authorization header is not present, return unauthorized response */ if (!notEmpty(authorizationHeader)) { + done(); + return reply; } @@ -40,6 +43,8 @@ export default (authService: AuthService): preHandlerHookHandler => { auth: tokenPayload, }; } finally { + done(); + return reply; } }; From 464f14111afc3f375ea2e3711a34e84d1bef7736 Mon Sep 17 00:00:00 2001 From: Peter Savchenko Date: Mon, 4 Sep 2023 01:03:25 +0300 Subject: [PATCH 3/3] fix: Reply was already sent --- src/presentation/http/middlewares/authRequired.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/presentation/http/middlewares/authRequired.ts b/src/presentation/http/middlewares/authRequired.ts index e6636c59..d0410020 100644 --- a/src/presentation/http/middlewares/authRequired.ts +++ b/src/presentation/http/middlewares/authRequired.ts @@ -40,6 +40,8 @@ export default (authService: AuthService): preHandlerHookHandler => { await authService.verifyAccessToken(token); done(); + + return reply; } catch (error) { return reply .code(StatusCodes.UNAUTHORIZED)