From 2ad6cd645c2bbe6d1b810afcc160031f39da0d4c Mon Sep 17 00:00:00 2001 From: Tushar Ahire Date: Fri, 1 Nov 2024 15:42:45 +0530 Subject: [PATCH 1/5] adds endpoint and functionality to list notes related parent note --- src/domain/service/note.ts | 13 +++ src/presentation/http/http-api.ts | 1 + .../http/middlewares/note/useNoteResolver.ts | 8 +- src/presentation/http/router/noteList.ts | 94 +++++++++++++++++++ src/repository/index.ts | 1 + src/repository/note.repository.ts | 10 ++ .../storage/postgres/orm/sequelize/note.ts | 64 +++++++++++++ 7 files changed, 189 insertions(+), 2 deletions(-) diff --git a/src/domain/service/note.ts b/src/domain/service/note.ts index 95976baf..cb92e483 100644 --- a/src/domain/service/note.ts +++ b/src/domain/service/note.ts @@ -236,6 +236,19 @@ export default class NoteService { items: await this.noteRepository.getNoteListByUserId(userId, offset, this.noteListPortionSize), }; } + + /** + * Returns note list by parent note id + * @param parentId - id of the parent note + * @param page - number of current page + * @returns list of the notes ordered by time of last visit + */ + public async getNoteListByParentNote(parentNoteId: NoteInternalId, page: number): Promise { + const offset = (page - 1) * this.noteListPortionSize; + return { + items: await this.noteRepository.getNoteListByParentNote(parentNoteId, offset, this.noteListPortionSize), + }; + } /** * Create note relation diff --git a/src/presentation/http/http-api.ts b/src/presentation/http/http-api.ts index 31c1046a..6376db8c 100644 --- a/src/presentation/http/http-api.ts +++ b/src/presentation/http/http-api.ts @@ -207,6 +207,7 @@ export default class HttpApi implements Api { await this.server?.register(NoteListRouter, { prefix: '/notes', noteService: domainServices.noteService, + noteSettingsService: domainServices.noteSettingsService }); await this.server?.register(JoinRouter, { diff --git a/src/presentation/http/middlewares/note/useNoteResolver.ts b/src/presentation/http/middlewares/note/useNoteResolver.ts index 73ae1da3..6ae3e200 100644 --- a/src/presentation/http/middlewares/note/useNoteResolver.ts +++ b/src/presentation/http/middlewares/note/useNoteResolver.ts @@ -4,7 +4,7 @@ import { notEmpty } from '@infrastructure/utils/empty.js'; import { StatusCodes } from 'http-status-codes'; import hasProperty from '@infrastructure/utils/hasProperty.js'; import { getLogger } from '@infrastructure/logging/index.js'; -import type { Note, NotePublicId } from '@domain/entities/note.js'; +import type { Note, NotePublicId, NoteInternalId } from '@domain/entities/note.js'; /** * Add middleware for resolve Note by public id and add it to request @@ -36,8 +36,12 @@ export default function useNoteResolver(noteService: NoteService): { return await noteService.getNoteByPublicId(publicId); } + else if (hasProperty(requestData, 'parentNoteId') && notEmpty(requestData.parentNoteId)) { + const noteId = requestData.parentNoteId as NoteInternalId; + return await noteService.getNoteById(noteId) + } } - + return { noteResolver: async function noteIdResolver(request, reply) { let note: Note | undefined; diff --git a/src/presentation/http/router/noteList.ts b/src/presentation/http/router/noteList.ts index 49507d2f..b12a5b13 100644 --- a/src/presentation/http/router/noteList.ts +++ b/src/presentation/http/router/noteList.ts @@ -1,7 +1,12 @@ import type { FastifyPluginCallback } from 'fastify'; import type NoteService from '@domain/service/note.js'; +import useNoteResolver from '../middlewares/note/useNoteResolver.js'; +import useNoteSettingsResolver from '../middlewares/noteSettings/useNoteSettingsResolver.js'; +import useMemberRoleResolver from '../middlewares/noteSettings/useMemberRoleResolver.js'; +import type NoteSettingsService from '@domain/service/noteSettings.js'; import { definePublicNote, type NotePublic } from '@domain/entities/notePublic.js'; import type { NoteListPublic } from '@domain/entities/noteList.js'; +import { NoteInternalId } from '@domain/entities/note.js'; /** * Interface for the noteList router. @@ -12,6 +17,12 @@ interface NoteListRouterOptions { */ noteService: NoteService; + /** + * Note Settings service instance + */ + noteSettingsService: NoteSettingsService; + + } /** @@ -22,6 +33,25 @@ interface NoteListRouterOptions { */ const NoteListRouter: FastifyPluginCallback = (fastify, opts, done) => { const noteService = opts.noteService; + const noteSettingsService = opts.noteSettingsService; + + /** + * Prepare note id resolver middleware + * It should be used in routes that accepts note public id + */ + const { noteResolver } = useNoteResolver(noteService); + + /** + * Prepare note settings resolver middleware + * It should be used to use note settings in middlewares + */ + const { noteSettingsResolver } = useNoteSettingsResolver(noteSettingsService); + + /** + * Prepare user role resolver middleware + * It should be used to use user role in middlewares + */ + const { memberRoleResolver } = useMemberRoleResolver(noteSettingsService); /** * Get note list ordered by time of last visit @@ -77,6 +107,70 @@ const NoteListRouter: FastifyPluginCallback = (fastify, o return reply.send(noteListPublic); }); + /** + * Get note list by parent note + */ + fastify.get<{ + Params: { + parentNoteId: NoteInternalId; + } + Querystring: { + page: number; + }; + }>('/:parentNoteId', { + config: { + policy: [ + 'notePublicOrUserInTeam', + ], + }, + schema: { + params: { + notePublicId: { + $ref: 'NoteSchema#/properties/id', + }, + }, 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' }, + }, + }, + }, + }, + }, preHandler: [ + noteResolver, + noteSettingsResolver, + memberRoleResolver, + ], + }, async (request, reply) => { + const { parentNoteId } = await request.params; + const { page } = await request.query; + + const noteList = await noteService.getNoteListByParentNote(parentNoteId, 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/index.ts b/src/repository/index.ts index ed40d01e..35ff6811 100644 --- a/src/repository/index.ts +++ b/src/repository/index.ts @@ -139,6 +139,7 @@ export async function init(orm: Orm, s3Config: S3StorageConfig): Promise { return await this.storage.getNotesByIds(noteIds); } + + /** + * Gets note list by parent note id + * @param parentId - parent note id + * @param offset - number of skipped notes + * @param limit - number of notes to get + */ + public async getNoteListByParentNote(parentNoteId: number, offset: number, limit: number): Promise { + return await this.storage.getNoteListByParentNote(parentNoteId, offset, limit); + } } diff --git a/src/repository/storage/postgres/orm/sequelize/note.ts b/src/repository/storage/postgres/orm/sequelize/note.ts index c45b3be5..c620c39a 100644 --- a/src/repository/storage/postgres/orm/sequelize/note.ts +++ b/src/repository/storage/postgres/orm/sequelize/note.ts @@ -5,6 +5,8 @@ import type { Note, NoteCreationAttributes, NoteInternalId, NotePublicId } from import { UserModel } from '@repository/storage/postgres/orm/sequelize/user.js'; import type { NoteSettingsModel } from './noteSettings.js'; import type { NoteVisitsModel } from './noteVisits.js'; +import type { NoteRelationsModel } from './noteRelations.js'; +import { DomainError } from '@domain/entities/DomainError.js'; import type { NoteHistoryModel } from './noteHistory.js'; /* eslint-disable @typescript-eslint/naming-convention */ @@ -75,6 +77,7 @@ export default class NoteSequelizeStorage { public historyModel: typeof NoteHistoryModel | null = null; + public relationsModel: typeof NoteRelationsModel | null = null; /** * Database instance */ @@ -155,6 +158,14 @@ export default class NoteSequelizeStorage { }); }; + public createAssociationWithNoteRelationsModel(model: ModelStatic): void { + this.relationsModel = model; + + this.model.hasMany(this.relationsModel, { + foreignKey: 'parentId', + as: 'noteRelations', + }) + } /** * Insert note to database * @param options - note creation options @@ -346,4 +357,57 @@ export default class NoteSequelizeStorage { return notes; } + + /** + * Gets note list by parent note id + * @param parentId - parent note id + * @param offset - number of skipped notes + * @param limit - number of notes to get + */ + public async getNoteListByParentNote(parentId: NoteInternalId, offset: number, limit: number): Promise { + if (!this.relationsModel) { + throw new Error('NoteRelations model not initialized'); + } + + if (!this.settingsModel) { + throw new Error('Note settings model not initialized'); + } + + const childNotes = await this.relationsModel.findAll({ + where: { parentId }, + attributes: ['noteId'], + }); + + const noteIds = childNotes.map(relation => relation.noteId); + + + const reply = await this.model.findAll({ + where: { + id: { + [Op.in]: noteIds, + }, + }, + include:[{ + model: this.settingsModel, + as: 'noteSettings', + attributes: ['cover'], + duplicating: false, + }], + offset, + limit, + }); + + return reply.map((note) => { + return { + id: note.id, + cover: note.noteSettings!.cover, + content: note.content, + updatedAt: note.updatedAt, + createdAt: note.createdAt, + publicId: note.publicId, + creatorId: note.creatorId, + tools: note.tools, + }; + }); + } } From 617ebc85e38532828dd4195a333e94c965082958 Mon Sep 17 00:00:00 2001 From: Tushar Ahire Date: Tue, 5 Nov 2024 14:29:21 +0530 Subject: [PATCH 2/5] [Feature] adds tests for note list by parent note api endpoint --- src/presentation/http/router/noteList.test.ts | 129 ++++++++++++++++++ src/presentation/http/router/noteList.ts | 1 + 2 files changed, 130 insertions(+) diff --git a/src/presentation/http/router/noteList.test.ts b/src/presentation/http/router/noteList.test.ts index 83de4587..66b1cf16 100644 --- a/src/presentation/http/router/noteList.test.ts +++ b/src/presentation/http/router/noteList.test.ts @@ -144,3 +144,132 @@ describe('GET /notes?page', () => { } }); }); + + +describe('GET /notes/:parentNoteId?page', () => { + test.each([ + /** + * Returns noteList with specified length + * User is authorized + */ + { + isAuthorized: true, + expectedStatusCode: 200, + expectedMessage: null, + expectedLength: 30, + pageNumber: 1, + }, + /** + * Returns noteList with specified length (for last page) + * User is authorized + */ + { + isAuthorized: true, + expectedStatusCode: 200, + expectedMessage: null, + expectedLength: 19, + pageNumber: 2, + }, + /** + * Returns noteList with no items if there are no notes for certain parentNote + * User is authorized + */ + { + isAuthorized: true, + expectedStatusCode: 200, + expectedMessage: null, + expectedLength: 0, + pageNumber: 3, + }, + /** + * Returns 'querystring/page must be >= 1' message when page < 0 + */ + { + isAuthorized: true, + expectedStatusCode: 400, + 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, + expectedMessage: 'querystring/page must be <= 30', + expectedLength: 0, + pageNumber: 31, + }, + /** + * Returns 'unauthorized' message when user is not authorized + */ + { + isAuthorized: false, + expectedStatusCode: 401, + expectedMessage: 'You must be authenticated to access this resource', + expectedLength: 0, + pageNumber: 1, + }, + ])('Get note list', async ({ isAuthorized, expectedStatusCode, 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); + } + + const parentNote = await global.db.insertNote({ + creatorId: creator.id, + }); + + await global.db.insertNoteSetting({ + noteId: parentNote.id, + cover: 'DZnvqi63.png', + isPublic: true, + }); + + for (let i = 0; i < portionSize; i++) { + const note = await global.db.insertNote({ + creatorId: creator.id, + }); + + await global.db.insertNoteSetting({ + noteId: note.id, + cover: 'DZnvqi63.png', + isPublic: true, + }); + + await global.db.insertNoteRelation({ + parentId: parentNote.id, + noteId: note.id, + }); + } + + const response = await global.api?.fakeRequest({ + method: 'GET', + headers: { + authorization: `Bearer ${accessToken}`, + }, + url: `/notes/${parentNote.id}?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); + } + }); +}); + diff --git a/src/presentation/http/router/noteList.ts b/src/presentation/http/router/noteList.ts index b12a5b13..c4d5c847 100644 --- a/src/presentation/http/router/noteList.ts +++ b/src/presentation/http/router/noteList.ts @@ -120,6 +120,7 @@ const NoteListRouter: FastifyPluginCallback = (fastify, o }>('/:parentNoteId', { config: { policy: [ + 'authRequired', 'notePublicOrUserInTeam', ], }, From 7ffc28bd187fd56f24b49480defaf8bb9fa7c0cf Mon Sep 17 00:00:00 2001 From: Tushar Ahire Date: Tue, 5 Nov 2024 15:16:43 +0530 Subject: [PATCH 3/5] [Fix] fix the eslint issues --- src/domain/service/note.ts | 5 ++-- src/presentation/http/http-api.ts | 2 +- .../http/middlewares/note/useNoteResolver.ts | 8 +++--- src/presentation/http/router/noteList.test.ts | 6 ++--- src/presentation/http/router/noteList.ts | 25 ++++++++++--------- src/repository/index.ts | 2 +- src/repository/note.repository.ts | 4 +-- .../storage/postgres/orm/sequelize/note.ts | 13 +++++----- 8 files changed, 32 insertions(+), 33 deletions(-) diff --git a/src/domain/service/note.ts b/src/domain/service/note.ts index cb92e483..602430e5 100644 --- a/src/domain/service/note.ts +++ b/src/domain/service/note.ts @@ -236,15 +236,16 @@ export default class NoteService { items: await this.noteRepository.getNoteListByUserId(userId, offset, this.noteListPortionSize), }; } - + /** * Returns note list by parent note id - * @param parentId - id of the parent note + * @param parentNoteId - id of the parent note * @param page - number of current page * @returns list of the notes ordered by time of last visit */ public async getNoteListByParentNote(parentNoteId: NoteInternalId, page: number): Promise { const offset = (page - 1) * this.noteListPortionSize; + return { items: await this.noteRepository.getNoteListByParentNote(parentNoteId, offset, this.noteListPortionSize), }; diff --git a/src/presentation/http/http-api.ts b/src/presentation/http/http-api.ts index 6376db8c..e607e4d8 100644 --- a/src/presentation/http/http-api.ts +++ b/src/presentation/http/http-api.ts @@ -207,7 +207,7 @@ export default class HttpApi implements Api { await this.server?.register(NoteListRouter, { prefix: '/notes', noteService: domainServices.noteService, - noteSettingsService: domainServices.noteSettingsService + noteSettingsService: domainServices.noteSettingsService, }); await this.server?.register(JoinRouter, { diff --git a/src/presentation/http/middlewares/note/useNoteResolver.ts b/src/presentation/http/middlewares/note/useNoteResolver.ts index 6ae3e200..2c1cc817 100644 --- a/src/presentation/http/middlewares/note/useNoteResolver.ts +++ b/src/presentation/http/middlewares/note/useNoteResolver.ts @@ -35,13 +35,13 @@ export default function useNoteResolver(noteService: NoteService): { const publicId = requestData.notePublicId as NotePublicId; return await noteService.getNoteByPublicId(publicId); - } - else if (hasProperty(requestData, 'parentNoteId') && notEmpty(requestData.parentNoteId)) { + } else if (hasProperty(requestData, 'parentNoteId') && notEmpty(requestData.parentNoteId)) { const noteId = requestData.parentNoteId as NoteInternalId; - return await noteService.getNoteById(noteId) + + return await noteService.getNoteById(noteId); } } - + return { noteResolver: async function noteIdResolver(request, reply) { let note: Note | undefined; diff --git a/src/presentation/http/router/noteList.test.ts b/src/presentation/http/router/noteList.test.ts index 66b1cf16..edb11ed0 100644 --- a/src/presentation/http/router/noteList.test.ts +++ b/src/presentation/http/router/noteList.test.ts @@ -145,7 +145,6 @@ describe('GET /notes?page', () => { }); }); - describe('GET /notes/:parentNoteId?page', () => { test.each([ /** @@ -233,7 +232,7 @@ describe('GET /notes/:parentNoteId?page', () => { cover: 'DZnvqi63.png', isPublic: true, }); - + for (let i = 0; i < portionSize; i++) { const note = await global.db.insertNote({ creatorId: creator.id, @@ -244,7 +243,7 @@ describe('GET /notes/:parentNoteId?page', () => { cover: 'DZnvqi63.png', isPublic: true, }); - + await global.db.insertNoteRelation({ parentId: parentNote.id, noteId: note.id, @@ -272,4 +271,3 @@ describe('GET /notes/:parentNoteId?page', () => { } }); }); - diff --git a/src/presentation/http/router/noteList.ts b/src/presentation/http/router/noteList.ts index c4d5c847..7a95ffc2 100644 --- a/src/presentation/http/router/noteList.ts +++ b/src/presentation/http/router/noteList.ts @@ -6,7 +6,7 @@ import useMemberRoleResolver from '../middlewares/noteSettings/useMemberRoleReso import type NoteSettingsService from '@domain/service/noteSettings.js'; import { definePublicNote, type NotePublic } from '@domain/entities/notePublic.js'; import type { NoteListPublic } from '@domain/entities/noteList.js'; -import { NoteInternalId } from '@domain/entities/note.js'; +import type { NoteInternalId } from '@domain/entities/note.js'; /** * Interface for the noteList router. @@ -18,11 +18,10 @@ interface NoteListRouterOptions { noteService: NoteService; /** - * Note Settings service instance - */ + * Note Settings service instance + */ noteSettingsService: NoteSettingsService; - } /** @@ -113,7 +112,7 @@ const NoteListRouter: FastifyPluginCallback = (fastify, o fastify.get<{ Params: { parentNoteId: NoteInternalId; - } + }; Querystring: { page: number; }; @@ -129,13 +128,15 @@ const NoteListRouter: FastifyPluginCallback = (fastify, o notePublicId: { $ref: 'NoteSchema#/properties/id', }, - }, querystring: { + }, + querystring: { page: { type: 'number', minimum: 1, maximum: 30, }, - }, response: { + }, + response: { '2xx': { description: 'Query notelist', properties: { @@ -149,14 +150,15 @@ const NoteListRouter: FastifyPluginCallback = (fastify, o }, }, }, - }, preHandler: [ + }, + preHandler: [ noteResolver, noteSettingsResolver, memberRoleResolver, ], }, async (request, reply) => { - const { parentNoteId } = await request.params; - const { page } = await request.query; + const { parentNoteId } = request.params; + const { page } = request.query; const noteList = await noteService.getNoteListByParentNote(parentNoteId, page); /** @@ -169,8 +171,7 @@ const NoteListRouter: FastifyPluginCallback = (fastify, o }; return reply.send(noteListPublic); - }) - + }); done(); }; diff --git a/src/repository/index.ts b/src/repository/index.ts index 35ff6811..f90aec6f 100644 --- a/src/repository/index.ts +++ b/src/repository/index.ts @@ -139,7 +139,7 @@ export async function init(orm: Orm, s3Config: S3StorageConfig): Promise { return await this.storage.getNotesByIds(noteIds); } - + /** * Gets note list by parent note id - * @param parentId - parent note id + * @param parentNoteId - parent note id * @param offset - number of skipped notes * @param limit - number of notes to get */ diff --git a/src/repository/storage/postgres/orm/sequelize/note.ts b/src/repository/storage/postgres/orm/sequelize/note.ts index c620c39a..c00e5a9b 100644 --- a/src/repository/storage/postgres/orm/sequelize/note.ts +++ b/src/repository/storage/postgres/orm/sequelize/note.ts @@ -6,7 +6,6 @@ import { UserModel } from '@repository/storage/postgres/orm/sequelize/user.js'; import type { NoteSettingsModel } from './noteSettings.js'; import type { NoteVisitsModel } from './noteVisits.js'; import type { NoteRelationsModel } from './noteRelations.js'; -import { DomainError } from '@domain/entities/DomainError.js'; import type { NoteHistoryModel } from './noteHistory.js'; /* eslint-disable @typescript-eslint/naming-convention */ @@ -164,8 +163,9 @@ export default class NoteSequelizeStorage { this.model.hasMany(this.relationsModel, { foreignKey: 'parentId', as: 'noteRelations', - }) + }); } + /** * Insert note to database * @param options - note creation options @@ -357,7 +357,7 @@ export default class NoteSequelizeStorage { return notes; } - + /** * Gets note list by parent note id * @param parentId - parent note id @@ -377,17 +377,16 @@ export default class NoteSequelizeStorage { where: { parentId }, attributes: ['noteId'], }); - + const noteIds = childNotes.map(relation => relation.noteId); - - + const reply = await this.model.findAll({ where: { id: { [Op.in]: noteIds, }, }, - include:[{ + include: [{ model: this.settingsModel, as: 'noteSettings', attributes: ['cover'], From e614623ca48edada833029e0c2eb757f0a69eea7 Mon Sep 17 00:00:00 2001 From: Tushar Ahire Date: Wed, 6 Nov 2024 17:33:55 +0530 Subject: [PATCH 4/5] [Fix] adds reviewed changes --- src/presentation/http/router/noteList.test.ts | 69 ++++++++++++++++--- src/presentation/http/router/noteList.ts | 7 +- .../storage/postgres/orm/sequelize/note.ts | 47 +++++++------ 3 files changed, 88 insertions(+), 35 deletions(-) diff --git a/src/presentation/http/router/noteList.test.ts b/src/presentation/http/router/noteList.test.ts index edb11ed0..6e997ed5 100644 --- a/src/presentation/http/router/noteList.test.ts +++ b/src/presentation/http/router/noteList.test.ts @@ -157,6 +157,8 @@ describe('GET /notes/:parentNoteId?page', () => { expectedMessage: null, expectedLength: 30, pageNumber: 1, + isTeamMember: false, + isPublic: true, }, /** * Returns noteList with specified length (for last page) @@ -168,6 +170,8 @@ describe('GET /notes/:parentNoteId?page', () => { expectedMessage: null, expectedLength: 19, pageNumber: 2, + isTeamMember: false, + isPublic: true, }, /** * Returns noteList with no items if there are no notes for certain parentNote @@ -179,6 +183,8 @@ describe('GET /notes/:parentNoteId?page', () => { expectedMessage: null, expectedLength: 0, pageNumber: 3, + isTeamMember: false, + isPublic: true, }, /** * Returns 'querystring/page must be >= 1' message when page < 0 @@ -189,6 +195,8 @@ describe('GET /notes/:parentNoteId?page', () => { expectedMessage: 'querystring/page must be >= 1', expectedLength: 0, pageNumber: -1, + isTeamMember: false, + isPublic: true, }, /** * Returns 'querystring/page must be <= 30' message when page is too large (maximum page numbrer is 30 by default) @@ -199,6 +207,8 @@ describe('GET /notes/:parentNoteId?page', () => { expectedMessage: 'querystring/page must be <= 30', expectedLength: 0, pageNumber: 31, + isTeamMember: false, + isPublic: true, }, /** * Returns 'unauthorized' message when user is not authorized @@ -209,30 +219,69 @@ describe('GET /notes/:parentNoteId?page', () => { expectedMessage: 'You must be authenticated to access this resource', expectedLength: 0, pageNumber: 1, + isTeamMember: false, + isPublic: true, }, - ])('Get note list', async ({ isAuthorized, expectedStatusCode, expectedMessage, expectedLength, pageNumber }) => { + /** + * Returns noteList if user is in team + * User is authorized + */ + { + isAuthorized: true, + expectedStatusCode: 200, + expectedMessage: null, + expectedLength: 30, + pageNumber: 1, + isTeamMember: true, + isPublic: false, + }, + /** + * Returns error message with no items if user is not in team + * User is authorized + */ + { + isAuthorized: true, + expectedStatusCode: 403, + expectedMessage: 'Permission denied', + expectedLength: 0, + pageNumber: 1, + isTeamMember: false, + isPublic: false, + }, + ])('Get note list', async ({ isAuthorized, expectedStatusCode, expectedMessage, expectedLength, pageNumber, isTeamMember, isPublic }) => { const portionSize = 49; let accessToken; - /** Insert creator and randomGuy */ + /** Insert creator */ const creator = await global.db.insertUser(); - const randomGuy = await global.db.insertUser(); - - if (isAuthorized) { - accessToken = global.auth(randomGuy.id); - } - + /** Insert Note */ const parentNote = await global.db.insertNote({ creatorId: creator.id, }); - await global.db.insertNoteSetting({ + const noteSetting = await global.db.insertNoteSetting({ noteId: parentNote.id, cover: 'DZnvqi63.png', - isPublic: true, + isPublic: isPublic, }); + const randomGuy = await global.db.insertUser(); + + if (isAuthorized) { + accessToken = global.auth(randomGuy.id); + } + + if (isTeamMember) { + await global.api?.fakeRequest({ + method: 'POST', + headers: { + authorization: `Bearer ${accessToken}`, + }, + url: `/join/${noteSetting.invitationHash}`, + }); + } + for (let i = 0; i < portionSize; i++) { const note = await global.db.insertNote({ creatorId: creator.id, diff --git a/src/presentation/http/router/noteList.ts b/src/presentation/http/router/noteList.ts index 7a95ffc2..46fd8276 100644 --- a/src/presentation/http/router/noteList.ts +++ b/src/presentation/http/router/noteList.ts @@ -141,11 +141,8 @@ const NoteListRouter: FastifyPluginCallback = (fastify, o description: 'Query notelist', properties: { items: { - id: { type: 'string' }, - content: { type: 'string' }, - createdAt: { type: 'string' }, - creatorId: { type: 'string' }, - updatedAt: { type: 'string' }, + type: 'array', + items: { $ref: 'NoteSchema#' }, }, }, }, diff --git a/src/repository/storage/postgres/orm/sequelize/note.ts b/src/repository/storage/postgres/orm/sequelize/note.ts index c00e5a9b..7a1457db 100644 --- a/src/repository/storage/postgres/orm/sequelize/note.ts +++ b/src/repository/storage/postgres/orm/sequelize/note.ts @@ -161,7 +161,7 @@ export default class NoteSequelizeStorage { this.relationsModel = model; this.model.hasMany(this.relationsModel, { - foreignKey: 'parentId', + foreignKey: 'noteId', as: 'noteRelations', }); } @@ -373,27 +373,34 @@ export default class NoteSequelizeStorage { throw new Error('Note settings model not initialized'); } - const childNotes = await this.relationsModel.findAll({ - where: { parentId }, - attributes: ['noteId'], - }); - - const noteIds = childNotes.map(relation => relation.noteId); - const reply = await this.model.findAll({ - where: { - id: { - [Op.in]: noteIds, + include: [ + { + model: this.relationsModel, + as: 'noteRelations', + where: { + parentId, + }, + duplicating: false, + attributes: [], }, - }, - include: [{ - model: this.settingsModel, - as: 'noteSettings', - attributes: ['cover'], - duplicating: false, - }], - offset, - limit, + { + model: this.settingsModel, + as: 'noteSettings', + attributes: ['cover'], + duplicating: false, + }, + ], + order: [[ + { + model: this.relationsModel, + as: 'noteRelations', + }, + 'id', + 'DESC', + ]], + offset: offset, + limit: limit, }); return reply.map((note) => { From ad8b82b0ac2645b96bc10571f8fedd935f5b3ba1 Mon Sep 17 00:00:00 2001 From: Tushar Ahire Date: Fri, 8 Nov 2024 22:34:53 +0530 Subject: [PATCH 5/5] [fix] adds checks for parent note is public or user is team member --- .../http/middlewares/note/useNoteResolver.ts | 6 +-- src/presentation/http/router/noteList.ts | 43 +++++++------------ .../storage/postgres/orm/sequelize/teams.ts | 2 +- 3 files changed, 18 insertions(+), 33 deletions(-) diff --git a/src/presentation/http/middlewares/note/useNoteResolver.ts b/src/presentation/http/middlewares/note/useNoteResolver.ts index 2c1cc817..73ae1da3 100644 --- a/src/presentation/http/middlewares/note/useNoteResolver.ts +++ b/src/presentation/http/middlewares/note/useNoteResolver.ts @@ -4,7 +4,7 @@ import { notEmpty } from '@infrastructure/utils/empty.js'; import { StatusCodes } from 'http-status-codes'; import hasProperty from '@infrastructure/utils/hasProperty.js'; import { getLogger } from '@infrastructure/logging/index.js'; -import type { Note, NotePublicId, NoteInternalId } from '@domain/entities/note.js'; +import type { Note, NotePublicId } from '@domain/entities/note.js'; /** * Add middleware for resolve Note by public id and add it to request @@ -35,10 +35,6 @@ export default function useNoteResolver(noteService: NoteService): { const publicId = requestData.notePublicId as NotePublicId; return await noteService.getNoteByPublicId(publicId); - } else if (hasProperty(requestData, 'parentNoteId') && notEmpty(requestData.parentNoteId)) { - const noteId = requestData.parentNoteId as NoteInternalId; - - return await noteService.getNoteById(noteId); } } diff --git a/src/presentation/http/router/noteList.ts b/src/presentation/http/router/noteList.ts index 46fd8276..6ca1eac2 100644 --- a/src/presentation/http/router/noteList.ts +++ b/src/presentation/http/router/noteList.ts @@ -1,8 +1,5 @@ import type { FastifyPluginCallback } from 'fastify'; import type NoteService from '@domain/service/note.js'; -import useNoteResolver from '../middlewares/note/useNoteResolver.js'; -import useNoteSettingsResolver from '../middlewares/noteSettings/useNoteSettingsResolver.js'; -import useMemberRoleResolver from '../middlewares/noteSettings/useMemberRoleResolver.js'; import type NoteSettingsService from '@domain/service/noteSettings.js'; import { definePublicNote, type NotePublic } from '@domain/entities/notePublic.js'; import type { NoteListPublic } from '@domain/entities/noteList.js'; @@ -34,24 +31,6 @@ const NoteListRouter: FastifyPluginCallback = (fastify, o const noteService = opts.noteService; const noteSettingsService = opts.noteSettingsService; - /** - * Prepare note id resolver middleware - * It should be used in routes that accepts note public id - */ - const { noteResolver } = useNoteResolver(noteService); - - /** - * Prepare note settings resolver middleware - * It should be used to use note settings in middlewares - */ - const { noteSettingsResolver } = useNoteSettingsResolver(noteSettingsService); - - /** - * Prepare user role resolver middleware - * It should be used to use user role in middlewares - */ - const { memberRoleResolver } = useMemberRoleResolver(noteSettingsService); - /** * Get note list ordered by time of last visit */ @@ -120,7 +99,6 @@ const NoteListRouter: FastifyPluginCallback = (fastify, o config: { policy: [ 'authRequired', - 'notePublicOrUserInTeam', ], }, schema: { @@ -148,15 +126,26 @@ const NoteListRouter: FastifyPluginCallback = (fastify, o }, }, }, - preHandler: [ - noteResolver, - noteSettingsResolver, - memberRoleResolver, - ], }, async (request, reply) => { const { parentNoteId } = request.params; + const userId = request.userId as number; const { page } = request.query; + /** + * Fetching note settings from noteSetting service + */ + const noteSettings = await noteSettingsService.getNoteSettingsByNoteId(parentNoteId); + + if (!noteSettings.isPublic) { + const isTeamMember = noteSettings.team?.find(team => team.userId === userId); + + /** + * Checks if the user is a member of the team + */ + if (!isTeamMember) { + return reply.forbidden(); + } + } const noteList = await noteService.getNoteListByParentNote(parentNoteId, page); /** * Wrapping Notelist for public use diff --git a/src/repository/storage/postgres/orm/sequelize/teams.ts b/src/repository/storage/postgres/orm/sequelize/teams.ts index 5dfc0cd5..cfaac841 100644 --- a/src/repository/storage/postgres/orm/sequelize/teams.ts +++ b/src/repository/storage/postgres/orm/sequelize/teams.ts @@ -192,7 +192,7 @@ export default class TeamsSequelizeStorage { return await this.model.findAll({ where: { noteId }, - attributes: ['id', 'role'], + attributes: ['id', 'role', 'userId'], include: { model: this.userModel, as: 'user',