From f348312839ac154848facecad7cdafaebc31ebd0 Mon Sep 17 00:00:00 2001 From: Dobrunia Kostrigin <48620984+Dobrunia@users.noreply.github.com> Date: Fri, 7 Nov 2025 20:56:26 +0300 Subject: [PATCH 1/2] feat: add my notes endpoint --- src/domain/service/note.ts | 18 ++++++ src/presentation/http/router/noteList.ts | 57 ++++++++++++++++++- src/repository/note.repository.ts | 10 ++++ .../storage/postgres/orm/sequelize/note.ts | 55 +++++++++++++++++- 4 files changed, 136 insertions(+), 4 deletions(-) diff --git a/src/domain/service/note.ts b/src/domain/service/note.ts index c4ae5b52..d6b5591c 100644 --- a/src/domain/service/note.ts +++ b/src/domain/service/note.ts @@ -238,6 +238,24 @@ export default class NoteService { }; } + /** + * Returns note list created by user + * @param creatorId - id of the note creator + * @param page - number of current page + * @returns list of the notes ordered by updatedAt DESC + */ + public async getMyNoteList(creatorId: User['id'], page: number): Promise { + const offset = (page - 1) * this.noteListPortionSize; + + return { + items: await this.noteRepository.getMyNoteList( + creatorId, + offset, + this.noteListPortionSize + ), + }; + } + /** * Create note relation * @param noteId - id of the current note diff --git a/src/presentation/http/router/noteList.ts b/src/presentation/http/router/noteList.ts index 49507d2f..d91d49b4 100644 --- a/src/presentation/http/router/noteList.ts +++ b/src/presentation/http/router/noteList.ts @@ -11,7 +11,6 @@ interface NoteListRouterOptions { * Note service instance */ noteService: NoteService; - } /** @@ -77,6 +76,62 @@ const NoteListRouter: FastifyPluginCallback = (fastify, o return reply.send(noteListPublic); }); + /** + * Get note list created by the user + */ + fastify.get<{ + Querystring: { + page: number; + }; + }>( + '/my', + { + config: { + policy: ['authRequired'], + }, + schema: { + querystring: { + page: { + type: 'number', + minimum: 1, + maximum: 30, + }, + }, + + response: { + '2xx': { + description: 'Query notelist created by user', + properties: { + items: { + id: { type: 'string' }, + content: { type: 'string' }, + createdAt: { type: 'string' }, + creatorId: { type: 'string' }, + updatedAt: { type: 'string' }, + }, + }, + }, + }, + }, + }, + async (request, reply) => { + const userId = request.userId as number; + const page = request.query.page; + + const noteList = await noteService.getMyNoteList(userId, page); + /** + * Wrapping Notelist for public use + */ + const noteListItemsPublic: NotePublic[] = noteList.items.map(definePublicNote); + + const noteListPublic: NoteListPublic = { + items: noteListItemsPublic, + }; + + return reply.send(noteListPublic); + } + ); + done(); }; diff --git a/src/repository/note.repository.ts b/src/repository/note.repository.ts index 234c0741..daad0746 100644 --- a/src/repository/note.repository.ts +++ b/src/repository/note.repository.ts @@ -82,6 +82,16 @@ export default class NoteRepository { return await this.storage.getNoteListByUserId(id, offset, limit); } + /** + * Gets note list created by user + * @param creatorId - id of note creator + * @param offset - number of skipped notes + * @param limit - number of notes to get + */ + public async getMyNoteList(creatorId: number, offset: number, limit: number): Promise { + return await this.storage.getMyNoteList(creatorId, offset, limit); + } + /** * Get all notes based on their ids * @param noteIds : list of note ids diff --git a/src/repository/storage/postgres/orm/sequelize/note.ts b/src/repository/storage/postgres/orm/sequelize/note.ts index 1b9ba87c..ef99a856 100644 --- a/src/repository/storage/postgres/orm/sequelize/note.ts +++ b/src/repository/storage/postgres/orm/sequelize/note.ts @@ -285,6 +285,55 @@ export default class NoteSequelizeStorage { }); } + /** + * Gets note list created by user + * @param creatorId - id of note creator + * @param offset - number of skipped notes + * @param limit - number of notes to get + * @returns list of the notes ordered by updatedAt DESC + */ + public async getMyNoteList(creatorId: number, offset: number, limit: number): Promise { + if (!this.settingsModel) { + throw new Error('NoteStorage: Note settings model not initialized'); + } + + const reply = await this.model.findAll({ + offset: offset, + limit: limit, + where: { + creatorId: creatorId, + }, + order: [['updatedAt', 'DESC']], + include: [ + { + model: this.settingsModel, + as: 'noteSettings', + attributes: ['cover'], + duplicating: false, + }, + ], + }); + + /** + * Convert note model data to Note entity with cover property + */ + return reply.map((note) => { + return { + id: note.id, + /** + * noteSettings is required to be, because we make join + */ + cover: note.noteSettings!.cover, + content: note.content, + updatedAt: note.updatedAt, + createdAt: note.createdAt, + publicId: note.publicId, + creatorId: note.creatorId, + tools: note.tools, + }; + }); + } + /** * Gets note by id * @param hostname - custom hostname @@ -356,7 +405,7 @@ export default class NoteSequelizeStorage { // Fetch all notes and relations in a recursive query const query = ` WITH RECURSIVE note_tree AS ( - SELECT + SELECT n.id AS "noteId", n.content, n.public_id AS "publicId", @@ -364,10 +413,10 @@ export default class NoteSequelizeStorage { FROM ${String(this.database.literal(this.tableName).val)} n LEFT JOIN ${String(this.database.literal('note_relations').val)} nr ON n.id = nr.note_id WHERE n.id = :startNoteId - + UNION ALL - SELECT + SELECT n.id AS "noteId", n.content, n.public_id AS "publicId", From 9ddb31916986e2caba49d4c5dd00f3c277ce552f Mon Sep 17 00:00:00 2001 From: Dobrunia Kostrigin <48620984+Dobrunia@users.noreply.github.com> Date: Sun, 9 Nov 2025 04:41:24 +0300 Subject: [PATCH 2/2] refactor: reuse existing code --- src/domain/service/note.ts | 27 +--- src/presentation/http/router/noteList.ts | 137 +++++++----------- src/repository/note.repository.ts | 19 +-- .../storage/postgres/orm/sequelize/note.ts | 108 +++++--------- 4 files changed, 97 insertions(+), 194 deletions(-) diff --git a/src/domain/service/note.ts b/src/domain/service/note.ts index d6b5591c..07217462 100644 --- a/src/domain/service/note.ts +++ b/src/domain/service/note.ts @@ -225,34 +225,17 @@ export default class NoteService { } /** - * Returns note list by creator id + * Returns note list by user id * @param userId - id of the user * @param page - number of current page - * @returns list of the notes ordered by time of last visit + * @param filterByCreator - if true, returns only notes created by user, otherwise returns notes visited by user + * @returns list of the notes */ - public async getNoteListByUserId(userId: User['id'], page: number): Promise { + public async getNoteListByUserId(userId: User['id'], page: number, filterByCreator = false): Promise { const offset = (page - 1) * this.noteListPortionSize; return { - items: await this.noteRepository.getNoteListByUserId(userId, offset, this.noteListPortionSize), - }; - } - - /** - * Returns note list created by user - * @param creatorId - id of the note creator - * @param page - number of current page - * @returns list of the notes ordered by updatedAt DESC - */ - public async getMyNoteList(creatorId: User['id'], page: number): Promise { - const offset = (page - 1) * this.noteListPortionSize; - - return { - items: await this.noteRepository.getMyNoteList( - creatorId, - offset, - this.noteListPortionSize - ), + items: await this.noteRepository.getNoteListByUserId(userId, offset, this.noteListPortionSize, filterByCreator), }; } diff --git a/src/presentation/http/router/noteList.ts b/src/presentation/http/router/noteList.ts index d91d49b4..2b21df3d 100644 --- a/src/presentation/http/router/noteList.ts +++ b/src/presentation/http/router/noteList.ts @@ -22,6 +22,49 @@ interface NoteListRouterOptions { const NoteListRouter: FastifyPluginCallback = (fastify, opts, done) => { const noteService = opts.noteService; + const noteListSchema = { + querystring: { + page: { + type: 'number', + minimum: 1, + maximum: 30, + }, + }, + response: { + '2xx': { + description: 'Query notelist', + properties: { + items: { + id: { type: 'string' }, + content: { type: 'string' }, + createdAt: { type: 'string' }, + creatorId: { type: 'string' }, + updatedAt: { type: 'string' }, + }, + }, + }, + }, + }; + + const createNoteListHandler = (filterByCreator: boolean) => { + return async (request: { userId: number | null; query: { page: number } }, reply: { send: (data: NoteListPublic) => void }): Promise => { + const userId = request.userId as number; + const page = request.query.page; + + const noteList = await noteService.getNoteListByUserId(userId, page, filterByCreator); + /** + * Wrapping Notelist for public use + */ + const noteListItemsPublic: NotePublic[] = noteList.items.map(definePublicNote); + + const noteListPublic: NoteListPublic = { + items: noteListItemsPublic, + }; + + return reply.send(noteListPublic); + }; + }; + /** * Get note list ordered by time of last visit */ @@ -35,46 +78,8 @@ const NoteListRouter: FastifyPluginCallback = (fastify, o 'authRequired', ], }, - schema: { - querystring: { - page: { - type: 'number', - minimum: 1, - maximum: 30, - }, - }, - - response: { - '2xx': { - description: 'Query notelist', - properties: { - items: { - id: { type: 'string' }, - content: { type: 'string' }, - createdAt: { type: 'string' }, - creatorId: { type: 'string' }, - updatedAt: { type: 'string' }, - }, - }, - }, - }, - }, - }, async (request, reply) => { - const userId = request.userId as number; - const page = request.query.page; - - const noteList = await noteService.getNoteListByUserId(userId, page); - /** - * Wrapping Notelist for public use - */ - const noteListItemsPublic: NotePublic[] = noteList.items.map(definePublicNote); - - const noteListPublic: NoteListPublic = { - items: noteListItemsPublic, - }; - - return reply.send(noteListPublic); - }); + schema: noteListSchema, + }, createNoteListHandler(false)); /** * Get note list created by the user @@ -83,54 +88,12 @@ const NoteListRouter: FastifyPluginCallback = (fastify, o Querystring: { page: number; }; - }>( - '/my', - { - config: { - policy: ['authRequired'], - }, - schema: { - querystring: { - page: { - type: 'number', - minimum: 1, - maximum: 30, - }, - }, - - response: { - '2xx': { - description: 'Query notelist created by user', - properties: { - items: { - id: { type: 'string' }, - content: { type: 'string' }, - createdAt: { type: 'string' }, - creatorId: { type: 'string' }, - updatedAt: { type: 'string' }, - }, - }, - }, - }, - }, + }>('/my', { + config: { + policy: ['authRequired'], }, - async (request, reply) => { - const userId = request.userId as number; - const page = request.query.page; - - const noteList = await noteService.getMyNoteList(userId, page); - /** - * Wrapping Notelist for public use - */ - const noteListItemsPublic: NotePublic[] = noteList.items.map(definePublicNote); - - const noteListPublic: NoteListPublic = { - items: noteListItemsPublic, - }; - - return reply.send(noteListPublic); - } - ); + schema: noteListSchema, + }, createNoteListHandler(true)); done(); }; diff --git a/src/repository/note.repository.ts b/src/repository/note.repository.ts index daad0746..6ef7b78b 100644 --- a/src/repository/note.repository.ts +++ b/src/repository/note.repository.ts @@ -73,23 +73,14 @@ export default class NoteRepository { } /** - * Gets note list by creator id - * @param id - note creator id + * Gets note list by user id + * @param id - user id * @param offset - number of skipped notes * @param limit - number of notes to get + * @param filterByCreator - if true, returns only notes created by user, otherwise returns notes visited by user */ - public async getNoteListByUserId(id: number, offset: number, limit: number): Promise { - return await this.storage.getNoteListByUserId(id, offset, limit); - } - - /** - * Gets note list created by user - * @param creatorId - id of note creator - * @param offset - number of skipped notes - * @param limit - number of notes to get - */ - public async getMyNoteList(creatorId: number, offset: number, limit: number): Promise { - return await this.storage.getMyNoteList(creatorId, offset, limit); + public async getNoteListByUserId(id: number, offset: number, limit: number, filterByCreator = false): Promise { + return await this.storage.getNoteListByUserId(id, offset, limit, filterByCreator); } /** diff --git a/src/repository/storage/postgres/orm/sequelize/note.ts b/src/repository/storage/postgres/orm/sequelize/note.ts index ef99a856..b8b7c619 100644 --- a/src/repository/storage/postgres/orm/sequelize/note.ts +++ b/src/repository/storage/postgres/orm/sequelize/note.ts @@ -224,94 +224,60 @@ export default class NoteSequelizeStorage { } /** - * Gets note list by creator id + * Gets note list by user id * @param userId - id of certain user * @param offset - number of skipped notes * @param limit - number of notes to get + * @param filterByCreator - if true, returns only notes created by user, otherwise returns notes visited by user * @returns list of the notes */ - public async getNoteListByUserId(userId: number, offset: number, limit: number): Promise { - if (this.visitsModel === null) { - throw new Error('NoteStorage: NoteVisit model should be defined'); - } - + public async getNoteListByUserId(userId: number, offset: number, limit: number, filterByCreator = false): Promise { if (!this.settingsModel) { throw new Error('NoteStorage: Note settings model not initialized'); } - const reply = await this.model.findAll({ - offset: offset, - limit: limit, - where: { - '$noteVisits.user_id$': userId, - }, - order: [[ - { - model: this.visitsModel, - as: 'noteVisits', - }, - 'visited_at', - 'DESC', - ]], - include: [{ - model: this.visitsModel, - as: 'noteVisits', - duplicating: false, - }, { + if (!filterByCreator && this.visitsModel === null) { + throw new Error('NoteStorage: NoteVisit model should be defined'); + } + + const where: Record = filterByCreator + ? { creatorId: userId } + : { '$noteVisits.user_id$': userId }; + + const order = filterByCreator + ? [['updatedAt', 'DESC']] + : [[ + { + model: this.visitsModel!, + as: 'noteVisits', + }, + 'visited_at', + 'DESC', + ]]; + + const include: Array> = [ + { model: this.settingsModel, as: 'noteSettings', attributes: ['cover'], duplicating: false, - }], - }); - - /** - * Convert note model data to Note entity with cover property - */ - return reply.map((note) => { - return { - id: note.id, - /** - * noteSettings is required to be, because we make join - */ - cover: note.noteSettings!.cover, - content: note.content, - updatedAt: note.updatedAt, - createdAt: note.createdAt, - publicId: note.publicId, - creatorId: note.creatorId, - tools: note.tools, - }; - }); - } + }, + ]; - /** - * Gets note list created by user - * @param creatorId - id of note creator - * @param offset - number of skipped notes - * @param limit - number of notes to get - * @returns list of the notes ordered by updatedAt DESC - */ - public async getMyNoteList(creatorId: number, offset: number, limit: number): Promise { - if (!this.settingsModel) { - throw new Error('NoteStorage: Note settings model not initialized'); + if (!filterByCreator) { + include.unshift({ + model: this.visitsModel!, + as: 'noteVisits', + duplicating: false, + }); } const reply = await this.model.findAll({ - offset: offset, - limit: limit, - where: { - creatorId: creatorId, - }, - order: [['updatedAt', 'DESC']], - include: [ - { - model: this.settingsModel, - as: 'noteSettings', - attributes: ['cover'], - duplicating: false, - }, - ], + offset, + limit, + where, + order: order as never, + include, }); /**