From 49bb773d0812d34c423905b819bf43b7adec1f07 Mon Sep 17 00:00:00 2001 From: e11sy Date: Thu, 4 Jul 2024 06:11:04 +0300 Subject: [PATCH 01/10] implemented public team member --- src/domain/entities/team.ts | 7 ++++++- src/domain/service/note.ts | 12 ++++++++++++ src/domain/service/noteSettings.ts | 13 ++++++++++--- src/domain/service/shared/note.ts | 8 ++++++++ src/presentation/http/router/join.ts | 4 ++-- 5 files changed, 38 insertions(+), 6 deletions(-) diff --git a/src/domain/entities/team.ts b/src/domain/entities/team.ts index 82415efd..b2b7a7c5 100644 --- a/src/domain/entities/team.ts +++ b/src/domain/entities/team.ts @@ -1,4 +1,4 @@ -import type { NoteInternalId } from './note.js'; +import type { NoteInternalId, NotePublicId } from './note.js'; import type User from './user.js'; export enum MemberRole { @@ -39,6 +39,11 @@ export interface TeamMember { role: MemberRole; } +/** + * Team member public entity sends to user with public id of the note + */ +export type TeamMemberPublic = Omit & { noteId: NotePublicId }; + export type Team = TeamMember[]; export type TeamMemberCreationAttributes = Omit; diff --git a/src/domain/service/note.ts b/src/domain/service/note.ts index 323ce519..9a09405b 100644 --- a/src/domain/service/note.ts +++ b/src/domain/service/note.ts @@ -310,4 +310,16 @@ export default class NoteService { throw new DomainError('Incorrect tools passed'); } } + + /** + * Get note public id by it's internal id + * Used for making entities that use NoteInternalId public + * @param id - internal id of the note + * @returns note public id + */ + public async getNotePublicIdByInternal(id: NoteInternalId): Promise { + const note = await this.noteRepository.getNoteById(id); + + return note!.publicId; + } } diff --git a/src/domain/service/noteSettings.ts b/src/domain/service/noteSettings.ts index a0d03c40..322b46b4 100644 --- a/src/domain/service/noteSettings.ts +++ b/src/domain/service/noteSettings.ts @@ -3,7 +3,7 @@ import type { InvitationHash } from '@domain/entities/noteSettings.js'; import type NoteSettings from '@domain/entities/noteSettings.js'; import type NoteSettingsRepository from '@repository/noteSettings.repository.js'; import type TeamRepository from '@repository/team.repository.js'; -import type { Team, TeamMember, TeamMemberCreationAttributes } from '@domain/entities/team.js'; +import type { Team, TeamMember, TeamMemberPublic, TeamMemberCreationAttributes } from '@domain/entities/team.js'; import { MemberRole } from '@domain/entities/team.js'; import type User from '@domain/entities/user.js'; import { createInvitationHash } from '@infrastructure/utils/invitationHash.js'; @@ -38,7 +38,7 @@ export default class NoteSettingsService { * @param invitationHash - hash for joining to the team * @param userId - user to add */ - public async addUserToTeamByInvitationHash(invitationHash: InvitationHash, userId: User['id']): Promise { + public async addUserToTeamByInvitationHash(invitationHash: InvitationHash, userId: User['id']): Promise { const defaultUserRole = MemberRole.Read; const noteSettings = await this.noteSettingsRepository.getNoteSettingsByInvitationHash(invitationHash); @@ -58,11 +58,18 @@ export default class NoteSettingsService { throw new DomainError(`User already in team`); } - return await this.teamRepository.createTeamMembership({ + const teamMember = await this.teamRepository.createTeamMembership({ noteId: noteSettings.noteId, userId, role: defaultUserRole, }); + + return { + id: teamMember.id, + noteId: await this.shared.note.getNotePublicIdByInternal(teamMember.noteId), + userId: teamMember.userId, + role: teamMember.role, + }; } /** diff --git a/src/domain/service/shared/note.ts b/src/domain/service/shared/note.ts index 0c0acff4..eee962f2 100644 --- a/src/domain/service/shared/note.ts +++ b/src/domain/service/shared/note.ts @@ -1,4 +1,5 @@ import type { NoteInternalId } from '@domain/entities/note.js'; +import type { NotePublicId } from '@domain/entities/note.js'; /** * Which methods of Domain can be used by other domains @@ -11,4 +12,11 @@ export default interface NoteServiceSharedMethods { * @param noteId - id of the current note */ getParentNoteIdByNoteId(noteId: NoteInternalId): Promise; + + /** + * Get note public id by it's internal id + * Used for making entities that use NoteInternalId public + * @param id - internal id of the note + */ + getNotePublicIdByInternal(noteId: NoteInternalId): Promise; } diff --git a/src/presentation/http/router/join.ts b/src/presentation/http/router/join.ts index 3c51c336..8f34c293 100644 --- a/src/presentation/http/router/join.ts +++ b/src/presentation/http/router/join.ts @@ -1,6 +1,6 @@ import type { FastifyPluginCallback } from 'fastify'; import type NoteSettingsService from '@domain/service/noteSettings.js'; -import type { TeamMember } from '@domain/entities/team.js'; +import type { TeamMemberPublic } from '@domain/entities/team.js'; /** * Represents AI router options @@ -55,7 +55,7 @@ const JoinRouter: FastifyPluginCallback = (fastify, opts, don }, async (request, reply) => { const { hash } = request.params; const { userId } = request; - let result: TeamMember | null = null; + let result: TeamMemberPublic | null = null; try { result = await noteSettingsService.addUserToTeamByInvitationHash(hash, userId as number); From 29c6eee714c30d0ea8fd28e61cca23dfe7bde7ed Mon Sep 17 00:00:00 2001 From: e11sy Date: Thu, 4 Jul 2024 06:22:24 +0300 Subject: [PATCH 02/10] tests fixed --- src/presentation/http/router/join.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/presentation/http/router/join.test.ts b/src/presentation/http/router/join.test.ts index ee2579e8..1631aa22 100644 --- a/src/presentation/http/router/join.test.ts +++ b/src/presentation/http/router/join.test.ts @@ -114,7 +114,7 @@ describe('Join API', () => { expect(response?.json()).toMatchObject({ result: { userId: randomGuy.id, - noteId: note.id, + noteId: note.publicId, role: 0, }, }); From f20250b5b3ef128cd4a2e2d458571abf4c92dbf3 Mon Sep 17 00:00:00 2001 From: e11sy Date: Thu, 4 Jul 2024 14:52:18 +0300 Subject: [PATCH 03/10] patched teamMemberPublic entity - now team member pubic does not contain id of team relation --- src/domain/entities/team.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domain/entities/team.ts b/src/domain/entities/team.ts index b2b7a7c5..3245bfb8 100644 --- a/src/domain/entities/team.ts +++ b/src/domain/entities/team.ts @@ -42,7 +42,7 @@ export interface TeamMember { /** * Team member public entity sends to user with public id of the note */ -export type TeamMemberPublic = Omit & { noteId: NotePublicId }; +export type TeamMemberPublic = Omit & { noteId: NotePublicId }; export type Team = TeamMember[]; From dd91e6a02ee82302518c14bf590681b885510286 Mon Sep 17 00:00:00 2001 From: e11sy Date: Thu, 4 Jul 2024 14:54:38 +0300 Subject: [PATCH 04/10] patched repository method - isUserInTeam deprecated - now we have getTeamMemberByNoteAndUserId method which will return null if user is not in team, teamMember otherwhise --- src/domain/service/noteSettings.ts | 15 +++++++++------ src/presentation/http/router/join.ts | 2 +- .../storage/postgres/orm/sequelize/teams.ts | 8 ++++---- src/repository/team.repository.ts | 8 ++++---- 4 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/domain/service/noteSettings.ts b/src/domain/service/noteSettings.ts index 322b46b4..841cc610 100644 --- a/src/domain/service/noteSettings.ts +++ b/src/domain/service/noteSettings.ts @@ -50,12 +50,16 @@ export default class NoteSettingsService { } /** - * Check if user not already in team + * Try to get team member by user and note id */ - const isUserTeamMember = await this.teamRepository.isUserInTeam(userId, noteSettings.noteId); - - if (isUserTeamMember) { - throw new DomainError(`User already in team`); + const member = await this.teamRepository.getTeamMemberByNoteAndUserId(userId, noteSettings.noteId); + + if (member !== null) { + return { + noteId: await this.shared.note.getNotePublicIdByInternal(member.noteId), + userId: member.userId, + role: member.role, + }; } const teamMember = await this.teamRepository.createTeamMembership({ @@ -65,7 +69,6 @@ export default class NoteSettingsService { }); return { - id: teamMember.id, noteId: await this.shared.note.getNotePublicIdByInternal(teamMember.noteId), userId: teamMember.userId, role: teamMember.role, diff --git a/src/presentation/http/router/join.ts b/src/presentation/http/router/join.ts index 8f34c293..f7e22ed5 100644 --- a/src/presentation/http/router/join.ts +++ b/src/presentation/http/router/join.ts @@ -65,7 +65,7 @@ const JoinRouter: FastifyPluginCallback = (fastify, opts, don return reply.notAcceptable(causedError.message); } - return reply.send({ result }); + return reply.send(result); }); done(); diff --git a/src/repository/storage/postgres/orm/sequelize/teams.ts b/src/repository/storage/postgres/orm/sequelize/teams.ts index ba599760..88b92f28 100644 --- a/src/repository/storage/postgres/orm/sequelize/teams.ts +++ b/src/repository/storage/postgres/orm/sequelize/teams.ts @@ -154,12 +154,12 @@ export default class TeamsSequelizeStorage { } /** - * Check if user is note team member + * Get team member by user id and note id * @param userId - user id to check * @param noteId - note id to identify team - * @returns returns true if user is team member + * @returns return null if user is not in team, teamMember otherwhise */ - public async isUserInTeam(userId: User['id'], noteId: NoteInternalId): Promise { + public async getTeamMemberByNoteAndUserId(userId: User['id'], noteId: NoteInternalId): Promise { const teamMemberShip = await this.model.findOne({ where: { noteId, @@ -167,7 +167,7 @@ export default class TeamsSequelizeStorage { }, }); - return teamMemberShip !== null; + return teamMemberShip; } /** diff --git a/src/repository/team.repository.ts b/src/repository/team.repository.ts index a3a23141..90de8673 100644 --- a/src/repository/team.repository.ts +++ b/src/repository/team.repository.ts @@ -30,13 +30,13 @@ export default class TeamRepository { } /** - * Check if user is note team member + * Get team member by user id and note id * @param userId - user id to check * @param noteId - note id to identify team - * @returns returns true if user is team member + * @returns null if user is not in team, teamMember otherwhise */ - public async isUserInTeam(userId: User['id'], noteId: NoteInternalId): Promise { - return await this.storage.isUserInTeam(userId, noteId); + public async getTeamMemberByNoteAndUserId(userId: User['id'], noteId: NoteInternalId): Promise { + return await this.storage.getTeamMemberByNoteAndUserId(userId, noteId); } /** From 1f371613d7691fc3eb38686ce87c920e637f6901 Mon Sep 17 00:00:00 2001 From: e11sy Date: Thu, 4 Jul 2024 14:56:29 +0300 Subject: [PATCH 05/10] patched tests --- src/presentation/http/router/join.test.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/presentation/http/router/join.test.ts b/src/presentation/http/router/join.test.ts index 1631aa22..602d1d2e 100644 --- a/src/presentation/http/router/join.test.ts +++ b/src/presentation/http/router/join.test.ts @@ -2,7 +2,7 @@ import { describe, test, expect } from 'vitest'; describe('Join API', () => { describe('POST /join/:hash', () => { - test('Returns 406 when user is already in the team', async () => { + test('Returns 200 and teamMember when user is already in the team', async () => { const invitationHash = 'Hzh2hy4igf'; /** @@ -45,7 +45,15 @@ describe('Join API', () => { url: `/join/${invitationHash}`, }); - expect(response?.statusCode).toBe(406); + expect(response?.statusCode).toBe(200); + + expect(response?.json()).toMatchObject({ + result: { + userId: user.id, + noteId: note.publicId, + role: 0, + }, + }); expect(response?.json()).toStrictEqual({ message: 'User already in team', From edb445658f4ba001fcbdfce68225bd868bfe7b05 Mon Sep 17 00:00:00 2001 From: e11sy Date: Thu, 4 Jul 2024 15:04:03 +0300 Subject: [PATCH 06/10] get rid of result decoration in join route --- src/presentation/http/router/join.test.ts | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/presentation/http/router/join.test.ts b/src/presentation/http/router/join.test.ts index 602d1d2e..1f55d3cf 100644 --- a/src/presentation/http/router/join.test.ts +++ b/src/presentation/http/router/join.test.ts @@ -48,11 +48,9 @@ describe('Join API', () => { expect(response?.statusCode).toBe(200); expect(response?.json()).toMatchObject({ - result: { - userId: user.id, - noteId: note.publicId, - role: 0, - }, + userId: user.id, + noteId: note.publicId, + role: 0, }); expect(response?.json()).toStrictEqual({ @@ -120,11 +118,9 @@ describe('Join API', () => { expect(response?.statusCode).toBe(200); expect(response?.json()).toMatchObject({ - result: { - userId: randomGuy.id, - noteId: note.publicId, - role: 0, - }, + userId: randomGuy.id, + noteId: note.publicId, + role: 0, }); }); }); From 02fe309a63a7e59d7589d7a36be6518508a40a35 Mon Sep 17 00:00:00 2001 From: e11sy Date: Thu, 4 Jul 2024 15:09:48 +0300 Subject: [PATCH 07/10] fixed test logic --- src/presentation/http/router/join.test.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/presentation/http/router/join.test.ts b/src/presentation/http/router/join.test.ts index 1f55d3cf..7539742a 100644 --- a/src/presentation/http/router/join.test.ts +++ b/src/presentation/http/router/join.test.ts @@ -28,12 +28,6 @@ describe('Join API', () => { invitationHash, }); - await global.db.insertNoteTeam({ - userId: user.id, - noteId: note.id, - role: 0, - }); - const accessToken = global.auth(user.id); /** add same user to the same note team */ @@ -50,7 +44,7 @@ describe('Join API', () => { expect(response?.json()).toMatchObject({ userId: user.id, noteId: note.publicId, - role: 0, + role: 1, }); expect(response?.json()).toStrictEqual({ From a5b1a7b323083be5538209464d8bf81bae4b11a9 Mon Sep 17 00:00:00 2001 From: e11sy Date: Thu, 4 Jul 2024 15:31:51 +0300 Subject: [PATCH 08/10] test fix --- src/presentation/http/router/join.test.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/presentation/http/router/join.test.ts b/src/presentation/http/router/join.test.ts index 7539742a..772c6b7b 100644 --- a/src/presentation/http/router/join.test.ts +++ b/src/presentation/http/router/join.test.ts @@ -46,10 +46,6 @@ describe('Join API', () => { noteId: note.publicId, role: 1, }); - - expect(response?.json()).toStrictEqual({ - message: 'User already in team', - }); }); test('Returns 406 when invitation hash is not valid', async () => { const hash = 'Jih23y4igf'; From ffd394c9caeeafc88ad211f338177804a7253754 Mon Sep 17 00:00:00 2001 From: e11sy Date: Thu, 4 Jul 2024 15:34:02 +0300 Subject: [PATCH 09/10] handled corner case --- src/domain/service/note.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/domain/service/note.ts b/src/domain/service/note.ts index 9a09405b..348142ae 100644 --- a/src/domain/service/note.ts +++ b/src/domain/service/note.ts @@ -320,6 +320,10 @@ export default class NoteService { public async getNotePublicIdByInternal(id: NoteInternalId): Promise { const note = await this.noteRepository.getNoteById(id); - return note!.publicId; + if (note === null) { + throw new DomainError(`Note with id ${id}was not found`); + } + + return note.publicId; } } From c4962c323e48e7a6b6fe6c4e5f987ba0cd4be91c Mon Sep 17 00:00:00 2001 From: e11sy Date: Thu, 4 Jul 2024 15:38:02 +0300 Subject: [PATCH 10/10] minor fixes --- src/domain/service/note.ts | 2 +- src/repository/storage/postgres/orm/sequelize/teams.ts | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/domain/service/note.ts b/src/domain/service/note.ts index 348142ae..0d677c32 100644 --- a/src/domain/service/note.ts +++ b/src/domain/service/note.ts @@ -321,7 +321,7 @@ export default class NoteService { const note = await this.noteRepository.getNoteById(id); if (note === null) { - throw new DomainError(`Note with id ${id}was not found`); + throw new DomainError(`Note with id ${id} was not found`); } return note.publicId; diff --git a/src/repository/storage/postgres/orm/sequelize/teams.ts b/src/repository/storage/postgres/orm/sequelize/teams.ts index 88b92f28..24df0d4c 100644 --- a/src/repository/storage/postgres/orm/sequelize/teams.ts +++ b/src/repository/storage/postgres/orm/sequelize/teams.ts @@ -160,14 +160,12 @@ export default class TeamsSequelizeStorage { * @returns return null if user is not in team, teamMember otherwhise */ public async getTeamMemberByNoteAndUserId(userId: User['id'], noteId: NoteInternalId): Promise { - const teamMemberShip = await this.model.findOne({ + return await this.model.findOne({ where: { noteId, userId, }, }); - - return teamMemberShip; } /**