mirror of
https://github.com/docmost/docmost.git
synced 2025-11-13 14:12:36 +10:00
feat: groups
This commit is contained in:
@ -9,6 +9,7 @@ import { EnvironmentModule } from '../environment/environment.module';
|
|||||||
import { CommentModule } from './comment/comment.module';
|
import { CommentModule } from './comment/comment.module';
|
||||||
import { SearchModule } from './search/search.module';
|
import { SearchModule } from './search/search.module';
|
||||||
import { SpaceModule } from './space/space.module';
|
import { SpaceModule } from './space/space.module';
|
||||||
|
import { GroupModule } from './group/group.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@ -23,6 +24,7 @@ import { SpaceModule } from './space/space.module';
|
|||||||
CommentModule,
|
CommentModule,
|
||||||
SearchModule,
|
SearchModule,
|
||||||
SpaceModule,
|
SpaceModule,
|
||||||
|
GroupModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class CoreModule {}
|
export class CoreModule {}
|
||||||
|
|||||||
8
apps/server/src/core/group/dto/add-group-user.dto.ts
Normal file
8
apps/server/src/core/group/dto/add-group-user.dto.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { IsNotEmpty, IsUUID } from 'class-validator';
|
||||||
|
import { GroupIdDto } from './group-id.dto';
|
||||||
|
|
||||||
|
export class AddGroupUserDto extends GroupIdDto {
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsUUID()
|
||||||
|
userId: string;
|
||||||
|
}
|
||||||
12
apps/server/src/core/group/dto/create-group.dto.ts
Normal file
12
apps/server/src/core/group/dto/create-group.dto.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { IsOptional, IsString, MaxLength, MinLength } from 'class-validator';
|
||||||
|
|
||||||
|
export class CreateGroupDto {
|
||||||
|
@MinLength(2)
|
||||||
|
@MaxLength(64)
|
||||||
|
@IsString()
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
7
apps/server/src/core/group/dto/group-id.dto.ts
Normal file
7
apps/server/src/core/group/dto/group-id.dto.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { IsNotEmpty, IsUUID } from 'class-validator';
|
||||||
|
|
||||||
|
export class GroupIdDto {
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsUUID()
|
||||||
|
groupId: string;
|
||||||
|
}
|
||||||
3
apps/server/src/core/group/dto/remove-group-user.dto.ts
Normal file
3
apps/server/src/core/group/dto/remove-group-user.dto.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { AddGroupUserDto } from './add-group-user.dto';
|
||||||
|
|
||||||
|
export class RemoveGroupUserDto extends AddGroupUserDto {}
|
||||||
4
apps/server/src/core/group/dto/update-group.dto.ts
Normal file
4
apps/server/src/core/group/dto/update-group.dto.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import { PartialType } from '@nestjs/mapped-types';
|
||||||
|
import { CreateGroupDto } from './create-group.dto';
|
||||||
|
|
||||||
|
export class UpdateGroupDto extends PartialType(CreateGroupDto) {}
|
||||||
43
apps/server/src/core/group/entities/group-user.entity.ts
Normal file
43
apps/server/src/core/group/entities/group-user.entity.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import {
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
Entity,
|
||||||
|
JoinColumn,
|
||||||
|
ManyToOne,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
Unique,
|
||||||
|
UpdateDateColumn,
|
||||||
|
} from 'typeorm';
|
||||||
|
import { User } from '../../user/entities/user.entity';
|
||||||
|
import { Group } from './group.entity';
|
||||||
|
|
||||||
|
@Entity('group_users')
|
||||||
|
@Unique(['groupId', 'userId'])
|
||||||
|
export class GroupUser {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
userId: string;
|
||||||
|
|
||||||
|
@ManyToOne(() => User, (user) => user.workspaceUsers, {
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
})
|
||||||
|
@JoinColumn({ name: 'userId' })
|
||||||
|
user: User;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
groupId: string;
|
||||||
|
|
||||||
|
@ManyToOne(() => Group, (group) => group.groupUsers, {
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
})
|
||||||
|
@JoinColumn({ name: 'groupId' })
|
||||||
|
group: Group;
|
||||||
|
|
||||||
|
@CreateDateColumn()
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
@UpdateDateColumn()
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
50
apps/server/src/core/group/entities/group.entity.ts
Normal file
50
apps/server/src/core/group/entities/group.entity.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import {
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
Entity,
|
||||||
|
JoinColumn,
|
||||||
|
ManyToOne,
|
||||||
|
OneToMany,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
UpdateDateColumn,
|
||||||
|
} from 'typeorm';
|
||||||
|
import { GroupUser } from './group-user.entity';
|
||||||
|
import { Workspace } from '../../workspace/entities/workspace.entity';
|
||||||
|
import { User } from '../../user/entities/user.entity';
|
||||||
|
|
||||||
|
@Entity('groups')
|
||||||
|
export class Group {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Column({ length: 255 })
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@Column({ type: 'text', nullable: true })
|
||||||
|
description: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
workspaceId: string;
|
||||||
|
|
||||||
|
@ManyToOne(() => Workspace, (workspace) => workspace.groups, {
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
})
|
||||||
|
@JoinColumn({ name: 'workspaceId' })
|
||||||
|
workspace: Workspace;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
creatorId: string;
|
||||||
|
|
||||||
|
@ManyToOne(() => User)
|
||||||
|
@JoinColumn({ name: 'creatorId' })
|
||||||
|
creator: User;
|
||||||
|
|
||||||
|
@CreateDateColumn()
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
@UpdateDateColumn()
|
||||||
|
updatedAt: Date;
|
||||||
|
|
||||||
|
@OneToMany(() => GroupUser, (groupUser) => groupUser.group)
|
||||||
|
groupUsers: GroupUser[];
|
||||||
|
}
|
||||||
20
apps/server/src/core/group/group.controller.spec.ts
Normal file
20
apps/server/src/core/group/group.controller.spec.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { GroupController } from './group.controller';
|
||||||
|
import { GroupService } from './services/group.service';
|
||||||
|
|
||||||
|
describe('GroupController', () => {
|
||||||
|
let controller: GroupController;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
controllers: [GroupController],
|
||||||
|
providers: [GroupService],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
controller = module.get<GroupController>(GroupController);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(controller).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
121
apps/server/src/core/group/group.controller.ts
Normal file
121
apps/server/src/core/group/group.controller.ts
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Post,
|
||||||
|
Body,
|
||||||
|
UseGuards,
|
||||||
|
HttpCode,
|
||||||
|
HttpStatus,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { GroupService } from './services/group.service';
|
||||||
|
import { CreateGroupDto } from './dto/create-group.dto';
|
||||||
|
import { JwtGuard } from '../auth/guards/jwt.guard';
|
||||||
|
import { AuthUser } from '../../decorators/auth-user.decorator';
|
||||||
|
import { CurrentWorkspace } from '../../decorators/current-workspace.decorator';
|
||||||
|
import { User } from '../user/entities/user.entity';
|
||||||
|
import { Workspace } from '../workspace/entities/workspace.entity';
|
||||||
|
import { GroupUserService } from './services/group-user.service';
|
||||||
|
import { GroupIdDto } from './dto/group-id.dto';
|
||||||
|
import { PaginationOptions } from '../../helpers/pagination/pagination-options';
|
||||||
|
import { AddGroupUserDto } from './dto/add-group-user.dto';
|
||||||
|
import { RemoveGroupUserDto } from './dto/remove-group-user.dto';
|
||||||
|
import { UpdateGroupDto } from './dto/update-group.dto';
|
||||||
|
|
||||||
|
@UseGuards(JwtGuard)
|
||||||
|
@Controller('groups')
|
||||||
|
export class GroupController {
|
||||||
|
constructor(
|
||||||
|
private readonly groupService: GroupService,
|
||||||
|
private readonly groupUserService: GroupUserService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@Post('/')
|
||||||
|
getWorkspaceGroups(
|
||||||
|
@Body() pagination: PaginationOptions,
|
||||||
|
@AuthUser() user: User,
|
||||||
|
@CurrentWorkspace() workspace: Workspace,
|
||||||
|
) {
|
||||||
|
return this.groupService.getGroupsInWorkspace(workspace.id, pagination);
|
||||||
|
}
|
||||||
|
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@Post('/details')
|
||||||
|
getGroup(
|
||||||
|
@Body() groupIdDto: GroupIdDto,
|
||||||
|
@AuthUser() user: User,
|
||||||
|
@CurrentWorkspace() workspace: Workspace,
|
||||||
|
) {
|
||||||
|
return this.groupService.getGroup(groupIdDto.groupId, workspace.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@Post('create')
|
||||||
|
createGroup(
|
||||||
|
@Body() createGroupDto: CreateGroupDto,
|
||||||
|
@AuthUser() user: User,
|
||||||
|
@CurrentWorkspace() workspace: Workspace,
|
||||||
|
) {
|
||||||
|
return this.groupService.createGroup(user, workspace.id, createGroupDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@Post('update')
|
||||||
|
updateGroup(
|
||||||
|
@Body() updateGroupDto: UpdateGroupDto,
|
||||||
|
@AuthUser() user: User,
|
||||||
|
@CurrentWorkspace() workspace: Workspace,
|
||||||
|
) {
|
||||||
|
return this.groupService.updateGroup(workspace.id, updateGroupDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@Post('members')
|
||||||
|
getGroupMembers(
|
||||||
|
@Body() groupIdDto: GroupIdDto,
|
||||||
|
@Body() pagination: PaginationOptions,
|
||||||
|
@CurrentWorkspace() workspace: Workspace,
|
||||||
|
) {
|
||||||
|
return this.groupUserService.getGroupUsers(
|
||||||
|
groupIdDto.groupId,
|
||||||
|
workspace.id,
|
||||||
|
pagination,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@Post('members/add')
|
||||||
|
addGroupMember(
|
||||||
|
@Body() addGroupUserDto: AddGroupUserDto,
|
||||||
|
@AuthUser() user: User,
|
||||||
|
@CurrentWorkspace() workspace: Workspace,
|
||||||
|
) {
|
||||||
|
return this.groupUserService.addUserToGroup(
|
||||||
|
addGroupUserDto.userId,
|
||||||
|
addGroupUserDto.groupId,
|
||||||
|
workspace.id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@Post('members/remove')
|
||||||
|
removeGroupMember(
|
||||||
|
@Body() removeGroupUserDto: RemoveGroupUserDto,
|
||||||
|
//@AuthUser() user: User,
|
||||||
|
//@CurrentWorkspace() workspace: Workspace,
|
||||||
|
) {
|
||||||
|
return this.groupUserService.removeUserFromGroup(
|
||||||
|
removeGroupUserDto.userId,
|
||||||
|
removeGroupUserDto.groupId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@Post('delete')
|
||||||
|
deleteGroup(
|
||||||
|
@Body() groupIdDto: GroupIdDto,
|
||||||
|
@AuthUser() user: User,
|
||||||
|
@CurrentWorkspace() workspace: Workspace,
|
||||||
|
) {
|
||||||
|
return this.groupService.deleteGroup(groupIdDto.groupId, workspace.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
22
apps/server/src/core/group/group.module.ts
Normal file
22
apps/server/src/core/group/group.module.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { GroupService } from './services/group.service';
|
||||||
|
import { GroupController } from './group.controller';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { AuthModule } from '../auth/auth.module';
|
||||||
|
import { Group } from './entities/group.entity';
|
||||||
|
import { GroupUser } from './entities/group-user.entity';
|
||||||
|
import { GroupRepository } from './respositories/group.repository';
|
||||||
|
import { GroupUserRepository } from './respositories/group-user.repository';
|
||||||
|
import { GroupUserService } from './services/group-user.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [TypeOrmModule.forFeature([Group, GroupUser]), AuthModule],
|
||||||
|
controllers: [GroupController],
|
||||||
|
providers: [
|
||||||
|
GroupService,
|
||||||
|
GroupUserService,
|
||||||
|
GroupRepository,
|
||||||
|
GroupUserRepository,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class GroupModule {}
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { DataSource, Repository } from 'typeorm';
|
||||||
|
import { GroupUser } from '../entities/group-user.entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GroupUserRepository extends Repository<GroupUser> {
|
||||||
|
constructor(private dataSource: DataSource) {
|
||||||
|
super(GroupUser, dataSource.createEntityManager());
|
||||||
|
}
|
||||||
|
}
|
||||||
10
apps/server/src/core/group/respositories/group.repository.ts
Normal file
10
apps/server/src/core/group/respositories/group.repository.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { DataSource, Repository } from 'typeorm';
|
||||||
|
import { Group } from '../entities/group.entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GroupRepository extends Repository<Group> {
|
||||||
|
constructor(private dataSource: DataSource) {
|
||||||
|
super(Group, dataSource.createEntityManager());
|
||||||
|
}
|
||||||
|
}
|
||||||
121
apps/server/src/core/group/services/group-user.service.ts
Normal file
121
apps/server/src/core/group/services/group-user.service.ts
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
import {
|
||||||
|
BadRequestException,
|
||||||
|
Injectable,
|
||||||
|
NotFoundException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { DataSource, EntityManager } from 'typeorm';
|
||||||
|
import { GroupUserRepository } from '../respositories/group-user.repository';
|
||||||
|
import { PaginationOptions } from '../../../helpers/pagination/pagination-options';
|
||||||
|
import {
|
||||||
|
WorkspaceUser,
|
||||||
|
WorkspaceUserRole,
|
||||||
|
} from '../../workspace/entities/workspace-user.entity';
|
||||||
|
import { transactionWrapper } from '../../../helpers/db.helper';
|
||||||
|
import { User } from '../../user/entities/user.entity';
|
||||||
|
import { GroupUser } from '../entities/group-user.entity';
|
||||||
|
import { PaginationMetaDto } from '../../../helpers/pagination/pagination-meta-dto';
|
||||||
|
import { PaginatedResult } from '../../../helpers/pagination/paginated-result';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GroupUserService {
|
||||||
|
constructor(
|
||||||
|
private groupUserRepository: GroupUserRepository,
|
||||||
|
private dataSource: DataSource,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async getGroupUsers(
|
||||||
|
groupId,
|
||||||
|
workspaceId: string,
|
||||||
|
paginationOptions: PaginationOptions,
|
||||||
|
): Promise<PaginatedResult<User>> {
|
||||||
|
const [groupUsers, count] = await this.groupUserRepository.findAndCount({
|
||||||
|
relations: ['user'],
|
||||||
|
where: {
|
||||||
|
group: {
|
||||||
|
workspaceId: workspaceId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
take: paginationOptions.limit,
|
||||||
|
skip: paginationOptions.skip,
|
||||||
|
});
|
||||||
|
|
||||||
|
const users = groupUsers.map((groupUser: GroupUser) => groupUser.user);
|
||||||
|
|
||||||
|
const paginationMeta = new PaginationMetaDto({ count, paginationOptions });
|
||||||
|
|
||||||
|
return new PaginatedResult(users, paginationMeta);
|
||||||
|
}
|
||||||
|
|
||||||
|
async addUserToGroup(
|
||||||
|
userId: string,
|
||||||
|
groupId: string,
|
||||||
|
workspaceId: string,
|
||||||
|
manager?: EntityManager,
|
||||||
|
): Promise<WorkspaceUser> {
|
||||||
|
let addedUser;
|
||||||
|
|
||||||
|
await transactionWrapper(
|
||||||
|
async (manager) => {
|
||||||
|
// TODO: make duplicate code reusable
|
||||||
|
const userExists = await manager.exists(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,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!workspaceUser) {
|
||||||
|
throw new NotFoundException('User is not a member of this workspace');
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingGroupUser = await manager.findOneBy(GroupUser, {
|
||||||
|
userId: userId,
|
||||||
|
groupId: groupId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingGroupUser) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
'User is already a member of this group',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupUser = new GroupUser();
|
||||||
|
groupUser.userId = userId;
|
||||||
|
groupUser.groupId = groupId;
|
||||||
|
|
||||||
|
addedUser = await manager.save(groupUser);
|
||||||
|
},
|
||||||
|
this.dataSource,
|
||||||
|
manager,
|
||||||
|
);
|
||||||
|
|
||||||
|
return addedUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeUserFromGroup(userId: string, groupId: string): Promise<void> {
|
||||||
|
const groupUser = await this.findGroupUser(userId, groupId);
|
||||||
|
|
||||||
|
if (!groupUser) {
|
||||||
|
throw new BadRequestException('Group member not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.groupUserRepository.delete({
|
||||||
|
userId,
|
||||||
|
groupId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findGroupUser(userId: string, groupId: string): Promise<GroupUser> {
|
||||||
|
return await this.groupUserRepository.findOneBy({
|
||||||
|
userId,
|
||||||
|
groupId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
18
apps/server/src/core/group/services/group.service.spec.ts
Normal file
18
apps/server/src/core/group/services/group.service.spec.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { GroupService } from './group.service';
|
||||||
|
|
||||||
|
describe('GroupService', () => {
|
||||||
|
let service: GroupService;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [GroupService],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
service = module.get<GroupService>(GroupService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(service).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
82
apps/server/src/core/group/services/group.service.ts
Normal file
82
apps/server/src/core/group/services/group.service.ts
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
import { CreateGroupDto } from '../dto/create-group.dto';
|
||||||
|
import { GroupRepository } from '../respositories/group.repository';
|
||||||
|
import { Group } from '../entities/group.entity';
|
||||||
|
import { plainToInstance } from 'class-transformer';
|
||||||
|
import { User } from '../../user/entities/user.entity';
|
||||||
|
import { PaginationMetaDto } from '../../../helpers/pagination/pagination-meta-dto';
|
||||||
|
import { PaginatedResult } from '../../../helpers/pagination/paginated-result';
|
||||||
|
import { PaginationOptions } from '../../../helpers/pagination/pagination-options';
|
||||||
|
import { UpdateGroupDto } from '../dto/update-group.dto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GroupService {
|
||||||
|
constructor(private groupRepository: GroupRepository) {}
|
||||||
|
|
||||||
|
async createGroup(
|
||||||
|
authUser: User,
|
||||||
|
workspaceId: string,
|
||||||
|
createGroupDto: CreateGroupDto,
|
||||||
|
): Promise<Group> {
|
||||||
|
const group = plainToInstance(Group, createGroupDto);
|
||||||
|
group.creatorId = authUser.id;
|
||||||
|
group.workspaceId = workspaceId;
|
||||||
|
|
||||||
|
return await this.groupRepository.save(group);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateGroup(
|
||||||
|
workspaceId: string,
|
||||||
|
updateGroupDto: UpdateGroupDto,
|
||||||
|
): Promise<Group> {
|
||||||
|
const group = new Group();
|
||||||
|
|
||||||
|
if (updateGroupDto.name) {
|
||||||
|
group.name = updateGroupDto.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateGroupDto.description) {
|
||||||
|
group.description = updateGroupDto.description;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.groupRepository.save(group);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getGroup(groupId: string, workspaceId: string): Promise<Group> {
|
||||||
|
const group = await this.groupRepository.findOneBy({
|
||||||
|
id: groupId,
|
||||||
|
workspaceId: workspaceId,
|
||||||
|
});
|
||||||
|
|
||||||
|
//TODO: get group member count
|
||||||
|
|
||||||
|
if (!group) {
|
||||||
|
throw new NotFoundException('Group not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getGroupsInWorkspace(
|
||||||
|
workspaceId: string,
|
||||||
|
paginationOptions: PaginationOptions,
|
||||||
|
): Promise<PaginatedResult<Group>> {
|
||||||
|
const [groupsInWorkspace, count] = await this.groupRepository.findAndCount({
|
||||||
|
where: {
|
||||||
|
workspaceId: workspaceId,
|
||||||
|
},
|
||||||
|
|
||||||
|
take: paginationOptions.limit,
|
||||||
|
skip: paginationOptions.skip,
|
||||||
|
});
|
||||||
|
|
||||||
|
const paginationMeta = new PaginationMetaDto({ count, paginationOptions });
|
||||||
|
|
||||||
|
return new PaginatedResult(groupsInWorkspace, paginationMeta);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteGroup(groupId: string, workspaceId: string) {
|
||||||
|
await this.getGroup(groupId, workspaceId);
|
||||||
|
await this.groupRepository.delete(groupId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,8 @@
|
|||||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
import {
|
||||||
|
BadRequestException,
|
||||||
|
Injectable,
|
||||||
|
NotFoundException,
|
||||||
|
} from '@nestjs/common';
|
||||||
import { CreateSpaceDto } from './dto/create-space.dto';
|
import { CreateSpaceDto } from './dto/create-space.dto';
|
||||||
import { Space } from './entities/space.entity';
|
import { Space } from './entities/space.entity';
|
||||||
import { plainToInstance } from 'class-transformer';
|
import { plainToInstance } from 'class-transformer';
|
||||||
@ -7,6 +11,8 @@ import { SpaceUserRepository } from './repositories/space-user.repository';
|
|||||||
import { SpaceUser } from './entities/space-user.entity';
|
import { SpaceUser } from './entities/space-user.entity';
|
||||||
import { transactionWrapper } from '../../helpers/db.helper';
|
import { transactionWrapper } from '../../helpers/db.helper';
|
||||||
import { DataSource, EntityManager } from 'typeorm';
|
import { DataSource, EntityManager } from 'typeorm';
|
||||||
|
import { WorkspaceUser } from '../workspace/entities/workspace-user.entity';
|
||||||
|
import { User } from '../user/entities/user.entity';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SpaceService {
|
export class SpaceService {
|
||||||
@ -51,14 +57,33 @@ export class SpaceService {
|
|||||||
userId: string,
|
userId: string,
|
||||||
spaceId: string,
|
spaceId: string,
|
||||||
role: string,
|
role: string,
|
||||||
|
workspaceId,
|
||||||
manager?: EntityManager,
|
manager?: EntityManager,
|
||||||
): Promise<SpaceUser> {
|
): Promise<SpaceUser> {
|
||||||
let addedUser: SpaceUser;
|
let addedUser: SpaceUser;
|
||||||
|
|
||||||
await transactionWrapper(
|
await transactionWrapper(
|
||||||
async (manager: EntityManager) => {
|
async (manager: EntityManager) => {
|
||||||
const existingSpaceUser = await manager.findOne(SpaceUser, {
|
const userExists = await manager.exists(User, {
|
||||||
where: { userId: userId, spaceId: spaceId },
|
where: { id: userId },
|
||||||
|
});
|
||||||
|
if (!userExists) {
|
||||||
|
throw new NotFoundException('User not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// only workspace users can be added to workspace spaces
|
||||||
|
const workspaceUser = await manager.findOneBy(WorkspaceUser, {
|
||||||
|
userId: userId,
|
||||||
|
workspaceId: workspaceId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!workspaceUser) {
|
||||||
|
throw new NotFoundException('User is not a member of this workspace');
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingSpaceUser = await manager.findOneBy(SpaceUser, {
|
||||||
|
userId: userId,
|
||||||
|
spaceId: spaceId,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (existingSpaceUser) {
|
if (existingSpaceUser) {
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import { Page } from '../../page/entities/page.entity';
|
|||||||
import { WorkspaceInvitation } from './workspace-invitation.entity';
|
import { WorkspaceInvitation } from './workspace-invitation.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 { Group } from '../../group/entities/group.entity';
|
||||||
|
|
||||||
@Entity('workspaces')
|
@Entity('workspaces')
|
||||||
export class Workspace {
|
export class Workspace {
|
||||||
@ -82,4 +83,7 @@ export class Workspace {
|
|||||||
|
|
||||||
@OneToMany(() => Space, (space) => space.workspace)
|
@OneToMany(() => Space, (space) => space.workspace)
|
||||||
spaces: [];
|
spaces: [];
|
||||||
|
|
||||||
|
@OneToMany(() => Group, (group) => group.workspace)
|
||||||
|
groups: [];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,10 +34,6 @@ export class WorkspaceUserService {
|
|||||||
|
|
||||||
await transactionWrapper(
|
await transactionWrapper(
|
||||||
async (manager) => {
|
async (manager) => {
|
||||||
const existingWorkspaceUser = await manager.findOne(WorkspaceUser, {
|
|
||||||
where: { userId: userId, workspaceId: workspaceId },
|
|
||||||
});
|
|
||||||
|
|
||||||
const userExists = await manager.exists(User, {
|
const userExists = await manager.exists(User, {
|
||||||
where: { id: userId },
|
where: { id: userId },
|
||||||
});
|
});
|
||||||
@ -45,6 +41,11 @@ export class WorkspaceUserService {
|
|||||||
throw new NotFoundException('User not found');
|
throw new NotFoundException('User not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const existingWorkspaceUser = await manager.findOneBy(WorkspaceUser, {
|
||||||
|
userId: userId,
|
||||||
|
workspaceId: workspaceId,
|
||||||
|
});
|
||||||
|
|
||||||
if (existingWorkspaceUser) {
|
if (existingWorkspaceUser) {
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
'User is already a member of this workspace',
|
'User is already a member of this workspace',
|
||||||
|
|||||||
@ -82,6 +82,7 @@ export class WorkspaceService {
|
|||||||
userId,
|
userId,
|
||||||
createdSpace.id,
|
createdSpace.id,
|
||||||
WorkspaceUserRole.OWNER,
|
WorkspaceUserRole.OWNER,
|
||||||
|
createdWorkspace.id,
|
||||||
manager,
|
manager,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -110,6 +111,7 @@ export class WorkspaceService {
|
|||||||
userId,
|
userId,
|
||||||
firstWorkspace[0].defaultSpaceId,
|
firstWorkspace[0].defaultSpaceId,
|
||||||
WorkspaceUserRole.MEMBER,
|
WorkspaceUserRole.MEMBER,
|
||||||
|
firstWorkspace[0].id,
|
||||||
manager,
|
manager,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
24
apps/server/src/database/migrations/1709644512305-Groups.ts
Normal file
24
apps/server/src/database/migrations/1709644512305-Groups.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
|
export class Groups1709644512305 implements MigrationInterface {
|
||||||
|
name = 'Groups1709644512305'
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`CREATE TABLE "group_users" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "userId" uuid NOT NULL, "groupId" uuid NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "UQ_129c2cb846b1f4beedf4c6373b5" UNIQUE ("groupId", "userId"), CONSTRAINT "PK_5df8869cdeffc693bd083153bcf" PRIMARY KEY ("id"))`);
|
||||||
|
await queryRunner.query(`CREATE TABLE "groups" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying(255) NOT NULL, "description" text, "workspaceId" uuid NOT NULL, "creatorId" uuid NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_659d1483316afb28afd3a90646e" PRIMARY KEY ("id"))`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "group_users" ADD CONSTRAINT "FK_ad937045ed48b757293b2011d36" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "group_users" ADD CONSTRAINT "FK_ba2d59b482905354e872896dba8" FOREIGN KEY ("groupId") REFERENCES "groups"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "groups" ADD CONSTRAINT "FK_cce5e5fec33dae0fcc991795b4a" FOREIGN KEY ("workspaceId") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||||
|
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 "FK_cce5e5fec33dae0fcc991795b4a"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "group_users" DROP CONSTRAINT "FK_ba2d59b482905354e872896dba8"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "group_users" DROP CONSTRAINT "FK_ad937045ed48b757293b2011d36"`);
|
||||||
|
await queryRunner.query(`DROP TABLE "groups"`);
|
||||||
|
await queryRunner.query(`DROP TABLE "group_users"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user