Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BC-7971 - introduce room members module #5291

Merged
merged 33 commits into from
Oct 23, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
786ce74
wip: added new roles and service to add user groups, tests broken.
EzzatOmar Oct 6, 2024
66b10bc
update authorization rule for room-members, connect to room member wi…
EzzatOmar Oct 15, 2024
91fe96e
Merge branch 'main' into BC-7971-room-members
EzzatOmar Oct 15, 2024
199aed9
fix broken do mapper tests
EzzatOmar Oct 15, 2024
6d085a3
remove docs
EzzatOmar Oct 15, 2024
ac210c1
Merge branch 'main' into BC-7971-room-members
EzzatOmar Oct 15, 2024
c2c60db
add group type = room
EzzatOmar Oct 16, 2024
d1c367c
- Add group type "room" to group module.
EzzatOmar Oct 16, 2024
48ae35f
Merge branch 'main' into BC-7971-room-members
EzzatOmar Oct 16, 2024
fb26f78
fix code coverage
EzzatOmar Oct 16, 2024
a04929d
Merge branch 'main' into BC-7971-room-members
EzzatOmar Oct 17, 2024
1ae2f1e
Merge branch 'main' into BC-7971-room-members
EzzatOmar Oct 21, 2024
72af51c
add group create service
EzzatOmar Oct 21, 2024
11dac30
add groupservice.addUserToGroup method
EzzatOmar Oct 21, 2024
eca91f3
update nest module, remove not needed imports
EzzatOmar Oct 21, 2024
bdb8a35
add entity and do for room member
EzzatOmar Oct 21, 2024
e8bc6a8
add mapper for room member
EzzatOmar Oct 21, 2024
f58d452
add room member repo
EzzatOmar Oct 21, 2024
d633c98
remove old mapper
EzzatOmar Oct 21, 2024
cc7d5b5
add room member rule
EzzatOmar Oct 21, 2024
97eb5a9
add authorization rule to room member
EzzatOmar Oct 21, 2024
ca1eaed
use new structure in room member service
EzzatOmar Oct 21, 2024
e3590ec
put authorization in room uc
EzzatOmar Oct 21, 2024
b6f00b9
reverse group repo exposure
EzzatOmar Oct 21, 2024
0d6143f
Merge branch 'main' into BC-7971-room-members
EzzatOmar Oct 21, 2024
4a9f7c4
fix tests
EzzatOmar Oct 22, 2024
c6fab6b
Merge branch 'main' into BC-7971-room-members
EzzatOmar Oct 22, 2024
da969dc
add missing tests and cleanup
EzzatOmar Oct 23, 2024
100f17d
Merge branch 'main' into BC-7971-room-members
EzzatOmar Oct 23, 2024
0b5dd82
reverse authz order to make sure 404 is returned
EzzatOmar Oct 23, 2024
7835361
Merge branch 'main' into BC-7971-room-members
EzzatOmar Oct 23, 2024
74f4f10
update migration backup
EzzatOmar Oct 23, 2024
23988d4
fix lint err
EzzatOmar Oct 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions apps/server/src/migrations/mikro-orm/Migration202410041210124.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Migration } from '@mikro-orm/migrations-mongodb';

