From 1c0d84e2da79db05c9e7a1608f157a20d1f14dfd Mon Sep 17 00:00:00 2001 From: Dobrunia Kostrigin <48620984+Dobrunia@users.noreply.github.com> Date: Wed, 19 Nov 2025 21:23:42 +0300 Subject: [PATCH 1/2] feat: add endpoint to retrieve notes created by user, ordered by creation time --- src/domain/service/note.ts | 14 +++++ src/presentation/http/router/noteList.ts | 54 +++++++++++++++++++ src/repository/note.repository.ts | 10 ++++ .../storage/postgres/orm/sequelize/note.ts | 49 +++++++++++++++++ 4 files changed, 127 insertions(+) diff --git a/src/domain/service/note.ts b/src/domain/service/note.ts index 0065dd6f..c74d2ca5 100644 --- a/src/domain/service/note.ts +++ b/src/domain/service/note.ts @@ -238,6 +238,20 @@ export default class NoteService { }; } + /** + * Returns notes created by user, ordered by creation time + * @param userId - id of the user + * @param page - number of current page + * @returns list of the notes ordered by creation time + */ + public async getNotesByCreatorId(userId: User['id'], page: number): Promise { + const offset = (page - 1) * this.noteListPortionSize; + + return { + items: await this.noteRepository.getNotesByCreatorId(userId, 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 fd227028..beb23fbc 100644 --- a/src/presentation/http/router/noteList.ts +++ b/src/presentation/http/router/noteList.ts @@ -77,6 +77,60 @@ const NoteListRouter: FastifyPluginCallback = (fastify, o return reply.send(noteListPublic); }); + /** + * Get note list created by user, ordered by creation time + */ + fastify.get<{ + Querystring: { + page: number; + }; + }>('/created', { + config: { + policy: [ + '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.getNotesByCreatorId(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 680728b3..c78d826f 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.getRecentNotesByUserId(id, offset, limit); } + /** + * Gets notes created by user, ordered by creation time + * @param creatorId - note creator id + * @param offset - number of skipped notes + * @param limit - number of notes to get + */ + public async getNotesByCreatorId(creatorId: number, offset: number, limit: number): Promise { + return await this.storage.getNotesByCreatorId(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 c8fd0935..48138345 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 notes created by user, ordered by creation time + * @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 creation time + */ + public async getNotesByCreatorId(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: [ + ['createdAt', '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 From 4429712ce6a7f3f266d779881154cf3b7c3d5a2f Mon Sep 17 00:00:00 2001 From: Dobrunia Kostrigin <48620984+Dobrunia@users.noreply.github.com> Date: Thu, 20 Nov 2025 03:19:23 +0300 Subject: [PATCH 2/2] test: add unit tests for GET /notes/created endpoint to validate note retrieval based on user authorization and pagination --- src/presentation/http/router/noteList.test.ts | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) diff --git a/src/presentation/http/router/noteList.test.ts b/src/presentation/http/router/noteList.test.ts index 83de4587..1eb61ad1 100644 --- a/src/presentation/http/router/noteList.test.ts +++ b/src/presentation/http/router/noteList.test.ts @@ -144,3 +144,133 @@ describe('GET /notes?page', () => { } }); }); + +describe('GET /notes/created?page', () => { + test.each([ + /** + * Returns noteList with specified length (not for last page) + * User is authorized, notes are created by user + */ + { + isAuthorized: true, + expectedStatusCode: 200, + notesCreated: true, + expectedMessage: null, + expectedLength: 30, + pageNumber: 1, + }, + /** + * Returns noteList with specified length (for last page) + * User is authorized, notes are created by user + */ + { + isAuthorized: true, + expectedStatusCode: 200, + notesCreated: true, + expectedMessage: null, + expectedLength: 19, + pageNumber: 2, + }, + /** + * Returns noteList with no items if there are no notes for certain page + * User is authorized, notes are created by user + */ + { + isAuthorized: true, + expectedStatusCode: 200, + notesCreated: true, + expectedMessage: null, + expectedLength: 0, + pageNumber: 3, + }, + /** + * Returns 'querystring/page must be >= 1' message when page < 0 + */ + { + isAuthorized: true, + expectedStatusCode: 400, + notesCreated: true, + expectedMessage: 'querystring/page must be >= 1', + expectedLength: 0, + pageNumber: -1, + }, + /** + * Returns 'querystring/page must be <= 30' message when page is too large (maximum page numbrer is 30 by default) + */ + { + isAuthorized: true, + expectedStatusCode: 400, + notesCreated: true, + expectedMessage: 'querystring/page must be <= 30', + expectedLength: 0, + pageNumber: 31, + }, + /** + * Returns 'unauthorized' message when user is not authorized + */ + { + isAuthorized: false, + expectedStatusCode: 401, + notesCreated: true, + expectedMessage: 'You must be authenticated to access this resource', + expectedLength: 0, + pageNumber: 1, + }, + /** + * Returns noteList with no items if user did not create any notes + * User is authorized, notes are not created by user + */ + { + isAuthorized: true, + expectedStatusCode: 200, + notesCreated: false, + expectedMessage: null, + expectedLength: 0, + pageNumber: 1, + }, + ])('Get note list created by user', async ({ isAuthorized, expectedStatusCode, notesCreated, expectedMessage, expectedLength, pageNumber }) => { + const portionSize = 49; + let accessToken; + + /** Insert creator and randomGuy */ + const creator = await global.db.insertUser(); + + const randomGuy = await global.db.insertUser(); + + if (isAuthorized) { + accessToken = global.auth(randomGuy.id); + } + + for (let i = 0; i < portionSize; i++) { + const note = await global.db.insertNote({ + creatorId: notesCreated ? randomGuy.id : creator.id, + }); + + await global.db.insertNoteSetting({ + noteId: note.id, + cover: 'DZnvqi63.png', + isPublic: false, + }); + } + + const response = await global.api?.fakeRequest({ + method: 'GET', + headers: { + authorization: `Bearer ${accessToken}`, + }, + url: `/notes/created?page=${pageNumber}`, + }); + + const body = response?.json(); + + if (expectedMessage !== null) { + expect(response?.statusCode).toBe(expectedStatusCode); + + expect(body.message).toBe(expectedMessage); + } else { + expect(response?.statusCode).toBe(expectedStatusCode); + + expect(body.items).toHaveLength(expectedLength); + } + }); +});