From 51b9808382051bffa1b8fa96e83f21cf20d86cf5 Mon Sep 17 00:00:00 2001 From: Philipinho <16838612+Philipinho@users.noreply.github.com> Date: Wed, 20 Mar 2024 01:26:03 +0000 Subject: [PATCH] Implement Space membership by group * Add all users to default group * Fixes and updates --- apps/server/src/core/auth/auth.module.ts | 2 + .../src/core/auth/services/auth.service.ts | 2 +- .../src/core/auth/services/signup.service.ts | 25 ++++- .../src/core/group/dto/create-group.dto.ts | 4 + .../core/group/entities/group-user.entity.ts | 2 +- .../src/core/group/entities/group.entity.ts | 11 ++- .../server/src/core/group/group.controller.ts | 3 +- apps/server/src/core/group/group.module.ts | 1 + .../core/group/services/group-user.service.ts | 78 +++++++++------ .../src/core/group/services/group.service.ts | 97 +++++++++++++++++-- .../core/space/entities/space-group.entity.ts | 45 +++++++++ .../src/core/space/entities/space.entity.ts | 6 +- .../repositories/space-group.repository.ts | 10 ++ .../server/src/core/space/space.controller.ts | 1 + apps/server/src/core/space/space.module.ts | 13 ++- apps/server/src/core/space/space.service.ts | 75 +++++++++++++- .../src/core/user/entities/user.entity.ts | 8 +- .../workspace/services/workspace.service.ts | 63 ++++++------ .../src/core/workspace/workspace.module.ts | 5 +- .../1710886360227-AddDefaultGroup.ts | 22 +++++ .../1710892343941-SpaceGroupsMembership.ts | 18 ++++ ...1710894465616-AddWorkspaceCascadeToUser.ts | 16 +++ 22 files changed, 425 insertions(+), 82 deletions(-) create mode 100644 apps/server/src/core/space/entities/space-group.entity.ts create mode 100644 apps/server/src/core/space/repositories/space-group.repository.ts create mode 100644 apps/server/src/database/migrations/1710886360227-AddDefaultGroup.ts create mode 100644 apps/server/src/database/migrations/1710892343941-SpaceGroupsMembership.ts create mode 100644 apps/server/src/database/migrations/1710894465616-AddWorkspaceCascadeToUser.ts diff --git a/apps/server/src/core/auth/auth.module.ts b/apps/server/src/core/auth/auth.module.ts index 5f2fcff..4716721 100644 --- a/apps/server/src/core/auth/auth.module.ts +++ b/apps/server/src/core/auth/auth.module.ts @@ -9,6 +9,7 @@ import { WorkspaceModule } from '../workspace/workspace.module'; import { SignupService } from './services/signup.service'; import { UserModule } from '../user/user.module'; import { SpaceModule } from '../space/space.module'; +import { GroupModule } from '../group/group.module'; @Module({ imports: [ @@ -26,6 +27,7 @@ import { SpaceModule } from '../space/space.module'; UserModule, WorkspaceModule, SpaceModule, + GroupModule, ], controllers: [AuthController], providers: [AuthService, SignupService, TokenService, JwtStrategy], diff --git a/apps/server/src/core/auth/services/auth.service.ts b/apps/server/src/core/auth/services/auth.service.ts index 06bbc61..8d9b61c 100644 --- a/apps/server/src/core/auth/services/auth.service.ts +++ b/apps/server/src/core/auth/services/auth.service.ts @@ -51,7 +51,7 @@ export class AuthService { } async setup(createAdminUserDto: CreateAdminUserDto) { - const user: User = await this.signupService.firstSetup(createAdminUserDto); + const user: User = await this.signupService.initialSetup(createAdminUserDto); const tokens: TokensDto = await this.tokenService.generateTokens(user); diff --git a/apps/server/src/core/auth/services/signup.service.ts b/apps/server/src/core/auth/services/signup.service.ts index b867f55..a02895c 100644 --- a/apps/server/src/core/auth/services/signup.service.ts +++ b/apps/server/src/core/auth/services/signup.service.ts @@ -10,6 +10,7 @@ import { CreateWorkspaceDto } from '../../workspace/dto/create-workspace.dto'; import { Workspace } from '../../workspace/entities/workspace.entity'; import { SpaceService } from '../../space/space.service'; import { CreateAdminUserDto } from '../dto/create-admin-user.dto'; +import { GroupUserService } from '../../group/services/group-user.service'; @Injectable() export class SignupService { @@ -18,6 +19,7 @@ export class SignupService { private workspaceRepository: WorkspaceRepository, private workspaceService: WorkspaceService, private spaceService: SpaceService, + private groupUserService: GroupUserService, private dataSource: DataSource, ) {} @@ -39,7 +41,6 @@ export class SignupService { async (transactionManager: EntityManager) => { let user = this.prepareUser(createUserDto); user = await transactionManager.save(user); - return user; }, this.dataSource, @@ -57,7 +58,9 @@ export class SignupService { workspaceId, ); if (userCheck) { - throw new BadRequestException('You have an account on this workspace'); + throw new BadRequestException( + 'You already have an account on this workspace', + ); } return await transactionWrapper( @@ -72,6 +75,14 @@ export class SignupService { undefined, manager, ); + + // add user to default group + await this.groupUserService.addUserToDefaultGroup( + user.id, + workspaceId, + manager, + ); + return user; }, this.dataSource, @@ -99,7 +110,7 @@ export class SignupService { ); } - async firstSetup( + async initialSetup( createAdminUserDto: CreateAdminUserDto, manager?: EntityManager, ): Promise { @@ -119,3 +130,11 @@ export class SignupService { ); } } + +// create user - +// create workspace - +// create default group +// create space +// add group to space instead of user + +// add new users to default group diff --git a/apps/server/src/core/group/dto/create-group.dto.ts b/apps/server/src/core/group/dto/create-group.dto.ts index 3ab5b4c..73c6d3b 100644 --- a/apps/server/src/core/group/dto/create-group.dto.ts +++ b/apps/server/src/core/group/dto/create-group.dto.ts @@ -10,3 +10,7 @@ export class CreateGroupDto { @IsString() description?: string; } + +export enum DefaultGroup { + EVERYONE = 'users', +} diff --git a/apps/server/src/core/group/entities/group-user.entity.ts b/apps/server/src/core/group/entities/group-user.entity.ts index cd13b5c..6307008 100644 --- a/apps/server/src/core/group/entities/group-user.entity.ts +++ b/apps/server/src/core/group/entities/group-user.entity.ts @@ -20,7 +20,7 @@ export class GroupUser { @Column() userId: string; - @ManyToOne(() => User, (user) => user.groups, { + @ManyToOne(() => User, { onDelete: 'CASCADE', }) @JoinColumn({ name: 'userId' }) diff --git a/apps/server/src/core/group/entities/group.entity.ts b/apps/server/src/core/group/entities/group.entity.ts index caa3189..afa08e6 100644 --- a/apps/server/src/core/group/entities/group.entity.ts +++ b/apps/server/src/core/group/entities/group.entity.ts @@ -11,8 +11,11 @@ import { 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'; @Entity('groups') +@Unique(['name', 'workspaceId']) export class Group { @PrimaryGeneratedColumn('uuid') id: string; @@ -23,6 +26,9 @@ export class Group { @Column({ type: 'text', nullable: true }) description: string; + @Column({ type: 'boolean', default: false }) + isDefault: boolean; + @Column() workspaceId: string; @@ -32,7 +38,7 @@ export class Group { @JoinColumn({ name: 'workspaceId' }) workspace: Workspace; - @Column() + @Column({ nullable: true }) creatorId: string; @ManyToOne(() => User) @@ -48,5 +54,8 @@ export class Group { @OneToMany(() => GroupUser, (groupUser) => groupUser.group) groupUsers: GroupUser[]; + @OneToMany(() => SpaceGroup, (spaceGroup) => spaceGroup.group) + spaces: SpaceGroup[]; + userCount?: number; } diff --git a/apps/server/src/core/group/group.controller.ts b/apps/server/src/core/group/group.controller.ts index 0bb2653..7a31bd9 100644 --- a/apps/server/src/core/group/group.controller.ts +++ b/apps/server/src/core/group/group.controller.ts @@ -119,11 +119,12 @@ export class GroupController { removeGroupMember( @Body() removeGroupUserDto: RemoveGroupUserDto, //@AuthUser() user: User, - //@CurrentWorkspace() workspace: Workspace, + @AuthWorkspace() workspace: Workspace, ) { return this.groupUserService.removeUserFromGroup( removeGroupUserDto.userId, removeGroupUserDto.groupId, + workspace.id, ); } diff --git a/apps/server/src/core/group/group.module.ts b/apps/server/src/core/group/group.module.ts index 46d1752..a6dcb3e 100644 --- a/apps/server/src/core/group/group.module.ts +++ b/apps/server/src/core/group/group.module.ts @@ -17,5 +17,6 @@ import { GroupUserService } from './services/group-user.service'; GroupRepository, GroupUserRepository, ], + exports: [GroupService, GroupUserService], }) export class GroupModule {} 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 e41507b..2af34ee 100644 --- a/apps/server/src/core/group/services/group-user.service.ts +++ b/apps/server/src/core/group/services/group-user.service.ts @@ -27,7 +27,7 @@ export class GroupUserService { workspaceId: string, paginationOptions: PaginationOptions, ): Promise> { - await this.groupService.validateGroup(groupId, workspaceId); + await this.groupService.findAndValidateGroup(groupId, workspaceId); const [groupUsers, count] = await this.groupUserRepository.findAndCount({ relations: ['user'], @@ -49,16 +49,36 @@ export class GroupUserService { return new PaginatedResult(users, paginationMeta); } + async addUserToDefaultGroup( + userId: string, + workspaceId: string, + manager?: EntityManager, + ): Promise { + return await transactionWrapper( + async (manager) => { + const defaultGroup = await this.groupService.getDefaultGroup( + workspaceId, + manager, + ); + await this.addUserToGroup( + userId, + defaultGroup.id, + workspaceId, + manager, + ); + }, + this.dataSource, + manager, + ); + } + async addUserToGroup( userId: string, groupId: string, workspaceId: string, manager?: EntityManager, - ): Promise { - let addedUser; - - /* - await transactionWrapper( + ): Promise { + return await transactionWrapper( async (manager) => { const group = await manager.findOneBy(Group, { id: groupId, @@ -69,21 +89,18 @@ export class GroupUserService { throw new NotFoundException('Group not found'); } - const userExists = await manager.exists(User, { + const find = await manager.findOne(User, { where: { id: userId }, }); - if (!userExists) { - throw new NotFoundException('User not found'); - } - // only workspace users can be added to workspace groups - const workspaceUser = await manager.findOneBy(WorkspaceUser, { - userId: userId, - workspaceId: workspaceId, + console.log(find); + + const userExists = await manager.exists(User, { + where: { id: userId, workspaceId }, }); - if (!workspaceUser) { - throw new NotFoundException('User is not a member of this workspace'); + if (!userExists) { + throw new NotFoundException('User not found'); } const existingGroupUser = await manager.findOneBy(GroupUser, { @@ -101,16 +118,29 @@ export class GroupUserService { groupUser.userId = userId; groupUser.groupId = groupId; - addedUser = await manager.save(groupUser); + return manager.save(groupUser); }, this.dataSource, manager, ); -*/ - return addedUser; } - async removeUserFromGroup(userId: string, groupId: string): Promise { + async removeUserFromGroup( + userId: string, + groupId: string, + workspaceId: string, + ): Promise { + const group = await this.groupService.findAndValidateGroup( + groupId, + workspaceId, + ); + + if (group.isDefault) { + throw new BadRequestException( + 'You cannot remove users from a default group', + ); + } + const groupUser = await this.getGroupUser(userId, groupId); if (!groupUser) { @@ -129,12 +159,4 @@ export class GroupUserService { groupId, }); } - - async getGroupUserCount(groupId: string): Promise { - return await this.groupUserRepository.count({ - where: { - groupId: groupId, - }, - }); - } } diff --git a/apps/server/src/core/group/services/group.service.ts b/apps/server/src/core/group/services/group.service.ts index 3182b20..113ee71 100644 --- a/apps/server/src/core/group/services/group.service.ts +++ b/apps/server/src/core/group/services/group.service.ts @@ -1,5 +1,9 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; -import { CreateGroupDto } from '../dto/create-group.dto'; +import { + BadRequestException, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { CreateGroupDto, DefaultGroup } from '../dto/create-group.dto'; import { GroupRepository } from '../respositories/group.repository'; import { Group } from '../entities/group.entity'; import { plainToInstance } from 'class-transformer'; @@ -8,10 +12,15 @@ import { PaginationMetaDto } from '../../../helpers/pagination/pagination-meta-d import { PaginatedResult } from '../../../helpers/pagination/paginated-result'; import { PaginationOptions } from '../../../helpers/pagination/pagination-options'; import { UpdateGroupDto } from '../dto/update-group.dto'; +import { DataSource, EntityManager } from 'typeorm'; +import { transactionWrapper } from '../../../helpers/db.helper'; @Injectable() export class GroupService { - constructor(private groupRepository: GroupRepository) {} + constructor( + private groupRepository: GroupRepository, + private dataSource: DataSource, + ) {} async createGroup( authUser: User, @@ -22,9 +31,52 @@ export class GroupService { group.creatorId = authUser.id; group.workspaceId = workspaceId; + const groupExists = await this.findGroupByName( + createGroupDto.name, + workspaceId, + ); + if (groupExists) { + throw new BadRequestException('Group name already exists'); + } + return await this.groupRepository.save(group); } + async createDefaultGroup( + workspaceId: string, + userId?: string, + manager?: EntityManager, + ): Promise { + return await transactionWrapper( + async (manager: EntityManager) => { + const group = new Group(); + group.name = DefaultGroup.EVERYONE; + group.isDefault = true; + group.creatorId = userId ?? null; + group.workspaceId = workspaceId; + return await manager.save(group); + }, + this.dataSource, + manager, + ); + } + + async getDefaultGroup( + workspaceId: string, + manager: EntityManager, + ): Promise { + return await transactionWrapper( + async (manager: EntityManager) => { + return await manager.findOneBy(Group, { + isDefault: true, + workspaceId, + }); + }, + this.dataSource, + manager, + ); + } + async updateGroup( workspaceId: string, updateGroupDto: UpdateGroupDto, @@ -38,6 +90,18 @@ export class GroupService { throw new NotFoundException('Group not found'); } + if (group.isDefault) { + throw new BadRequestException('You cannot update a default group'); + } + + const groupExists = await this.findGroupByName( + updateGroupDto.name, + workspaceId, + ); + if (groupExists) { + throw new BadRequestException('Group name already exists'); + } + if (updateGroupDto.name) { group.name = updateGroupDto.name; } @@ -90,19 +154,38 @@ export class GroupService { } async deleteGroup(groupId: string, workspaceId: string): Promise { - await this.validateGroup(groupId, workspaceId); + const group = await this.findAndValidateGroup(groupId, workspaceId); + if (group.isDefault) { + throw new BadRequestException('You cannot delete a default group'); + } await this.groupRepository.delete(groupId); } - async validateGroup(groupId: string, workspaceId: string): Promise { - const groupExists = await this.groupRepository.exists({ + async findAndValidateGroup( + groupId: string, + workspaceId: string, + ): Promise { + const group = await this.groupRepository.findOne({ where: { id: groupId, workspaceId: workspaceId, }, }); - if (!groupExists) { + if (!group) { throw new NotFoundException('Group not found'); } + + return group; + } + + async findGroupByName( + groupName: string, + workspaceId: string, + ): Promise { + return this.groupRepository + .createQueryBuilder('group') + .where('LOWER(group.name) = LOWER(:groupName)', { groupName }) + .andWhere('group.workspaceId = :workspaceId', { workspaceId }) + .getOne(); } } diff --git a/apps/server/src/core/space/entities/space-group.entity.ts b/apps/server/src/core/space/entities/space-group.entity.ts new file mode 100644 index 0000000..8fcf5ae --- /dev/null +++ b/apps/server/src/core/space/entities/space-group.entity.ts @@ -0,0 +1,45 @@ +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.entity.ts b/apps/server/src/core/space/entities/space.entity.ts index 5d6903c..91ef680 100644 --- a/apps/server/src/core/space/entities/space.entity.ts +++ b/apps/server/src/core/space/entities/space.entity.ts @@ -14,6 +14,7 @@ import { Workspace } from '../../workspace/entities/workspace.entity'; import { SpaceUser } from './space-user.entity'; import { Page } from '../../page/entities/page.entity'; import { SpacePrivacy, SpaceRole } from '../../../helpers/types/permission'; +import { SpaceGroup } from './space-group.entity'; @Entity('spaces') @Unique(['slug', 'workspaceId']) @@ -42,7 +43,7 @@ export class Space { @Column() creatorId: string; - @ManyToOne(() => User, (user) => user.spaces) + @ManyToOne(() => User) @JoinColumn({ name: 'creatorId' }) creator: User; @@ -58,6 +59,9 @@ export class Space { @OneToMany(() => SpaceUser, (spaceUser) => spaceUser.space) spaceUsers: SpaceUser[]; + @OneToMany(() => SpaceGroup, (spaceGroup) => spaceGroup.space) + spaceGroups: SpaceGroup[]; + @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 new file mode 100644 index 0000000..f7f52df --- /dev/null +++ b/apps/server/src/core/space/repositories/space-group.repository.ts @@ -0,0 +1,10 @@ +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/space.controller.ts b/apps/server/src/core/space/space.controller.ts index cd55736..7b67707 100644 --- a/apps/server/src/core/space/space.controller.ts +++ b/apps/server/src/core/space/space.controller.ts @@ -57,6 +57,7 @@ export class SpaceController { @HttpCode(HttpStatus.OK) @Post('members') async getSpaceMembers( + // todo: accept type? users | groups @Body() spaceIdDto: SpaceIdDto, @Body() pagination: PaginationOptions, diff --git a/apps/server/src/core/space/space.module.ts b/apps/server/src/core/space/space.module.ts index 303435c..f21ffdf 100644 --- a/apps/server/src/core/space/space.module.ts +++ b/apps/server/src/core/space/space.module.ts @@ -6,11 +6,18 @@ 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'; @Module({ - imports: [TypeOrmModule.forFeature([Space, SpaceUser])], + imports: [TypeOrmModule.forFeature([Space, SpaceUser, SpaceGroup])], controllers: [SpaceController], - providers: [SpaceService, SpaceRepository, SpaceUserRepository], - exports: [SpaceService, SpaceRepository, SpaceUserRepository], + providers: [ + SpaceService, + SpaceRepository, + SpaceUserRepository, + SpaceGroupRepository, + ], + exports: [SpaceService], }) export class SpaceModule {} diff --git a/apps/server/src/core/space/space.service.ts b/apps/server/src/core/space/space.service.ts index 6de8b09..e5ec86f 100644 --- a/apps/server/src/core/space/space.service.ts +++ b/apps/server/src/core/space/space.service.ts @@ -14,12 +14,16 @@ 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, ) {} @@ -94,7 +98,7 @@ export class SpaceService { 'space.userCount', 'space.spaceUsers', 'spaceUsers', - ) + ) // TODO: add groups to userCount .getOne(); if (!space) { @@ -115,7 +119,7 @@ export class SpaceService { 'space.userCount', 'space.spaceUsers', 'spaceUsers', - ) + ) // TODO: add groups to userCount .take(paginationOptions.limit) .skip(paginationOptions.skip) .getManyAndCount(); @@ -178,4 +182,71 @@ export class SpaceService { 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 4ab1607..67e6e41 100644 --- a/apps/server/src/core/user/entities/user.entity.ts +++ b/apps/server/src/core/user/entities/user.entity.ts @@ -15,7 +15,6 @@ 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 { Group } from '../../group/entities/group.entity'; @Entity('users') @Unique(['email', 'workspaceId']) @@ -44,7 +43,9 @@ export class User { @Column({ nullable: true }) workspaceId: string; - @ManyToOne(() => Workspace, (workspace) => workspace.users) + @ManyToOne(() => Workspace, (workspace) => workspace.users, { + onDelete: 'CASCADE', + }) workspace: Workspace; @Column({ length: 100, nullable: true }) @@ -68,9 +69,6 @@ export class User { @UpdateDateColumn() updatedAt: Date; - @OneToMany(() => Group, (group) => group.creator) - groups: Group[]; - @OneToMany(() => Page, (page) => page.creator) createdPages: Page[]; diff --git a/apps/server/src/core/workspace/services/workspace.service.ts b/apps/server/src/core/workspace/services/workspace.service.ts index 74566c3..3a844e2 100644 --- a/apps/server/src/core/workspace/services/workspace.service.ts +++ b/apps/server/src/core/workspace/services/workspace.service.ts @@ -17,7 +17,8 @@ import { UserRepository } from '../../user/repositories/user.repository'; import { SpaceRole, UserRole } from '../../../helpers/types/permission'; import { User } from '../../user/entities/user.entity'; import { EnvironmentService } from '../../../environment/environment.service'; -import { Space } from '../../space/entities/space.entity'; +import { GroupService } from '../../group/services/group.service'; +import { GroupUserService } from '../../group/services/group-user.service'; @Injectable() export class WorkspaceService { @@ -25,6 +26,8 @@ export class WorkspaceService { private workspaceRepository: WorkspaceRepository, private userRepository: UserRepository, private spaceService: SpaceService, + private groupService: GroupService, + private groupUserService: GroupUserService, private environmentService: EnvironmentService, private dataSource: DataSource, @@ -68,12 +71,28 @@ export class WorkspaceService { workspace.creatorId = user.id; workspace = await manager.save(workspace); + // create default group + const group = await this.groupService.createDefaultGroup( + workspace.id, + user.id, + manager, + ); + + // attach user to workspace user.workspaceId = workspace.id; user.role = UserRole.OWNER; await manager.save(user); + // add user to default group + await this.groupUserService.addUserToGroup( + user.id, + group.id, + workspace.id, + manager, + ); + // create default space - const spaceData: CreateSpaceDto = { + const spaceInfo: CreateSpaceDto = { name: 'General', }; @@ -81,11 +100,11 @@ export class WorkspaceService { const createdSpace = await this.spaceService.create( user.id, workspace.id, - spaceData, + spaceInfo, manager, ); - // and add user to it too. + // and add user to space as owner await this.spaceService.addUserToSpace( user.id, createdSpace.id, @@ -94,6 +113,15 @@ export class WorkspaceService { manager, ); + // add default group to space as writer + await this.spaceService.addGroupToSpace( + group.id, + createdSpace.id, + SpaceRole.WRITER, + workspace.id, + manager, + ); + workspace.defaultSpaceId = createdSpace.id; await manager.save(workspace); return workspace; @@ -108,7 +136,7 @@ export class WorkspaceService { workspaceId, assignedRole?: UserRole, manager?: EntityManager, - ): Promise { + ): Promise { return await transactionWrapper( async (manager: EntityManager) => { const workspace = await manager.findOneBy(Workspace, { @@ -123,25 +151,7 @@ export class WorkspaceService { user.workspaceId = workspace.id; await manager.save(user); - const space = await manager.findOneBy(Space, { - id: workspace.defaultSpaceId, - workspaceId, - }); - - if (!space) { - throw new NotFoundException('Space not found'); - } - - // add user to default space - await this.spaceService.addUserToSpace( - user.id, - space.id, - space.defaultRole, - workspace.id, - manager, - ); - - return workspace; + // User is now added to the default space via the default group }, this.dataSource, manager, @@ -175,9 +185,6 @@ export class WorkspaceService { if (!workspace) { throw new NotFoundException('Workspace not found'); } - - //TODO - // remove all existing users from workspace - // delete workspace + // delete } } diff --git a/apps/server/src/core/workspace/workspace.module.ts b/apps/server/src/core/workspace/workspace.module.ts index 8eeea9b..5a24164 100644 --- a/apps/server/src/core/workspace/workspace.module.ts +++ b/apps/server/src/core/workspace/workspace.module.ts @@ -10,11 +10,14 @@ import { WorkspaceInvitationService } from './services/workspace-invitation.serv import { WorkspaceInvitationRepository } from './repositories/workspace-invitation.repository'; import { WorkspaceUserService } from './services/workspace-user.service'; import { UserModule } from '../user/user.module'; +import { GroupModule } from '../group/group.module'; @Module({ imports: [ TypeOrmModule.forFeature([Workspace, WorkspaceInvitation]), - SpaceModule, UserModule + SpaceModule, + UserModule, + GroupModule, ], controllers: [WorkspaceController], providers: [ diff --git a/apps/server/src/database/migrations/1710886360227-AddDefaultGroup.ts b/apps/server/src/database/migrations/1710886360227-AddDefaultGroup.ts new file mode 100644 index 0000000..4ac3ab0 --- /dev/null +++ b/apps/server/src/database/migrations/1710886360227-AddDefaultGroup.ts @@ -0,0 +1,22 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddDefaultGroup1710886360227 implements MigrationInterface { + name = 'AddDefaultGroup1710886360227' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "groups" ADD "isDefault" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`ALTER TABLE "groups" DROP CONSTRAINT "FK_accb24ba8f4f213f33d08e2a20f"`); + await queryRunner.query(`ALTER TABLE "groups" ALTER COLUMN "creatorId" DROP NOT NULL`); + await queryRunner.query(`ALTER TABLE "groups" ADD CONSTRAINT "UQ_c092c7c01795e6ad7af46bf2d24" UNIQUE ("name", "workspaceId")`); + await queryRunner.query(`ALTER TABLE "groups" ADD CONSTRAINT "FK_accb24ba8f4f213f33d08e2a20f" 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 "groups" DROP CONSTRAINT "FK_accb24ba8f4f213f33d08e2a20f"`); + await queryRunner.query(`ALTER TABLE "groups" DROP CONSTRAINT "UQ_c092c7c01795e6ad7af46bf2d24"`); + await queryRunner.query(`ALTER TABLE "groups" ALTER COLUMN "creatorId" SET NOT NULL`); + await queryRunner.query(`ALTER TABLE "groups" ADD CONSTRAINT "FK_accb24ba8f4f213f33d08e2a20f" FOREIGN KEY ("creatorId") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "groups" DROP COLUMN "isDefault"`); + } + +} diff --git a/apps/server/src/database/migrations/1710892343941-SpaceGroupsMembership.ts b/apps/server/src/database/migrations/1710892343941-SpaceGroupsMembership.ts new file mode 100644 index 0000000..d018ef2 --- /dev/null +++ b/apps/server/src/database/migrations/1710892343941-SpaceGroupsMembership.ts @@ -0,0 +1,18 @@ +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/1710894465616-AddWorkspaceCascadeToUser.ts b/apps/server/src/database/migrations/1710894465616-AddWorkspaceCascadeToUser.ts new file mode 100644 index 0000000..17c0380 --- /dev/null +++ b/apps/server/src/database/migrations/1710894465616-AddWorkspaceCascadeToUser.ts @@ -0,0 +1,16 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddWorkspaceCascadeToUser1710894465616 implements MigrationInterface { + name = 'AddWorkspaceCascadeToUser1710894465616' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "users" DROP CONSTRAINT "FK_949fea12b7977a8b2f483bf802a"`); + await queryRunner.query(`ALTER TABLE "users" ADD CONSTRAINT "FK_949fea12b7977a8b2f483bf802a" FOREIGN KEY ("workspaceId") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "users" DROP CONSTRAINT "FK_949fea12b7977a8b2f483bf802a"`); + await queryRunner.query(`ALTER TABLE "users" ADD CONSTRAINT "FK_949fea12b7977a8b2f483bf802a" FOREIGN KEY ("workspaceId") REFERENCES "workspaces"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + } + +}