Implement Space membership by group

* Add all users to default group
* Fixes and updates
This commit is contained in:
Philipinho
2024-03-20 01:26:03 +00:00
parent a821e37028
commit 51b9808382
22 changed files with 425 additions and 82 deletions

View File

@ -9,6 +9,7 @@ import { WorkspaceModule } from '../workspace/workspace.module';
import { SignupService } from './services/signup.service'; import { SignupService } from './services/signup.service';
import { UserModule } from '../user/user.module'; import { UserModule } from '../user/user.module';
import { SpaceModule } from '../space/space.module'; import { SpaceModule } from '../space/space.module';
import { GroupModule } from '../group/group.module';
@Module({ @Module({
imports: [ imports: [
@ -26,6 +27,7 @@ import { SpaceModule } from '../space/space.module';
UserModule, UserModule,
WorkspaceModule, WorkspaceModule,
SpaceModule, SpaceModule,
GroupModule,
], ],
controllers: [AuthController], controllers: [AuthController],
providers: [AuthService, SignupService, TokenService, JwtStrategy], providers: [AuthService, SignupService, TokenService, JwtStrategy],

View File

@ -51,7 +51,7 @@ export class AuthService {
} }
async setup(createAdminUserDto: CreateAdminUserDto) { 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); const tokens: TokensDto = await this.tokenService.generateTokens(user);

View File

@ -10,6 +10,7 @@ import { CreateWorkspaceDto } from '../../workspace/dto/create-workspace.dto';
import { Workspace } from '../../workspace/entities/workspace.entity'; import { Workspace } from '../../workspace/entities/workspace.entity';
import { SpaceService } from '../../space/space.service'; import { SpaceService } from '../../space/space.service';
import { CreateAdminUserDto } from '../dto/create-admin-user.dto'; import { CreateAdminUserDto } from '../dto/create-admin-user.dto';
import { GroupUserService } from '../../group/services/group-user.service';
@Injectable() @Injectable()
export class SignupService { export class SignupService {
@ -18,6 +19,7 @@ export class SignupService {
private workspaceRepository: WorkspaceRepository, private workspaceRepository: WorkspaceRepository,
private workspaceService: WorkspaceService, private workspaceService: WorkspaceService,
private spaceService: SpaceService, private spaceService: SpaceService,
private groupUserService: GroupUserService,
private dataSource: DataSource, private dataSource: DataSource,
) {} ) {}
@ -39,7 +41,6 @@ export class SignupService {
async (transactionManager: EntityManager) => { async (transactionManager: EntityManager) => {
let user = this.prepareUser(createUserDto); let user = this.prepareUser(createUserDto);
user = await transactionManager.save(user); user = await transactionManager.save(user);
return user; return user;
}, },
this.dataSource, this.dataSource,
@ -57,7 +58,9 @@ export class SignupService {
workspaceId, workspaceId,
); );
if (userCheck) { 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( return await transactionWrapper(
@ -72,6 +75,14 @@ export class SignupService {
undefined, undefined,
manager, manager,
); );
// add user to default group
await this.groupUserService.addUserToDefaultGroup(
user.id,
workspaceId,
manager,
);
return user; return user;
}, },
this.dataSource, this.dataSource,
@ -99,7 +110,7 @@ export class SignupService {
); );
} }
async firstSetup( async initialSetup(
createAdminUserDto: CreateAdminUserDto, createAdminUserDto: CreateAdminUserDto,
manager?: EntityManager, manager?: EntityManager,
): Promise<User> { ): Promise<User> {
@ -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

View File

@ -10,3 +10,7 @@ export class CreateGroupDto {
@IsString() @IsString()
description?: string; description?: string;
} }
export enum DefaultGroup {
EVERYONE = 'users',
}

View File

@ -20,7 +20,7 @@ export class GroupUser {
@Column() @Column()
userId: string; userId: string;
@ManyToOne(() => User, (user) => user.groups, { @ManyToOne(() => User, {
onDelete: 'CASCADE', onDelete: 'CASCADE',
}) })
@JoinColumn({ name: 'userId' }) @JoinColumn({ name: 'userId' })

View File

@ -11,8 +11,11 @@ import {
import { GroupUser } from './group-user.entity'; import { GroupUser } from './group-user.entity';
import { Workspace } from '../../workspace/entities/workspace.entity'; import { Workspace } from '../../workspace/entities/workspace.entity';
import { User } from '../../user/entities/user.entity'; import { User } from '../../user/entities/user.entity';
import { Unique } from 'typeorm';
import { SpaceGroup } from '../../space/entities/space-group.entity';
@Entity('groups') @Entity('groups')
@Unique(['name', 'workspaceId'])
export class Group { export class Group {
@PrimaryGeneratedColumn('uuid') @PrimaryGeneratedColumn('uuid')
id: string; id: string;
@ -23,6 +26,9 @@ export class Group {
@Column({ type: 'text', nullable: true }) @Column({ type: 'text', nullable: true })
description: string; description: string;
@Column({ type: 'boolean', default: false })
isDefault: boolean;
@Column() @Column()
workspaceId: string; workspaceId: string;
@ -32,7 +38,7 @@ export class Group {
@JoinColumn({ name: 'workspaceId' }) @JoinColumn({ name: 'workspaceId' })
workspace: Workspace; workspace: Workspace;
@Column() @Column({ nullable: true })
creatorId: string; creatorId: string;
@ManyToOne(() => User) @ManyToOne(() => User)
@ -48,5 +54,8 @@ export class Group {
@OneToMany(() => GroupUser, (groupUser) => groupUser.group) @OneToMany(() => GroupUser, (groupUser) => groupUser.group)
groupUsers: GroupUser[]; groupUsers: GroupUser[];
@OneToMany(() => SpaceGroup, (spaceGroup) => spaceGroup.group)
spaces: SpaceGroup[];
userCount?: number; userCount?: number;
} }

View File

@ -119,11 +119,12 @@ export class GroupController {
removeGroupMember( removeGroupMember(
@Body() removeGroupUserDto: RemoveGroupUserDto, @Body() removeGroupUserDto: RemoveGroupUserDto,
//@AuthUser() user: User, //@AuthUser() user: User,
//@CurrentWorkspace() workspace: Workspace, @AuthWorkspace() workspace: Workspace,
) { ) {
return this.groupUserService.removeUserFromGroup( return this.groupUserService.removeUserFromGroup(
removeGroupUserDto.userId, removeGroupUserDto.userId,
removeGroupUserDto.groupId, removeGroupUserDto.groupId,
workspace.id,
); );
} }

View File

@ -17,5 +17,6 @@ import { GroupUserService } from './services/group-user.service';
GroupRepository, GroupRepository,
GroupUserRepository, GroupUserRepository,
], ],
exports: [GroupService, GroupUserService],
}) })
export class GroupModule {} export class GroupModule {}

View File

@ -27,7 +27,7 @@ export class GroupUserService {
workspaceId: string, workspaceId: string,
paginationOptions: PaginationOptions, paginationOptions: PaginationOptions,
): Promise<PaginatedResult<User>> { ): Promise<PaginatedResult<User>> {
await this.groupService.validateGroup(groupId, workspaceId); await this.groupService.findAndValidateGroup(groupId, workspaceId);
const [groupUsers, count] = await this.groupUserRepository.findAndCount({ const [groupUsers, count] = await this.groupUserRepository.findAndCount({
relations: ['user'], relations: ['user'],
@ -49,16 +49,36 @@ export class GroupUserService {
return new PaginatedResult(users, paginationMeta); return new PaginatedResult(users, paginationMeta);
} }
async addUserToDefaultGroup(
userId: string,
workspaceId: string,
manager?: EntityManager,
): Promise<void> {
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( async addUserToGroup(
userId: string, userId: string,
groupId: string, groupId: string,
workspaceId: string, workspaceId: string,
manager?: EntityManager, manager?: EntityManager,
): Promise<any> { ): Promise<GroupUser> {
let addedUser; return await transactionWrapper(
/*
await transactionWrapper(
async (manager) => { async (manager) => {
const group = await manager.findOneBy(Group, { const group = await manager.findOneBy(Group, {
id: groupId, id: groupId,
@ -69,21 +89,18 @@ export class GroupUserService {
throw new NotFoundException('Group not found'); throw new NotFoundException('Group not found');
} }
const userExists = await manager.exists(User, { const find = await manager.findOne(User, {
where: { id: userId }, where: { id: userId },
}); });
if (!userExists) {
throw new NotFoundException('User not found');
}
// only workspace users can be added to workspace groups console.log(find);
const workspaceUser = await manager.findOneBy(WorkspaceUser, {
userId: userId, const userExists = await manager.exists(User, {
workspaceId: workspaceId, where: { id: userId, workspaceId },
}); });
if (!workspaceUser) { if (!userExists) {
throw new NotFoundException('User is not a member of this workspace'); throw new NotFoundException('User not found');
} }
const existingGroupUser = await manager.findOneBy(GroupUser, { const existingGroupUser = await manager.findOneBy(GroupUser, {
@ -101,16 +118,29 @@ export class GroupUserService {
groupUser.userId = userId; groupUser.userId = userId;
groupUser.groupId = groupId; groupUser.groupId = groupId;
addedUser = await manager.save(groupUser); return manager.save(groupUser);
}, },
this.dataSource, this.dataSource,
manager, manager,
); );
*/
return addedUser;
} }
async removeUserFromGroup(userId: string, groupId: string): Promise<void> { async removeUserFromGroup(
userId: string,
groupId: string,
workspaceId: string,
): Promise<void> {
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); const groupUser = await this.getGroupUser(userId, groupId);
if (!groupUser) { if (!groupUser) {
@ -129,12 +159,4 @@ export class GroupUserService {
groupId, groupId,
}); });
} }
async getGroupUserCount(groupId: string): Promise<number> {
return await this.groupUserRepository.count({
where: {
groupId: groupId,
},
});
}
} }

View File

@ -1,5 +1,9 @@
import { Injectable, NotFoundException } from '@nestjs/common'; import {
import { CreateGroupDto } from '../dto/create-group.dto'; BadRequestException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { CreateGroupDto, DefaultGroup } from '../dto/create-group.dto';
import { GroupRepository } from '../respositories/group.repository'; import { GroupRepository } from '../respositories/group.repository';
import { Group } from '../entities/group.entity'; import { Group } from '../entities/group.entity';
import { plainToInstance } from 'class-transformer'; 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 { PaginatedResult } from '../../../helpers/pagination/paginated-result';
import { PaginationOptions } from '../../../helpers/pagination/pagination-options'; import { PaginationOptions } from '../../../helpers/pagination/pagination-options';
import { UpdateGroupDto } from '../dto/update-group.dto'; import { UpdateGroupDto } from '../dto/update-group.dto';
import { DataSource, EntityManager } from 'typeorm';
import { transactionWrapper } from '../../../helpers/db.helper';
@Injectable() @Injectable()
export class GroupService { export class GroupService {
constructor(private groupRepository: GroupRepository) {} constructor(
private groupRepository: GroupRepository,
private dataSource: DataSource,
) {}
async createGroup( async createGroup(
authUser: User, authUser: User,
@ -22,9 +31,52 @@ export class GroupService {
group.creatorId = authUser.id; group.creatorId = authUser.id;
group.workspaceId = workspaceId; 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); return await this.groupRepository.save(group);
} }
async createDefaultGroup(
workspaceId: string,
userId?: string,
manager?: EntityManager,
): Promise<Group> {
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<Group> {
return await transactionWrapper(
async (manager: EntityManager) => {
return await manager.findOneBy(Group, {
isDefault: true,
workspaceId,
});
},
this.dataSource,
manager,
);
}
async updateGroup( async updateGroup(
workspaceId: string, workspaceId: string,
updateGroupDto: UpdateGroupDto, updateGroupDto: UpdateGroupDto,
@ -38,6 +90,18 @@ export class GroupService {
throw new NotFoundException('Group not found'); 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) { if (updateGroupDto.name) {
group.name = updateGroupDto.name; group.name = updateGroupDto.name;
} }
@ -90,19 +154,38 @@ export class GroupService {
} }
async deleteGroup(groupId: string, workspaceId: string): Promise<void> { async deleteGroup(groupId: string, workspaceId: string): Promise<void> {
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); await this.groupRepository.delete(groupId);
} }
async validateGroup(groupId: string, workspaceId: string): Promise<void> { async findAndValidateGroup(
const groupExists = await this.groupRepository.exists({ groupId: string,
workspaceId: string,
): Promise<Group> {
const group = await this.groupRepository.findOne({
where: { where: {
id: groupId, id: groupId,
workspaceId: workspaceId, workspaceId: workspaceId,
}, },
}); });
if (!groupExists) { if (!group) {
throw new NotFoundException('Group not found'); throw new NotFoundException('Group not found');
} }
return group;
}
async findGroupByName(
groupName: string,
workspaceId: string,
): Promise<Group> {
return this.groupRepository
.createQueryBuilder('group')
.where('LOWER(group.name) = LOWER(:groupName)', { groupName })
.andWhere('group.workspaceId = :workspaceId', { workspaceId })
.getOne();
} }
} }

View File

@ -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;
}

View File

@ -14,6 +14,7 @@ import { Workspace } from '../../workspace/entities/workspace.entity';
import { SpaceUser } from './space-user.entity'; import { SpaceUser } from './space-user.entity';
import { Page } from '../../page/entities/page.entity'; import { Page } from '../../page/entities/page.entity';
import { SpacePrivacy, SpaceRole } from '../../../helpers/types/permission'; import { SpacePrivacy, SpaceRole } from '../../../helpers/types/permission';
import { SpaceGroup } from './space-group.entity';
@Entity('spaces') @Entity('spaces')
@Unique(['slug', 'workspaceId']) @Unique(['slug', 'workspaceId'])
@ -42,7 +43,7 @@ export class Space {
@Column() @Column()
creatorId: string; creatorId: string;
@ManyToOne(() => User, (user) => user.spaces) @ManyToOne(() => User)
@JoinColumn({ name: 'creatorId' }) @JoinColumn({ name: 'creatorId' })
creator: User; creator: User;
@ -58,6 +59,9 @@ export class Space {
@OneToMany(() => SpaceUser, (spaceUser) => spaceUser.space) @OneToMany(() => SpaceUser, (spaceUser) => spaceUser.space)
spaceUsers: SpaceUser[]; spaceUsers: SpaceUser[];
@OneToMany(() => SpaceGroup, (spaceGroup) => spaceGroup.space)
spaceGroups: SpaceGroup[];
@OneToMany(() => Page, (page) => page.space) @OneToMany(() => Page, (page) => page.space)
pages: Page[]; pages: Page[];

View File

@ -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<SpaceGroup> {
constructor(private dataSource: DataSource) {
super(SpaceGroup, dataSource.createEntityManager());
}
}

View File

@ -57,6 +57,7 @@ export class SpaceController {
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@Post('members') @Post('members')
async getSpaceMembers( async getSpaceMembers(
// todo: accept type? users | groups
@Body() spaceIdDto: SpaceIdDto, @Body() spaceIdDto: SpaceIdDto,
@Body() @Body()
pagination: PaginationOptions, pagination: PaginationOptions,

View File

@ -6,11 +6,18 @@ import { Space } from './entities/space.entity';
import { SpaceUser } from './entities/space-user.entity'; import { SpaceUser } from './entities/space-user.entity';
import { SpaceRepository } from './repositories/space.repository'; import { SpaceRepository } from './repositories/space.repository';
import { SpaceUserRepository } from './repositories/space-user.repository'; import { SpaceUserRepository } from './repositories/space-user.repository';
import { SpaceGroup } from './entities/space-group.entity';
import { SpaceGroupRepository } from './repositories/space-group.repository';
@Module({ @Module({
imports: [TypeOrmModule.forFeature([Space, SpaceUser])], imports: [TypeOrmModule.forFeature([Space, SpaceUser, SpaceGroup])],
controllers: [SpaceController], controllers: [SpaceController],
providers: [SpaceService, SpaceRepository, SpaceUserRepository], providers: [
exports: [SpaceService, SpaceRepository, SpaceUserRepository], SpaceService,
SpaceRepository,
SpaceUserRepository,
SpaceGroupRepository,
],
exports: [SpaceService],
}) })
export class SpaceModule {} export class SpaceModule {}

View File

@ -14,12 +14,16 @@ import { User } from '../user/entities/user.entity';
import { PaginationOptions } from '../../helpers/pagination/pagination-options'; import { PaginationOptions } from '../../helpers/pagination/pagination-options';
import { PaginationMetaDto } from '../../helpers/pagination/pagination-meta-dto'; import { PaginationMetaDto } from '../../helpers/pagination/pagination-meta-dto';
import { PaginatedResult } from '../../helpers/pagination/paginated-result'; 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() @Injectable()
export class SpaceService { export class SpaceService {
constructor( constructor(
private spaceRepository: SpaceRepository, private spaceRepository: SpaceRepository,
private spaceUserRepository: SpaceUserRepository, private spaceUserRepository: SpaceUserRepository,
private spaceGroupRepository: SpaceGroupRepository,
private dataSource: DataSource, private dataSource: DataSource,
) {} ) {}
@ -94,7 +98,7 @@ export class SpaceService {
'space.userCount', 'space.userCount',
'space.spaceUsers', 'space.spaceUsers',
'spaceUsers', 'spaceUsers',
) ) // TODO: add groups to userCount
.getOne(); .getOne();
if (!space) { if (!space) {
@ -115,7 +119,7 @@ export class SpaceService {
'space.userCount', 'space.userCount',
'space.spaceUsers', 'space.spaceUsers',
'spaceUsers', 'spaceUsers',
) ) // TODO: add groups to userCount
.take(paginationOptions.limit) .take(paginationOptions.limit)
.skip(paginationOptions.skip) .skip(paginationOptions.skip)
.getManyAndCount(); .getManyAndCount();
@ -178,4 +182,71 @@ export class SpaceService {
const paginationMeta = new PaginationMetaDto({ count, paginationOptions }); const paginationMeta = new PaginationMetaDto({ count, paginationOptions });
return new PaginatedResult(users, paginationMeta); return new PaginatedResult(users, paginationMeta);
} }
async addGroupToSpace(
groupId: string,
spaceId: string,
role: string,
workspaceId,
manager?: EntityManager,
): Promise<SpaceGroup> {
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);
}
} }

View File

@ -15,7 +15,6 @@ import { Page } from '../../page/entities/page.entity';
import { Comment } from '../../comment/entities/comment.entity'; import { Comment } from '../../comment/entities/comment.entity';
import { Space } from '../../space/entities/space.entity'; import { Space } from '../../space/entities/space.entity';
import { SpaceUser } from '../../space/entities/space-user.entity'; import { SpaceUser } from '../../space/entities/space-user.entity';
import { Group } from '../../group/entities/group.entity';
@Entity('users') @Entity('users')
@Unique(['email', 'workspaceId']) @Unique(['email', 'workspaceId'])
@ -44,7 +43,9 @@ export class User {
@Column({ nullable: true }) @Column({ nullable: true })
workspaceId: string; workspaceId: string;
@ManyToOne(() => Workspace, (workspace) => workspace.users) @ManyToOne(() => Workspace, (workspace) => workspace.users, {
onDelete: 'CASCADE',
})
workspace: Workspace; workspace: Workspace;
@Column({ length: 100, nullable: true }) @Column({ length: 100, nullable: true })
@ -68,9 +69,6 @@ export class User {
@UpdateDateColumn() @UpdateDateColumn()
updatedAt: Date; updatedAt: Date;
@OneToMany(() => Group, (group) => group.creator)
groups: Group[];
@OneToMany(() => Page, (page) => page.creator) @OneToMany(() => Page, (page) => page.creator)
createdPages: Page[]; createdPages: Page[];

View File

@ -17,7 +17,8 @@ import { UserRepository } from '../../user/repositories/user.repository';
import { SpaceRole, UserRole } from '../../../helpers/types/permission'; import { SpaceRole, UserRole } from '../../../helpers/types/permission';
import { User } from '../../user/entities/user.entity'; import { User } from '../../user/entities/user.entity';
import { EnvironmentService } from '../../../environment/environment.service'; 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() @Injectable()
export class WorkspaceService { export class WorkspaceService {
@ -25,6 +26,8 @@ export class WorkspaceService {
private workspaceRepository: WorkspaceRepository, private workspaceRepository: WorkspaceRepository,
private userRepository: UserRepository, private userRepository: UserRepository,
private spaceService: SpaceService, private spaceService: SpaceService,
private groupService: GroupService,
private groupUserService: GroupUserService,
private environmentService: EnvironmentService, private environmentService: EnvironmentService,
private dataSource: DataSource, private dataSource: DataSource,
@ -68,12 +71,28 @@ export class WorkspaceService {
workspace.creatorId = user.id; workspace.creatorId = user.id;
workspace = await manager.save(workspace); 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.workspaceId = workspace.id;
user.role = UserRole.OWNER; user.role = UserRole.OWNER;
await manager.save(user); await manager.save(user);
// add user to default group
await this.groupUserService.addUserToGroup(
user.id,
group.id,
workspace.id,
manager,
);
// create default space // create default space
const spaceData: CreateSpaceDto = { const spaceInfo: CreateSpaceDto = {
name: 'General', name: 'General',
}; };
@ -81,11 +100,11 @@ export class WorkspaceService {
const createdSpace = await this.spaceService.create( const createdSpace = await this.spaceService.create(
user.id, user.id,
workspace.id, workspace.id,
spaceData, spaceInfo,
manager, manager,
); );
// and add user to it too. // and add user to space as owner
await this.spaceService.addUserToSpace( await this.spaceService.addUserToSpace(
user.id, user.id,
createdSpace.id, createdSpace.id,
@ -94,6 +113,15 @@ export class WorkspaceService {
manager, 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; workspace.defaultSpaceId = createdSpace.id;
await manager.save(workspace); await manager.save(workspace);
return workspace; return workspace;
@ -108,7 +136,7 @@ export class WorkspaceService {
workspaceId, workspaceId,
assignedRole?: UserRole, assignedRole?: UserRole,
manager?: EntityManager, manager?: EntityManager,
): Promise<Workspace> { ): Promise<void> {
return await transactionWrapper( return await transactionWrapper(
async (manager: EntityManager) => { async (manager: EntityManager) => {
const workspace = await manager.findOneBy(Workspace, { const workspace = await manager.findOneBy(Workspace, {
@ -123,25 +151,7 @@ export class WorkspaceService {
user.workspaceId = workspace.id; user.workspaceId = workspace.id;
await manager.save(user); await manager.save(user);
const space = await manager.findOneBy(Space, { // User is now added to the default space via the default group
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;
}, },
this.dataSource, this.dataSource,
manager, manager,
@ -175,9 +185,6 @@ export class WorkspaceService {
if (!workspace) { if (!workspace) {
throw new NotFoundException('Workspace not found'); throw new NotFoundException('Workspace not found');
} }
// delete
//TODO
// remove all existing users from workspace
// delete workspace
} }
} }

View File

@ -10,11 +10,14 @@ import { WorkspaceInvitationService } from './services/workspace-invitation.serv
import { WorkspaceInvitationRepository } from './repositories/workspace-invitation.repository'; import { WorkspaceInvitationRepository } from './repositories/workspace-invitation.repository';
import { WorkspaceUserService } from './services/workspace-user.service'; import { WorkspaceUserService } from './services/workspace-user.service';
import { UserModule } from '../user/user.module'; import { UserModule } from '../user/user.module';
import { GroupModule } from '../group/group.module';
@Module({ @Module({
imports: [ imports: [
TypeOrmModule.forFeature([Workspace, WorkspaceInvitation]), TypeOrmModule.forFeature([Workspace, WorkspaceInvitation]),
SpaceModule, UserModule SpaceModule,
UserModule,
GroupModule,
], ],
controllers: [WorkspaceController], controllers: [WorkspaceController],
providers: [ providers: [

View File

@ -0,0 +1,22 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddDefaultGroup1710886360227 implements MigrationInterface {
name = 'AddDefaultGroup1710886360227'
public async up(queryRunner: QueryRunner): Promise<void> {
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<void> {
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"`);
}
}

View File

@ -0,0 +1,18 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class SpaceGroupsMembership1710892343941 implements MigrationInterface {
name = 'SpaceGroupsMembership1710892343941'
public async up(queryRunner: QueryRunner): Promise<void> {
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<void> {
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"`);
}
}

View File

@ -0,0 +1,16 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddWorkspaceCascadeToUser1710894465616 implements MigrationInterface {
name = 'AddWorkspaceCascadeToUser1710894465616'
public async up(queryRunner: QueryRunner): Promise<void> {
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<void> {
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`);
}
}