diff --git a/apps/server/src/core/auth/services/signup.service.ts b/apps/server/src/core/auth/services/signup.service.ts index a02895c..834e5a4 100644 --- a/apps/server/src/core/auth/services/signup.service.ts +++ b/apps/server/src/core/auth/services/signup.service.ts @@ -8,7 +8,7 @@ import { WorkspaceRepository } from '../../workspace/repositories/workspace.repo import { WorkspaceService } from '../../workspace/services/workspace.service'; import { CreateWorkspaceDto } from '../../workspace/dto/create-workspace.dto'; import { Workspace } from '../../workspace/entities/workspace.entity'; -import { SpaceService } from '../../space/space.service'; +import { SpaceService } from '../../space/services/space.service'; import { CreateAdminUserDto } from '../dto/create-admin-user.dto'; import { GroupUserService } from '../../group/services/group-user.service'; diff --git a/apps/server/src/core/casl/abilities/casl-ability.factory.ts b/apps/server/src/core/casl/abilities/casl-ability.factory.ts index d0862fe..5176450 100644 --- a/apps/server/src/core/casl/abilities/casl-ability.factory.ts +++ b/apps/server/src/core/casl/abilities/casl-ability.factory.ts @@ -15,16 +15,16 @@ import { Group } from '../../group/entities/group.entity'; import { GroupUser } from '../../group/entities/group-user.entity'; import { Attachment } from '../../attachment/entities/attachment.entity'; import { Space } from '../../space/entities/space.entity'; -import { SpaceUser } from '../../space/entities/space-user.entity'; import { Page } from '../../page/entities/page.entity'; import { Comment } from '../../comment/entities/comment.entity'; +import { SpaceMember } from '../../space/entities/space-member.entity'; export type Subjects = | InferSubjects< | typeof Workspace | typeof WorkspaceInvitation | typeof Space - | typeof SpaceUser + | typeof SpaceMember | typeof Group | typeof GroupUser | typeof Attachment diff --git a/apps/server/src/core/group/entities/group.entity.ts b/apps/server/src/core/group/entities/group.entity.ts index afa08e6..54ed32c 100644 --- a/apps/server/src/core/group/entities/group.entity.ts +++ b/apps/server/src/core/group/entities/group.entity.ts @@ -12,7 +12,7 @@ import { GroupUser } from './group-user.entity'; import { Workspace } from '../../workspace/entities/workspace.entity'; import { User } from '../../user/entities/user.entity'; import { Unique } from 'typeorm'; -import { SpaceGroup } from '../../space/entities/space-group.entity'; +import { SpaceMember } from '../../space/entities/space-member.entity'; @Entity('groups') @Unique(['name', 'workspaceId']) @@ -54,8 +54,8 @@ export class Group { @OneToMany(() => GroupUser, (groupUser) => groupUser.group) groupUsers: GroupUser[]; - @OneToMany(() => SpaceGroup, (spaceGroup) => spaceGroup.group) - spaces: SpaceGroup[]; + @OneToMany(() => SpaceMember, (spaceMembership) => spaceMembership.group) + spaces: SpaceMember[]; - userCount?: number; + memberCount?: number; } diff --git a/apps/server/src/core/group/services/group-user.service.ts b/apps/server/src/core/group/services/group-user.service.ts index 2af34ee..a906b39 100644 --- a/apps/server/src/core/group/services/group-user.service.ts +++ b/apps/server/src/core/group/services/group-user.service.ts @@ -89,12 +89,6 @@ export class GroupUserService { throw new NotFoundException('Group not found'); } - const find = await manager.findOne(User, { - where: { id: userId }, - }); - - console.log(find); - const userExists = await manager.exists(User, { where: { id: userId, workspaceId }, }); diff --git a/apps/server/src/core/group/services/group.service.ts b/apps/server/src/core/group/services/group.service.ts index 113ee71..100876a 100644 --- a/apps/server/src/core/group/services/group.service.ts +++ b/apps/server/src/core/group/services/group.service.ts @@ -119,7 +119,7 @@ export class GroupService { .where('group.id = :groupId', { groupId }) .andWhere('group.workspaceId = :workspaceId', { workspaceId }) .loadRelationCountAndMap( - 'group.userCount', + 'group.memberCount', 'group.groupUsers', 'groupUsers', ) @@ -140,7 +140,7 @@ export class GroupService { .createQueryBuilder('group') .where('group.workspaceId = :workspaceId', { workspaceId }) .loadRelationCountAndMap( - 'group.userCount', + 'group.memberCount', 'group.groupUsers', 'groupUsers', ) diff --git a/apps/server/src/core/space/entities/space-group.entity.ts b/apps/server/src/core/space/entities/space-group.entity.ts deleted file mode 100644 index 8fcf5ae..0000000 --- a/apps/server/src/core/space/entities/space-group.entity.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - JoinColumn, - Unique, -} from 'typeorm'; -import { Space } from './space.entity'; -import { Group } from '../../group/entities/group.entity'; - -@Entity('space_groups') -@Unique(['spaceId', 'groupId']) -export class SpaceGroup { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column() - groupId: string; - - @ManyToOne(() => Group, (group) => group.spaces, { - onDelete: 'CASCADE', - }) - @JoinColumn({ name: 'groupId' }) - group: Group; - - @Column() - spaceId: string; - - @ManyToOne(() => Space, (space) => space.spaceGroups, { - onDelete: 'CASCADE', - }) - space: Space; - - @Column({ length: 100, nullable: true }) - role: string; - - @CreateDateColumn() - createdAt: Date; - - @UpdateDateColumn() - updatedAt: Date; -} diff --git a/apps/server/src/core/space/entities/space-member.entity.ts b/apps/server/src/core/space/entities/space-member.entity.ts new file mode 100644 index 0000000..55b2344 --- /dev/null +++ b/apps/server/src/core/space/entities/space-member.entity.ts @@ -0,0 +1,69 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + ManyToOne, + JoinColumn, + Unique, + Check, +} from 'typeorm'; +import { User } from '../../user/entities/user.entity'; +import { Space } from './space.entity'; +import { Group } from '../../group/entities/group.entity'; + +@Entity('space_members') +// allow either userId or groupId +@Check( + 'CHK_allow_userId_or_groupId', + `("userId" IS NOT NULL AND "groupId" IS NULL) OR ("userId" IS NULL AND "groupId" IS NOT NULL)`, +) +@Unique(['spaceId', 'userId']) +@Unique(['spaceId', 'groupId']) +export class SpaceMember { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ nullable: true }) + userId: string; + + @ManyToOne(() => User, (user) => user.spaces, { + onDelete: 'CASCADE', + }) + @JoinColumn({ name: 'userId' }) + user: User; + + @Column({ nullable: true }) + groupId: string; + + @ManyToOne(() => Group, (group) => group.spaces, { + onDelete: 'CASCADE', + }) + @JoinColumn({ name: 'groupId' }) + group: Group; + + @Column() + spaceId: string; + + @ManyToOne(() => Space, (space) => space.spaceMembers, { + onDelete: 'CASCADE', + }) + space: Space; + + @Column({ length: 100 }) + role: string; + + @Column({ nullable: true }) + creatorId: string; + + @ManyToOne(() => User) + @JoinColumn({ name: 'creatorId' }) + creator: User; + + @CreateDateColumn() + createdAt: Date; + + @UpdateDateColumn() + updatedAt: Date; +} diff --git a/apps/server/src/core/space/entities/space-user.entity.ts b/apps/server/src/core/space/entities/space-user.entity.ts deleted file mode 100644 index 4592ae7..0000000 --- a/apps/server/src/core/space/entities/space-user.entity.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - JoinColumn, - Unique, -} from 'typeorm'; -import { User } from '../../user/entities/user.entity'; -import { Space } from './space.entity'; - -@Entity('space_users') -@Unique(['spaceId', 'userId']) -export class SpaceUser { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column() - userId: string; - - @ManyToOne(() => User, (user) => user.spaces, { - onDelete: 'CASCADE', - }) - @JoinColumn({ name: 'userId' }) - user: User; - - @Column() - spaceId: string; - - @ManyToOne(() => Space, (space) => space.spaceUsers, { - onDelete: 'CASCADE', - }) - space: Space; - - @Column({ length: 100, nullable: true }) - role: string; - - @CreateDateColumn() - createdAt: Date; - - @UpdateDateColumn() - updatedAt: Date; -} diff --git a/apps/server/src/core/space/entities/space.entity.ts b/apps/server/src/core/space/entities/space.entity.ts index 35b23a6..8cedeb7 100644 --- a/apps/server/src/core/space/entities/space.entity.ts +++ b/apps/server/src/core/space/entities/space.entity.ts @@ -11,10 +11,9 @@ import { } from 'typeorm'; import { User } from '../../user/entities/user.entity'; import { Workspace } from '../../workspace/entities/workspace.entity'; -import { SpaceUser } from './space-user.entity'; import { Page } from '../../page/entities/page.entity'; import { SpaceVisibility, SpaceRole } from '../../../helpers/types/permission'; -import { SpaceGroup } from './space-group.entity'; +import { SpaceMember } from './space-member.entity'; @Entity('spaces') @Unique(['slug', 'workspaceId']) @@ -56,11 +55,8 @@ export class Space { @JoinColumn({ name: 'workspaceId' }) workspace: Workspace; - @OneToMany(() => SpaceUser, (spaceUser) => spaceUser.space) - spaceUsers: SpaceUser[]; - - @OneToMany(() => SpaceGroup, (spaceGroup) => spaceGroup.space) - spaceGroups: SpaceGroup[]; + @OneToMany(() => SpaceMember, (spaceMember) => spaceMember.space) + spaceMembers: SpaceMember[]; @OneToMany(() => Page, (page) => page.space) pages: Page[]; diff --git a/apps/server/src/core/space/repositories/space-group.repository.ts b/apps/server/src/core/space/repositories/space-group.repository.ts deleted file mode 100644 index f7f52df..0000000 --- a/apps/server/src/core/space/repositories/space-group.repository.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { DataSource, Repository } from 'typeorm'; -import { SpaceGroup } from '../entities/space-group.entity'; - -@Injectable() -export class SpaceGroupRepository extends Repository { - constructor(private dataSource: DataSource) { - super(SpaceGroup, dataSource.createEntityManager()); - } -} diff --git a/apps/server/src/core/space/repositories/space-member.repository.ts b/apps/server/src/core/space/repositories/space-member.repository.ts new file mode 100644 index 0000000..34841a6 --- /dev/null +++ b/apps/server/src/core/space/repositories/space-member.repository.ts @@ -0,0 +1,10 @@ +import { Injectable } from '@nestjs/common'; +import { DataSource, Repository } from 'typeorm'; +import { SpaceMember } from '../entities/space-member.entity'; + +@Injectable() +export class SpaceMemberRepository extends Repository { + constructor(private dataSource: DataSource) { + super(SpaceMember, dataSource.createEntityManager()); + } +} diff --git a/apps/server/src/core/space/repositories/space-user.repository.ts b/apps/server/src/core/space/repositories/space-user.repository.ts deleted file mode 100644 index 5b90c7d..0000000 --- a/apps/server/src/core/space/repositories/space-user.repository.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { DataSource, Repository } from 'typeorm'; -import { SpaceUser } from '../entities/space-user.entity'; - -@Injectable() -export class SpaceUserRepository extends Repository { - constructor(private dataSource: DataSource) { - super(SpaceUser, dataSource.createEntityManager()); - } -} diff --git a/apps/server/src/core/space/services/space-member.service.ts b/apps/server/src/core/space/services/space-member.service.ts new file mode 100644 index 0000000..068cc35 --- /dev/null +++ b/apps/server/src/core/space/services/space-member.service.ts @@ -0,0 +1,230 @@ +import { + BadRequestException, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { SpaceRepository } from '../repositories/space.repository'; +import { transactionWrapper } from '../../../helpers/db.helper'; +import { DataSource, EntityManager, IsNull, Not } from 'typeorm'; +import { User } from '../../user/entities/user.entity'; +import { PaginationOptions } from '../../../helpers/pagination/pagination-options'; +import { PaginationMetaDto } from '../../../helpers/pagination/pagination-meta-dto'; +import { PaginatedResult } from '../../../helpers/pagination/paginated-result'; +import { Group } from '../../group/entities/group.entity'; +import { SpaceMemberRepository } from '../repositories/space-member.repository'; +import { SpaceMember } from '../entities/space-member.entity'; + +@Injectable() +export class SpaceMemberService { + constructor( + private spaceRepository: SpaceRepository, + private spaceMemberRepository: SpaceMemberRepository, + private dataSource: DataSource, + ) {} + + async addUserToSpace( + userId: string, + spaceId: string, + role: string, + workspaceId, + manager?: EntityManager, + ): Promise { + return await transactionWrapper( + async (manager: EntityManager) => { + const userExists = await manager.exists(User, { + where: { id: userId, workspaceId }, + }); + if (!userExists) { + throw new NotFoundException('User not found'); + } + + const existingSpaceUser = await manager.findOneBy(SpaceMember, { + userId: userId, + spaceId: spaceId, + }); + + if (existingSpaceUser) { + throw new BadRequestException('User already added to this space'); + } + + const spaceMember = new SpaceMember(); + spaceMember.userId = userId; + spaceMember.spaceId = spaceId; + spaceMember.role = role; + await manager.save(spaceMember); + + return spaceMember; + }, + this.dataSource, + manager, + ); + } + + async getUserSpaces( + userId: string, + workspaceId: string, + paginationOptions: PaginationOptions, + ) { + const [userSpaces, count] = await this.spaceMemberRepository + .createQueryBuilder('spaceMember') + .leftJoinAndSelect('spaceMember.space', 'space') + .where('spaceMember.userId = :userId', { userId }) + .andWhere('space.workspaceId = :workspaceId', { workspaceId }) + .loadRelationCountAndMap( + 'space.memberCount', + 'space.spaceMembers', + 'spaceMembers', + ) + .take(paginationOptions.limit) + .skip(paginationOptions.skip) + .getManyAndCount(); + + /* + const getUserSpacesViaGroup = this.spaceRepository + .createQueryBuilder('space') + .leftJoin('space.spaceGroups', 'spaceGroup') + .leftJoin('spaceGroup.group', 'group') + .leftJoin('group.groupUsers', 'groupUser') + .where('groupUser.userId = :userId', { userId }) + .andWhere('space.workspaceId = :workspaceId', { workspaceId }) + .getManyAndCount(); + + console.log(await getUserSpacesViaGroup); +*/ + + const spaces = userSpaces.map((userSpace) => userSpace.space); + + const paginationMeta = new PaginationMetaDto({ count, paginationOptions }); + return new PaginatedResult(spaces, paginationMeta); + } + + async getSpaceMembers( + spaceId: string, + workspaceId: string, + paginationOptions: PaginationOptions, + ) { + const [spaceMembers, count] = await this.spaceMemberRepository.findAndCount( + { + relations: ['user', 'group'], + where: { + space: { + id: spaceId, + workspaceId, + }, + }, + order: { + createdAt: 'ASC', + }, + take: paginationOptions.limit, + skip: paginationOptions.skip, + }, + ); + + const members = await Promise.all( + spaceMembers.map(async (member) => { + let memberInfo = {}; + + if (member.user) { + memberInfo = { + id: member.user.id, + name: member.user.name, + email: member.user.email, + avatarUrl: member.user.avatarUrl, + type: 'user', + }; + } else if (member.group) { + const memberCount = await this.dataSource.getRepository(Group).count({ + where: { + id: member.groupId, + workspaceId, + }, + }); + + memberInfo = { + id: member.group.id, + name: member.group.name, + isDefault: member.group.isDefault, + memberCount: memberCount, + type: 'group', + }; + } + + return { + ...memberInfo, + role: member.role, + }; + }), + ); + + const paginationMeta = new PaginationMetaDto({ count, paginationOptions }); + return new PaginatedResult(members, paginationMeta); + } + + async addGroupToSpace( + groupId: string, + spaceId: string, + role: string, + workspaceId, + manager?: EntityManager, + ): Promise { + return await transactionWrapper( + async (manager: EntityManager) => { + const groupExists = await manager.exists(Group, { + where: { id: groupId, workspaceId }, + }); + if (!groupExists) { + throw new NotFoundException('Group not found'); + } + + const existingSpaceGroup = await manager.findOneBy(SpaceMember, { + groupId: groupId, + spaceId: spaceId, + }); + + if (existingSpaceGroup) { + throw new BadRequestException('Group already added to this space'); + } + + const spaceMember = new SpaceMember(); + spaceMember.groupId = groupId; + spaceMember.spaceId = spaceId; + spaceMember.role = role; + await manager.save(spaceMember); + + return spaceMember; + }, + this.dataSource, + manager, + ); + } + + async getSpaceGroup( + spaceId: string, + workspaceId: string, + paginationOptions: PaginationOptions, + ) { + const [spaceGroups, count] = await this.spaceMemberRepository.findAndCount({ + relations: ['group'], + where: { + groupId: Not(IsNull()), + space: { + id: spaceId, + workspaceId, + }, + }, + take: paginationOptions.limit, + skip: paginationOptions.skip, + }); + + // TODO: add group memberCount + const groups = spaceGroups.map((spaceGroup) => { + return { + ...spaceGroup.group, + spaceRole: spaceGroup.role, + }; + }); + + const paginationMeta = new PaginationMetaDto({ count, paginationOptions }); + return new PaginatedResult(groups, paginationMeta); + } +} diff --git a/apps/server/src/core/space/space.service.spec.ts b/apps/server/src/core/space/services/space.service.spec.ts similarity index 100% rename from apps/server/src/core/space/space.service.spec.ts rename to apps/server/src/core/space/services/space.service.spec.ts diff --git a/apps/server/src/core/space/services/space.service.ts b/apps/server/src/core/space/services/space.service.ts new file mode 100644 index 0000000..151baf5 --- /dev/null +++ b/apps/server/src/core/space/services/space.service.ts @@ -0,0 +1,84 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { CreateSpaceDto } from '../dto/create-space.dto'; +import { Space } from '../entities/space.entity'; +import { SpaceRepository } from '../repositories/space.repository'; +import { transactionWrapper } from '../../../helpers/db.helper'; +import { DataSource, EntityManager } from 'typeorm'; +import { PaginationOptions } from '../../../helpers/pagination/pagination-options'; +import { PaginationMetaDto } from '../../../helpers/pagination/pagination-meta-dto'; +import { PaginatedResult } from '../../../helpers/pagination/paginated-result'; +import { SpaceMemberRepository } from '../repositories/space-member.repository'; +import slugify from 'slugify'; + +@Injectable() +export class SpaceService { + constructor( + private spaceRepository: SpaceRepository, + private spaceMemberRepository: SpaceMemberRepository, + private dataSource: DataSource, + ) {} + + async create( + userId: string, + workspaceId: string, + createSpaceDto?: CreateSpaceDto, + manager?: EntityManager, + ): Promise { + return await transactionWrapper( + async (manager: EntityManager) => { + const space = new Space(); + space.name = createSpaceDto.name ?? 'untitled space '; + space.description = createSpaceDto.description ?? ''; + space.creatorId = userId; + space.workspaceId = workspaceId; + + space.slug = slugify(space.name.toLowerCase()); // TODO: check for duplicate + + await manager.save(space); + return space; + }, + this.dataSource, + manager, + ); + } + + async getSpaceInfo(spaceId: string, workspaceId: string): Promise { + const space = await this.spaceRepository + .createQueryBuilder('space') + .where('space.id = :spaceId', { spaceId }) + .andWhere('space.workspaceId = :workspaceId', { workspaceId }) + .loadRelationCountAndMap( + 'space.memberCount', + 'space.spaceMembers', + 'spaceMembers', + ) // TODO: add groups to memberCount + .getOne(); + + if (!space) { + throw new NotFoundException('Space not found'); + } + + return space; + } + + async getWorkspaceSpaces( + workspaceId: string, + paginationOptions: PaginationOptions, + ): Promise> { + const [spaces, count] = await this.spaceRepository + .createQueryBuilder('space') + .where('space.workspaceId = :workspaceId', { workspaceId }) + .loadRelationCountAndMap( + 'space.memberCount', + 'space.spaceMembers', + 'spaceMembers', + ) // TODO: add groups to memberCount + .take(paginationOptions.limit) + .skip(paginationOptions.skip) + .getManyAndCount(); + + const paginationMeta = new PaginationMetaDto({ count, paginationOptions }); + + return new PaginatedResult(spaces, paginationMeta); + } +} diff --git a/apps/server/src/core/space/space.controller.spec.ts b/apps/server/src/core/space/space.controller.spec.ts index 0402b23..4e7b9f8 100644 --- a/apps/server/src/core/space/space.controller.spec.ts +++ b/apps/server/src/core/space/space.controller.spec.ts @@ -1,6 +1,6 @@ import { Test, TestingModule } from '@nestjs/testing'; import { SpaceController } from './space.controller'; -import { SpaceService } from './space.service'; +import { SpaceService } from './services/space.service'; describe('SpaceController', () => { let controller: SpaceController; diff --git a/apps/server/src/core/space/space.controller.ts b/apps/server/src/core/space/space.controller.ts index 7b67707..60e49b4 100644 --- a/apps/server/src/core/space/space.controller.ts +++ b/apps/server/src/core/space/space.controller.ts @@ -6,7 +6,7 @@ import { Post, UseGuards, } from '@nestjs/common'; -import { SpaceService } from './space.service'; +import { SpaceService } from './services/space.service'; import { AuthUser } from '../../decorators/auth-user.decorator'; import { User } from '../user/entities/user.entity'; import { AuthWorkspace } from '../../decorators/auth-workspace.decorator'; @@ -14,11 +14,15 @@ import { Workspace } from '../workspace/entities/workspace.entity'; import { JwtAuthGuard } from '../../guards/jwt-auth.guard'; import { SpaceIdDto } from './dto/space-id.dto'; import { PaginationOptions } from '../../helpers/pagination/pagination-options'; +import { SpaceMemberService } from './services/space-member.service'; @UseGuards(JwtAuthGuard) @Controller('spaces') export class SpaceController { - constructor(private readonly spaceService: SpaceService) {} + constructor( + private readonly spaceService: SpaceService, + private readonly spaceMemberService: SpaceMemberService, + ) {} @HttpCode(HttpStatus.OK) @Post('/') @@ -41,7 +45,11 @@ export class SpaceController { @AuthUser() user: User, @AuthWorkspace() workspace: Workspace, ) { - return this.spaceService.getUserSpaces(user.id, workspace.id, pagination); + return this.spaceMemberService.getUserSpaces( + user.id, + workspace.id, + pagination, + ); } @HttpCode(HttpStatus.OK) @@ -64,7 +72,7 @@ export class SpaceController { @AuthUser() user: User, @AuthWorkspace() workspace: Workspace, ) { - return this.spaceService.getSpaceUsers( + return this.spaceMemberService.getSpaceMembers( spaceIdDto.spaceId, workspace.id, pagination, diff --git a/apps/server/src/core/space/space.module.ts b/apps/server/src/core/space/space.module.ts index f21ffdf..3a502d6 100644 --- a/apps/server/src/core/space/space.module.ts +++ b/apps/server/src/core/space/space.module.ts @@ -1,23 +1,22 @@ import { Module } from '@nestjs/common'; -import { SpaceService } from './space.service'; +import { SpaceService } from './services/space.service'; import { SpaceController } from './space.controller'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Space } from './entities/space.entity'; -import { SpaceUser } from './entities/space-user.entity'; import { SpaceRepository } from './repositories/space.repository'; -import { SpaceUserRepository } from './repositories/space-user.repository'; -import { SpaceGroup } from './entities/space-group.entity'; -import { SpaceGroupRepository } from './repositories/space-group.repository'; +import { SpaceMember } from './entities/space-member.entity'; +import { SpaceMemberRepository } from './repositories/space-member.repository'; +import { SpaceMemberService } from './services/space-member.service'; @Module({ - imports: [TypeOrmModule.forFeature([Space, SpaceUser, SpaceGroup])], + imports: [TypeOrmModule.forFeature([Space, SpaceMember])], controllers: [SpaceController], providers: [ SpaceService, + SpaceMemberService, SpaceRepository, - SpaceUserRepository, - SpaceGroupRepository, + SpaceMemberRepository, ], - exports: [SpaceService], + exports: [SpaceService, SpaceMemberService], }) export class SpaceModule {} diff --git a/apps/server/src/core/space/space.service.ts b/apps/server/src/core/space/space.service.ts deleted file mode 100644 index b07f386..0000000 --- a/apps/server/src/core/space/space.service.ts +++ /dev/null @@ -1,263 +0,0 @@ -import { - BadRequestException, - Injectable, - NotFoundException, -} from '@nestjs/common'; -import { CreateSpaceDto } from './dto/create-space.dto'; -import { Space } from './entities/space.entity'; -import { SpaceRepository } from './repositories/space.repository'; -import { SpaceUserRepository } from './repositories/space-user.repository'; -import { SpaceUser } from './entities/space-user.entity'; -import { transactionWrapper } from '../../helpers/db.helper'; -import { DataSource, EntityManager } from 'typeorm'; -import { User } from '../user/entities/user.entity'; -import { PaginationOptions } from '../../helpers/pagination/pagination-options'; -import { PaginationMetaDto } from '../../helpers/pagination/pagination-meta-dto'; -import { PaginatedResult } from '../../helpers/pagination/paginated-result'; -import { SpaceGroupRepository } from './repositories/space-group.repository'; -import { Group } from '../group/entities/group.entity'; -import { SpaceGroup } from './entities/space-group.entity'; - -@Injectable() -export class SpaceService { - constructor( - private spaceRepository: SpaceRepository, - private spaceUserRepository: SpaceUserRepository, - private spaceGroupRepository: SpaceGroupRepository, - private dataSource: DataSource, - ) {} - - async create( - userId: string, - workspaceId: string, - createSpaceDto?: CreateSpaceDto, - manager?: EntityManager, - ): Promise { - return await transactionWrapper( - async (manager: EntityManager) => { - const space = new Space(); - space.name = createSpaceDto.name ?? 'untitled space '; - space.description = createSpaceDto.description ?? ''; - space.creatorId = userId; - space.workspaceId = workspaceId; - - space.slug = space.name.toLowerCase(); // TODO: fix - - await manager.save(space); - return space; - }, - this.dataSource, - manager, - ); - } - - async addUserToSpace( - userId: string, - spaceId: string, - role: string, - workspaceId, - manager?: EntityManager, - ): Promise { - return await transactionWrapper( - async (manager: EntityManager) => { - const userExists = await manager.exists(User, { - where: { id: userId, workspaceId }, - }); - if (!userExists) { - throw new NotFoundException('User not found'); - } - - const existingSpaceUser = await manager.findOneBy(SpaceUser, { - userId: userId, - spaceId: spaceId, - }); - - if (existingSpaceUser) { - throw new BadRequestException('User already added to this space'); - } - - const spaceUser = new SpaceUser(); - spaceUser.userId = userId; - spaceUser.spaceId = spaceId; - spaceUser.role = role; - await manager.save(spaceUser); - - return spaceUser; - }, - this.dataSource, - manager, - ); - } - - async getSpaceInfo(spaceId: string, workspaceId: string): Promise { - const space = await this.spaceRepository - .createQueryBuilder('space') - .where('space.id = :spaceId', { spaceId }) - .andWhere('space.workspaceId = :workspaceId', { workspaceId }) - .loadRelationCountAndMap( - 'space.userCount', - 'space.spaceUsers', - 'spaceUsers', - ) // TODO: add groups to userCount - .getOne(); - - if (!space) { - throw new NotFoundException('Space not found'); - } - - return space; - } - - async getWorkspaceSpaces( - workspaceId: string, - paginationOptions: PaginationOptions, - ): Promise> { - const [spaces, count] = await this.spaceRepository - .createQueryBuilder('space') - .where('space.workspaceId = :workspaceId', { workspaceId }) - .loadRelationCountAndMap( - 'space.userCount', - 'space.spaceUsers', - 'spaceUsers', - ) // TODO: add groups to userCount - .take(paginationOptions.limit) - .skip(paginationOptions.skip) - .getManyAndCount(); - - const paginationMeta = new PaginationMetaDto({ count, paginationOptions }); - - return new PaginatedResult(spaces, paginationMeta); - } - - async getUserSpaces( - userId: string, - workspaceId: string, - paginationOptions: PaginationOptions, - ) { - const [userSpaces, count] = await this.spaceUserRepository - .createQueryBuilder('spaceUser') - .leftJoinAndSelect('spaceUser.space', 'space') - .where('spaceUser.userId = :userId', { userId }) - .andWhere('space.workspaceId = :workspaceId', { workspaceId }) - .loadRelationCountAndMap( - 'space.userCount', - 'space.spaceUsers', - 'spaceUsers', - ) - .take(paginationOptions.limit) - .skip(paginationOptions.skip) - .getManyAndCount(); - - - const getUserSpacesViaGroup = this.spaceRepository - .createQueryBuilder('space') - .leftJoin('space.spaceGroups', 'spaceGroup') - .leftJoin('spaceGroup.group', 'group') - .leftJoin('group.groupUsers', 'groupUser') - .where('groupUser.userId = :userId', { userId }) - .andWhere('space.workspaceId = :workspaceId', { workspaceId }).getManyAndCount(); - - console.log(await getUserSpacesViaGroup); - - const spaces = userSpaces.map((userSpace) => userSpace.space); - - const paginationMeta = new PaginationMetaDto({ count, paginationOptions }); - return new PaginatedResult(spaces, paginationMeta); - } - - async getSpaceUsers( - spaceId: string, - workspaceId: string, - paginationOptions: PaginationOptions, - ) { - const [spaceUsers, count] = await this.spaceUserRepository.findAndCount({ - relations: ['user'], - where: { - space: { - id: spaceId, - workspaceId, - }, - }, - take: paginationOptions.limit, - skip: paginationOptions.skip, - }); - - const users = spaceUsers.map((spaceUser) => { - delete spaceUser.user.password; - return { - ...spaceUser.user, - spaceRole: spaceUser.role, - }; - }); - - const paginationMeta = new PaginationMetaDto({ count, paginationOptions }); - return new PaginatedResult(users, paginationMeta); - } - - async addGroupToSpace( - groupId: string, - spaceId: string, - role: string, - workspaceId, - manager?: EntityManager, - ): Promise { - return await transactionWrapper( - async (manager: EntityManager) => { - const groupExists = await manager.exists(Group, { - where: { id: groupId, workspaceId }, - }); - if (!groupExists) { - throw new NotFoundException('Group not found'); - } - - const existingSpaceGroup = await manager.findOneBy(SpaceGroup, { - groupId: groupId, - spaceId: spaceId, - }); - - if (existingSpaceGroup) { - throw new BadRequestException('Group already added to this space'); - } - - const spaceGroup = new SpaceGroup(); - spaceGroup.groupId = groupId; - spaceGroup.spaceId = spaceId; - spaceGroup.role = role; - await manager.save(spaceGroup); - - return spaceGroup; - }, - this.dataSource, - manager, - ); - } - - async getSpaceGroups( - spaceId: string, - workspaceId: string, - paginationOptions: PaginationOptions, - ) { - const [spaceGroups, count] = await this.spaceGroupRepository.findAndCount({ - relations: ['group'], - where: { - space: { - id: spaceId, - workspaceId, - }, - }, - take: paginationOptions.limit, - skip: paginationOptions.skip, - }); - - // TODO: add group userCount - const groups = spaceGroups.map((spaceGroup) => { - return { - ...spaceGroup.group, - spaceRole: spaceGroup.role, - }; - }); - - const paginationMeta = new PaginationMetaDto({ count, paginationOptions }); - return new PaginatedResult(groups, paginationMeta); - } -} diff --git a/apps/server/src/core/user/entities/user.entity.ts b/apps/server/src/core/user/entities/user.entity.ts index 67e6e41..803b8ab 100644 --- a/apps/server/src/core/user/entities/user.entity.ts +++ b/apps/server/src/core/user/entities/user.entity.ts @@ -14,7 +14,7 @@ import { Workspace } from '../../workspace/entities/workspace.entity'; import { Page } from '../../page/entities/page.entity'; import { Comment } from '../../comment/entities/comment.entity'; import { Space } from '../../space/entities/space.entity'; -import { SpaceUser } from '../../space/entities/space-user.entity'; +import { SpaceMember } from '../../space/entities/space-member.entity'; @Entity('users') @Unique(['email', 'workspaceId']) @@ -78,8 +78,8 @@ export class User { @OneToMany(() => Space, (space) => space.creator) createdSpaces: Space[]; - @OneToMany(() => SpaceUser, (spaceUser) => spaceUser.user) - spaces: SpaceUser[]; + @OneToMany(() => SpaceMember, (spaceMembership) => spaceMembership.user) + spaces: SpaceMember[]; toJSON() { delete this.password; diff --git a/apps/server/src/core/workspace/services/workspace.service.ts b/apps/server/src/core/workspace/services/workspace.service.ts index 50015cd..7fcc49e 100644 --- a/apps/server/src/core/workspace/services/workspace.service.ts +++ b/apps/server/src/core/workspace/services/workspace.service.ts @@ -9,7 +9,7 @@ import { Workspace } from '../entities/workspace.entity'; import { v4 as uuidv4 } from 'uuid'; import { UpdateWorkspaceDto } from '../dto/update-workspace.dto'; import { DeleteWorkspaceDto } from '../dto/delete-workspace.dto'; -import { SpaceService } from '../../space/space.service'; +import { SpaceService } from '../../space/services/space.service'; import { DataSource, EntityManager } from 'typeorm'; import { transactionWrapper } from '../../../helpers/db.helper'; import { CreateSpaceDto } from '../../space/dto/create-space.dto'; @@ -19,6 +19,7 @@ import { User } from '../../user/entities/user.entity'; import { EnvironmentService } from '../../../integrations/environment/environment.service'; import { GroupService } from '../../group/services/group.service'; import { GroupUserService } from '../../group/services/group-user.service'; +import { SpaceMemberService } from '../../space/services/space-member.service'; @Injectable() export class WorkspaceService { @@ -26,6 +27,7 @@ export class WorkspaceService { private workspaceRepository: WorkspaceRepository, private userRepository: UserRepository, private spaceService: SpaceService, + private spaceMemberService: SpaceMemberService, private groupService: GroupService, private groupUserService: GroupUserService, private environmentService: EnvironmentService, @@ -42,7 +44,7 @@ export class WorkspaceService { .createQueryBuilder('workspace') .where('workspace.id = :workspaceId', { workspaceId }) .loadRelationCountAndMap( - 'workspace.userCount', + 'workspace.memberCount', 'workspace.users', 'workspaceUsers', ) @@ -105,7 +107,7 @@ export class WorkspaceService { ); // and add user to space as owner - await this.spaceService.addUserToSpace( + await this.spaceMemberService.addUserToSpace( user.id, createdSpace.id, SpaceRole.OWNER, @@ -114,7 +116,7 @@ export class WorkspaceService { ); // add default group to space as writer - await this.spaceService.addGroupToSpace( + await this.spaceMemberService.addGroupToSpace( group.id, createdSpace.id, SpaceRole.WRITER, diff --git a/apps/server/src/database/migrations/1708941651476-AddSpacesUsers.ts b/apps/server/src/database/migrations/1708941651476-AddSpacesUsers.ts deleted file mode 100644 index 1e843c6..0000000 --- a/apps/server/src/database/migrations/1708941651476-AddSpacesUsers.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -export class AddSpacesUsers1708941651476 implements MigrationInterface { - name = 'AddSpacesUsers1708941651476' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`CREATE TABLE "space_users" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "userId" uuid NOT NULL, "spaceId" uuid NOT NULL, "role" character varying(100), "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "UQ_5819a4f6b83e86596c57c19e39f" UNIQUE ("spaceId", "userId"), CONSTRAINT "PK_8d03fbe7f6bc26f9ac665250e1d" PRIMARY KEY ("id"))`); - await queryRunner.query(`ALTER TABLE "space_users" ADD CONSTRAINT "FK_e735cdb3781f344a2dff3083fd5" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "space_users" ADD CONSTRAINT "FK_dae4f7e55306bdcec6ac8f602c1" FOREIGN KEY ("spaceId") REFERENCES "spaces"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "space_users" DROP CONSTRAINT "FK_dae4f7e55306bdcec6ac8f602c1"`); - await queryRunner.query(`ALTER TABLE "space_users" DROP CONSTRAINT "FK_e735cdb3781f344a2dff3083fd5"`); - await queryRunner.query(`DROP TABLE "space_users"`); - } - -} diff --git a/apps/server/src/database/migrations/1710892343941-SpaceGroupsMembership.ts b/apps/server/src/database/migrations/1710892343941-SpaceGroupsMembership.ts deleted file mode 100644 index d018ef2..0000000 --- a/apps/server/src/database/migrations/1710892343941-SpaceGroupsMembership.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -export class SpaceGroupsMembership1710892343941 implements MigrationInterface { - name = 'SpaceGroupsMembership1710892343941' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`CREATE TABLE "space_groups" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "groupId" uuid NOT NULL, "spaceId" uuid NOT NULL, "role" character varying(100), "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "UQ_68e59d7b983dfefc7d33febe4c3" UNIQUE ("spaceId", "groupId"), CONSTRAINT "PK_31f9b87a8dced378cb68f04836b" PRIMARY KEY ("id"))`); - await queryRunner.query(`ALTER TABLE "space_groups" ADD CONSTRAINT "FK_b3950d22b51148de9e14a1e5020" FOREIGN KEY ("groupId") REFERENCES "groups"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "space_groups" ADD CONSTRAINT "FK_80567cbf54af9e8e8ec469d247d" FOREIGN KEY ("spaceId") REFERENCES "spaces"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "space_groups" DROP CONSTRAINT "FK_80567cbf54af9e8e8ec469d247d"`); - await queryRunner.query(`ALTER TABLE "space_groups" DROP CONSTRAINT "FK_b3950d22b51148de9e14a1e5020"`); - await queryRunner.query(`DROP TABLE "space_groups"`); - } - -} diff --git a/apps/server/src/database/migrations/1711054895950-PolymorphicSpaceMembers.ts b/apps/server/src/database/migrations/1711054895950-PolymorphicSpaceMembers.ts new file mode 100644 index 0000000..50e0466 --- /dev/null +++ b/apps/server/src/database/migrations/1711054895950-PolymorphicSpaceMembers.ts @@ -0,0 +1,22 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class PolymorphicSpaceMembers1711054895950 implements MigrationInterface { + name = 'PolymorphicSpaceMembers1711054895950' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE "space_members" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "userId" uuid, "groupId" uuid, "spaceId" uuid NOT NULL, "role" character varying(100) NOT NULL, "creatorId" uuid, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "UQ_07add45942b705c4b8c6c88013d" UNIQUE ("spaceId", "groupId"), CONSTRAINT "UQ_e91b442c5a1c7aa13c767c88363" UNIQUE ("spaceId", "userId"), CONSTRAINT "PK_5aaa6440d7f1e8b8c051df43d5e" PRIMARY KEY ("id"))`); + await queryRunner.query(`ALTER TABLE "space_members" ADD CONSTRAINT "FK_6b3b64db93d9a721ff7005eb6a3" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "space_members" ADD CONSTRAINT "FK_1677eab7e3f6602e13ca23418f5" FOREIGN KEY ("groupId") REFERENCES "groups"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "space_members" ADD CONSTRAINT "FK_25571cab1e221c0278499f4e801" FOREIGN KEY ("spaceId") REFERENCES "spaces"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "space_members" ADD CONSTRAINT "FK_63ce441685d52339875a4a33b7e" FOREIGN KEY ("creatorId") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "space_members" DROP CONSTRAINT "FK_63ce441685d52339875a4a33b7e"`); + await queryRunner.query(`ALTER TABLE "space_members" DROP CONSTRAINT "FK_25571cab1e221c0278499f4e801"`); + await queryRunner.query(`ALTER TABLE "space_members" DROP CONSTRAINT "FK_1677eab7e3f6602e13ca23418f5"`); + await queryRunner.query(`ALTER TABLE "space_members" DROP CONSTRAINT "FK_6b3b64db93d9a721ff7005eb6a3"`); + await queryRunner.query(`DROP TABLE "space_members"`); + } + +} diff --git a/apps/server/src/database/migrations/1711059108729-SpaceMemberEntityConstraint.ts b/apps/server/src/database/migrations/1711059108729-SpaceMemberEntityConstraint.ts new file mode 100644 index 0000000..8b716cf --- /dev/null +++ b/apps/server/src/database/migrations/1711059108729-SpaceMemberEntityConstraint.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class SpaceMemberEntityConstraint1711059108729 implements MigrationInterface { + name = 'SpaceMemberEntityConstraint1711059108729' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "space_members" ADD CONSTRAINT "CHK_allow_userId_or_groupId" CHECK (("userId" IS NOT NULL AND "groupId" IS NULL) OR ("userId" IS NULL AND "groupId" IS NOT NULL))`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "space_members" DROP CONSTRAINT "CHK_allow_userId_or_groupId"`); + } + +}