Skip to content

Commit

Permalink
Merge pull request #267 from codex-team/implement-public-team-member
Browse files Browse the repository at this point in the history
chore(team): Implemented public team member
  • Loading branch information
e11sy authored Jul 4, 2024
2 parents 17be90d + c4962c3 commit b393213
Show file tree
Hide file tree
Showing 8 changed files with 68 additions and 37 deletions.
7 changes: 6 additions & 1 deletion src/domain/entities/team.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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<TeamMember, 'noteId' | 'id'> & { noteId: NotePublicId };

export type Team = TeamMember[];

export type TeamMemberCreationAttributes = Omit<TeamMember, 'id'>;
16 changes: 16 additions & 0 deletions src/domain/service/note.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,4 +310,20 @@ 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<NotePublicId> {
const note = await this.noteRepository.getNoteById(id);

if (note === null) {
throw new DomainError(`Note with id ${id} was not found`);
}

return note.publicId;
}
}
26 changes: 18 additions & 8 deletions src/domain/service/noteSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<TeamMember | null> {
public async addUserToTeamByInvitationHash(invitationHash: InvitationHash, userId: User['id']): Promise<TeamMemberPublic | null> {
const defaultUserRole = MemberRole.Read;
const noteSettings = await this.noteSettingsRepository.getNoteSettingsByInvitationHash(invitationHash);

Expand All @@ -50,19 +50,29 @@ 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,
};
}

return await this.teamRepository.createTeamMembership({
const teamMember = await this.teamRepository.createTeamMembership({
noteId: noteSettings.noteId,
userId,
role: defaultUserRole,
});

return {
noteId: await this.shared.note.getNotePublicIdByInternal(teamMember.noteId),
userId: teamMember.userId,
role: teamMember.role,
};
}

/**
Expand Down
8 changes: 8 additions & 0 deletions src/domain/service/shared/note.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -11,4 +12,11 @@ export default interface NoteServiceSharedMethods {
* @param noteId - id of the current note
*/
getParentNoteIdByNoteId(noteId: NoteInternalId): Promise<NoteInternalId | null>;

/**
* 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<NotePublicId>;
}
24 changes: 9 additions & 15 deletions src/presentation/http/router/join.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand All @@ -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 */
Expand All @@ -45,10 +39,12 @@ describe('Join API', () => {
url: `/join/${invitationHash}`,
});

expect(response?.statusCode).toBe(406);
expect(response?.statusCode).toBe(200);

expect(response?.json()).toStrictEqual({
message: 'User already in team',
expect(response?.json()).toMatchObject({
userId: user.id,
noteId: note.publicId,
role: 1,
});
});
test('Returns 406 when invitation hash is not valid', async () => {
Expand Down Expand Up @@ -112,11 +108,9 @@ describe('Join API', () => {
expect(response?.statusCode).toBe(200);

expect(response?.json()).toMatchObject({
result: {
userId: randomGuy.id,
noteId: note.id,
role: 0,
},
userId: randomGuy.id,
noteId: note.publicId,
role: 0,
});
});
});
Expand Down
6 changes: 3 additions & 3 deletions src/presentation/http/router/join.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -55,7 +55,7 @@ const JoinRouter: FastifyPluginCallback<JoinRouterOptions> = (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);
Expand All @@ -65,7 +65,7 @@ const JoinRouter: FastifyPluginCallback<JoinRouterOptions> = (fastify, opts, don
return reply.notAcceptable(causedError.message);
}

return reply.send({ result });
return reply.send(result);
});

done();
Expand Down
10 changes: 4 additions & 6 deletions src/repository/storage/postgres/orm/sequelize/teams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,20 +154,18 @@ 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<boolean> {
const teamMemberShip = await this.model.findOne({
public async getTeamMemberByNoteAndUserId(userId: User['id'], noteId: NoteInternalId): Promise<TeamMember | null> {
return await this.model.findOne({
where: {
noteId,
userId,
},
});

return teamMemberShip !== null;
}

/**
Expand Down
8 changes: 4 additions & 4 deletions src/repository/team.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean> {
return await this.storage.isUserInTeam(userId, noteId);
public async getTeamMemberByNoteAndUserId(userId: User['id'], noteId: NoteInternalId): Promise<TeamMember | null> {
return await this.storage.getTeamMemberByNoteAndUserId(userId, noteId);
}

/**
Expand Down

0 comments on commit b393213

Please sign in to comment.