export class Migration202410041210124 extends Migration {
async up(): Promise<void> {
// Add ROOM_VIEWER role
await this.getCollection('roles').insertOne({
name: 'room_viewer',
permissions: ['ROOM_VIEW'],
});
console.info('Added ROOM_VIEWER role with ROOM_VIEW permission');

// Add ROOM_EDITOR role
await this.getCollection('roles').insertOne({
name: 'room_editor',
permissions: ['ROOM_VIEW', 'ROOM_EDIT'],
});
console.info('Added ROOM_EDITOR role with ROOM_VIEW and ROOM_EDIT permissions');
}

async down(): Promise<void> {
// Remove ROOM_VIEWER role
await this.getCollection('roles').deleteOne({ name: 'ROOM_VIEWER' });
console.info('Rollback: Removed ROOM_VIEWER role');

// Remove ROOM_EDITOR role
await this.getCollection('roles').deleteOne({ name: 'ROOM_EDITOR' });
console.info('Rollback: Removed ROOM_EDITOR role');
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export enum GroupTypeResponse {
CLASS = 'class',
COURSE = 'course',
ROOM = 'room',
OTHER = 'other',
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { PeriodResponse } from '../dto/response/period.response';
const typeMapping: Record<GroupTypes, GroupTypeResponse> = {
[GroupTypes.CLASS]: GroupTypeResponse.CLASS,
[GroupTypes.COURSE]: GroupTypeResponse.COURSE,
[GroupTypes.ROOM]: GroupTypeResponse.ROOM,
[GroupTypes.OTHER]: GroupTypeResponse.OTHER,
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW I'm wondering why we aren't using the domain enum for the group type in the response. That should actually be possible and valid IMHO. But that's not a subject of this PR.


Expand Down
1 change: 1 addition & 0 deletions apps/server/src/modules/group/domain/group-types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export enum GroupTypes {
CLASS = 'class',
COURSE = 'course',
ROOM = 'room',
OTHER = 'other',
}
1 change: 1 addition & 0 deletions apps/server/src/modules/group/entity/group.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { GroupValidPeriodEmbeddable } from './group-valid-period.embeddable';
export enum GroupEntityTypes {
CLASS = 'class',
COURSE = 'course',
ROOM = 'room',
OTHER = 'other',
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also similar to the response above we could use the GroupType enum from the domain here. Again maybe not in this PR.

Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/modules/group/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ export { GroupModule } from './group.module';
export { GroupConfig } from './group.config';
export * from './domain';
export { GroupService } from './service';
export * from './repo';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the repo and entity are private, and shall not be exported.

export * from './entity';
2 changes: 2 additions & 0 deletions apps/server/src/modules/group/repo/group-domain.mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ import { GroupEntity, GroupEntityTypes, GroupUserEmbeddable, GroupValidPeriodEmb
const GroupEntityTypesToGroupTypesMapping: Record<GroupEntityTypes, GroupTypes> = {
[GroupEntityTypes.CLASS]: GroupTypes.CLASS,
[GroupEntityTypes.COURSE]: GroupTypes.COURSE,
[GroupEntityTypes.ROOM]: GroupTypes.ROOM,
[GroupEntityTypes.OTHER]: GroupTypes.OTHER,
};

export const GroupTypesToGroupEntityTypesMapping: Record<GroupTypes, GroupEntityTypes> = {
[GroupTypes.CLASS]: GroupEntityTypes.CLASS,
[GroupTypes.COURSE]: GroupEntityTypes.COURSE,
[GroupTypes.ROOM]: GroupEntityTypes.ROOM,
[GroupTypes.OTHER]: GroupEntityTypes.OTHER,
};

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This kind of mapping wouldn't be necessary when we used GroupType enum from domain.

Expand Down
12 changes: 7 additions & 5 deletions apps/server/src/modules/group/repo/group.repo.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,11 +100,12 @@ describe('GroupRepo', () => {
const setup = async () => {
const userEntity: User = userFactory.buildWithId();
const userId: EntityId = userEntity.id;
const groups: GroupEntity[] = groupEntityFactory.buildListWithId(3, {
const groups: GroupEntity[] = groupEntityFactory.buildListWithId(4, {
users: [{ user: userEntity, role: roleFactory.buildWithId() }],
});
groups[1].type = GroupEntityTypes.COURSE;
groups[2].type = GroupEntityTypes.OTHER;
groups[3].type = GroupEntityTypes.ROOM;

const nameQuery = groups[1].name.slice(-3);

Expand Down Expand Up @@ -137,7 +138,6 @@ describe('GroupRepo', () => {

expect(result.total).toEqual(groups.length);
expect(result.data.length).toEqual(1);
expect(result.data[0].id).toEqual(groups[1].id);
});

it('should return groups according to name query', async () => {
Expand All @@ -152,9 +152,11 @@ describe('GroupRepo', () => {
it('should return only groups of the given group types', async () => {
const { userId } = await setup();

const result: Page<Group> = await repo.findGroups({ userId, groupTypes: [GroupTypes.CLASS] });
const resultClass: Page<Group> = await repo.findGroups({ userId, groupTypes: [GroupTypes.CLASS] });
expect(resultClass.data).toEqual([expect.objectContaining<Partial<Group>>({ type: GroupTypes.CLASS })]);

expect(result.data).toEqual([expect.objectContaining<Partial<Group>>({ type: GroupTypes.CLASS })]);
const resultRoom: Page<Group> = await repo.findGroups({ userId, groupTypes: [GroupTypes.ROOM] });
expect(resultRoom.data).toEqual([expect.objectContaining<Partial<Group>>({ type: GroupTypes.ROOM })]);
});
});

Expand Down Expand Up @@ -513,7 +515,7 @@ describe('GroupRepo', () => {

expect(result.total).toEqual(availableGroupsCount);
expect(result.data.length).toEqual(1);
expect(result.data[0].id).toEqual(groups[2].id);
expect(result.data[0].id).toEqual(groups[1].id);
});

it('should return groups according to name query', async () => {
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/modules/group/repo/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './group.repo';
export * from './group-domain.mapper';
37 changes: 37 additions & 0 deletions apps/server/src/modules/room-member/do/room-member.do.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { roleFactory } from '@shared/testing';
import { ObjectId } from 'bson';
import { RoomMember, RoomMemberProps } from './room-member.do';

describe('RoomMember', () => {
describe('a new instance', () => {
const setup = () => {
const userId = new ObjectId();
const roomMemberProps: RoomMemberProps = {
id: '1',
roomId: new ObjectId(),
userGroupId: new ObjectId(),
members: [{ userId, role: roleFactory.build() }],
createdAt: new Date(),
updatedAt: new Date(),
};
const roomMember = new RoomMember(roomMemberProps);
return { roomMember, userId };
};

it('should have can set members', () => {
const { roomMember } = setup();
roomMember.members = [];

expect(roomMember.members).toEqual([]);
});

it('should have can remove members', () => {
const { roomMember, userId } = setup();
roomMember.members = [];

roomMember.removeMember(userId);

expect(roomMember.members).toEqual([]);
});
});
});
53 changes: 53 additions & 0 deletions apps/server/src/modules/room-member/do/room-member.do.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { ObjectId } from '@mikro-orm/mongodb';
import { AuthorizableObject, DomainObject } from '@shared/domain/domain-object';
import { Role } from '@shared/domain/entity';
import { EntityId } from '@shared/domain/types';

export interface RoomMemberProps extends AuthorizableObject {
id: EntityId;
roomId: ObjectId;
userGroupId: ObjectId;
members: { userId: ObjectId; role: Role }[];
createdAt: Date;
updatedAt: Date;
}

export class RoomMember extends DomainObject<RoomMemberProps> {
Metauriel marked this conversation as resolved.
Show resolved Hide resolved
public constructor(props: RoomMemberProps) {
super(props);
}

public getProps(): RoomMemberProps {
// Note: Propagated hotfix. Will be resolved with mikro-orm update. Look at the comment in board-node.do.ts.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const { domainObject, ...copyProps } = this.props;

return copyProps;
}

public get roomId(): ObjectId {
return this.props.roomId;
}

public get userGroupId(): ObjectId {
return this.props.userGroupId;
}

public get members(): RoomMemberProps['members'] {
return this.props.members;
}

public set members(members: RoomMemberProps['members']) {
this.props.members = members;
}

public addMember(userId: ObjectId, role: Role): void {
this.props.members.push({ userId, role });
}

public removeMember(userId: ObjectId): void {
this.props.members = this.props.members.filter((member) => member.userId !== userId);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldnt find any code in the data layer that transforms this back into the usergroup. How do you make sure changes made here are saved into the database?

I guess either you should have an actual reference to a groupDO in here that you update, or the changes made to this array need to be mapped back into the group somewhere.

}
}
7 changes: 7 additions & 0 deletions apps/server/src/modules/room-member/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { RoomMemberEntity } from './repo/entity';
import { RoomMemberRepo } from './repo/room-member.repo';
import { RoomMemberService } from './service/room-member.service';

export * from './do/room-member.do';
export * from './room-member.module';
export { RoomMemberEntity, RoomMemberRepo, RoomMemberService };
1 change: 1 addition & 0 deletions apps/server/src/modules/room-member/repo/entity/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './room-member.entity';
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Entity, Index, Property } from '@mikro-orm/core';
import { ObjectId } from '@mikro-orm/mongodb';
import { AuthorizableObject } from '@shared/domain/domain-object';
import { BaseEntityWithTimestamps } from '@shared/domain/entity/base.entity';
import { EntityId } from '@shared/domain/types';
import { RoomMember } from '../../do/room-member.do';

export interface RoomMemberEntityProps extends AuthorizableObject {
id: EntityId;
roomId: ObjectId;
userGroupId: ObjectId;
createdAt: Date;
updatedAt: Date;
}

@Entity({ tableName: 'room-members' })
export class RoomMemberEntity extends BaseEntityWithTimestamps implements RoomMemberEntityProps {
@Property()
@Index()
roomId!: ObjectId;

@Property()
@Index()
userGroupId!: ObjectId;

@Property({ persist: false })
domainObject: RoomMember | undefined;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { EntityManager, ObjectId } from '@mikro-orm/mongodb';
import { Test, TestingModule } from '@nestjs/testing';
import { RoleName } from '@shared/domain/interface';
import { cleanupCollections, groupEntityFactory, roleFactory, userFactory } from '@shared/testing';
import { MongoMemoryDatabaseModule } from '@src/infra/database';
import { RoomMember } from '../do/room-member.do';
import { roomMemberEntityFactory } from '../testing';
import { RoomMemberEntity } from './entity';
import { RoomMemberDomainMapper } from './room-member-domain.mapper';

describe('RoomMemberDomainMapper', () => {
let module: TestingModule;
let em: EntityManager;

beforeAll(async () => {
module = await Test.createTestingModule({
imports: [MongoMemoryDatabaseModule.forRoot()],
providers: [],
}).compile();

em = module.get(EntityManager);
});

afterAll(async () => {
await module.close();
});

afterEach(async () => {
await cleanupCollections(em);
});

describe('mapEntityToDo', () => {
it('should map RoomMemberEntity to RoomMember domain object', () => {
const user = userFactory.buildWithId();
const role = roleFactory.buildWithId({ name: RoleName.ROOM_EDITOR });
const userGroupEntity = groupEntityFactory.buildWithId({
users: [{ role, user }],
});

const roomMemberEntity = roomMemberEntityFactory.buildWithId({ userGroupId: userGroupEntity.id });

const result = RoomMemberDomainMapper.mapEntityToDo(roomMemberEntity, userGroupEntity.users);
expect(result.id).toEqual(roomMemberEntity.id);
expect(result.userGroupId.id).toEqual(roomMemberEntity.userGroupId.id);
expect(result.members.length).toEqual(1);
expect(result.members[0].userId.toHexString()).toEqual(user.id);
expect(result.members[0].role.id).toEqual(role.id);
expect(result.roomId).toEqual(roomMemberEntity.roomId);
});

it('should return existing domainObject if present', () => {
const user = userFactory.build();
const role = roleFactory.buildWithId({ name: RoleName.ROOM_EDITOR });
const userGroupEntity = groupEntityFactory.buildWithId({
users: [{ role, user }],
});

const roomMemberDo = new RoomMember({
id: '1',
roomId: new ObjectId(),
userGroupId: new ObjectId(userGroupEntity.id),
// eslint-disable-next-line @typescript-eslint/dot-notation
members: RoomMemberDomainMapper['mapGroupUserEmbeddableToMembers'](userGroupEntity.users),
createdAt: new Date('2023-01-01'),
updatedAt: new Date('2023-01-01'),
});

const roomMemberEntity = {
id: '1',
roomId: roomMemberDo.roomId,
userGroupId: new ObjectId(userGroupEntity.id),
createdAt: new Date('2023-01-01'),
updatedAt: new Date('2023-01-01'),
domainObject: roomMemberDo,
} as RoomMemberEntity;

const result = RoomMemberDomainMapper.mapEntityToDo(roomMemberEntity, userGroupEntity.users);

expect(result).toBe(roomMemberDo);
expect(result).toBeInstanceOf(RoomMember);
expect(result.getProps()).toEqual({
id: '1',
roomId: roomMemberDo.roomId,
createdAt: new Date('2023-01-01'),
updatedAt: new Date('2023-01-01'),
userGroupId: new ObjectId(userGroupEntity.id),
members: roomMemberDo.members,
});
});
});

describe('mapDoToEntity', () => {
const setup = () => {
const user = userFactory.build();
const role = roleFactory.buildWithId({ name: RoleName.ROOM_EDITOR });
const userGroupEntity = groupEntityFactory.buildWithId({
users: [{ role, user }],
});
const roomMemberEntity = roomMemberEntityFactory.buildWithId({ userGroupId: userGroupEntity.id });

return { roomMemberEntity, userGroupEntity };
};

it('should convert them to an entity and return it', () => {
const { userGroupEntity, roomMemberEntity } = setup();

// eslint-disable-next-line @typescript-eslint/dot-notation
const members = RoomMemberDomainMapper['mapGroupUserEmbeddableToMembers'](userGroupEntity.users);
const roomMember = new RoomMember({ ...roomMemberEntity, members });
const result = RoomMemberDomainMapper.mapDoToEntity(roomMember);

expect(result).toBeInstanceOf(RoomMemberEntity);
expect(result).toMatchObject(roomMemberEntity);
});

it('should return the entity', () => {
const roomMemberEntity = roomMemberEntityFactory.buildWithId();
const roomMember = new RoomMember({ ...roomMemberEntity, members: [] });
const result = RoomMemberDomainMapper.mapDoToEntity(roomMember);
expect(result.id).toEqual(roomMemberEntity.id);
expect(result.roomId).toEqual(roomMemberEntity.roomId);
expect(result.userGroupId).toEqual(roomMemberEntity.userGroupId);
expect(result.createdAt).toEqual(roomMemberEntity.createdAt);
expect(result.updatedAt).toEqual(roomMemberEntity.updatedAt);
});
});
});
Loading
Loading