Refactoring

* replace TypeORM with Kysely query builder
* refactor migrations
* other changes and fixes
This commit is contained in:
Philipinho
2024-03-29 01:46:11 +00:00
parent cacb5606b1
commit c18c9ae02b
122 changed files with 2619 additions and 3541 deletions

View File

@ -1,43 +0,0 @@
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, {
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;
}

View File

@ -1,61 +0,0 @@
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';
import { Unique } from 'typeorm';
import { SpaceMember } from '../../space/entities/space-member.entity';
@Entity('groups')
@Unique(['name', 'workspaceId'])
export class Group {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ length: 255 })
name: string;
@Column({ type: 'text', nullable: true })
description: string;
@Column({ type: 'boolean', default: false })
isDefault: boolean;
@Column()
workspaceId: string;
@ManyToOne(() => Workspace, (workspace) => workspace.groups, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'workspaceId' })
workspace: Workspace;
@Column({ nullable: true })
creatorId: string;
@ManyToOne(() => User)
@JoinColumn({ name: 'creatorId' })
creator: User;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
@OneToMany(() => GroupUser, (groupUser) => groupUser.group)
groupUsers: GroupUser[];
@OneToMany(() => SpaceMember, (spaceMembership) => spaceMembership.group)
spaces: SpaceMember[];
memberCount?: number;
}

View File

@ -10,8 +10,6 @@ import { GroupService } from './services/group.service';
import { CreateGroupDto } from './dto/create-group.dto';
import { AuthUser } from '../../decorators/auth-user.decorator';
import { AuthWorkspace } from '../../decorators/auth-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';
@ -19,12 +17,11 @@ import { AddGroupUserDto } from './dto/add-group-user.dto';
import { RemoveGroupUserDto } from './dto/remove-group-user.dto';
import { UpdateGroupDto } from './dto/update-group.dto';
import { Action } from '../casl/ability.action';
import { Group } from './entities/group.entity';
import { GroupUser } from './entities/group-user.entity';
import { PoliciesGuard } from '../casl/guards/policies.guard';
import { CheckPolicies } from '../casl/decorators/policies.decorator';
import { AppAbility } from '../casl/abilities/casl-ability.factory';
import { JwtAuthGuard } from '../../guards/jwt-auth.guard';
import { User, Workspace } from '@docmost/db/types/entity.types';
@UseGuards(JwtAuthGuard)
@Controller('groups')
@ -45,7 +42,7 @@ export class GroupController {
}
@UseGuards(PoliciesGuard)
@CheckPolicies((ability: AppAbility) => ability.can(Action.Read, Group))
@CheckPolicies((ability: AppAbility) => ability.can(Action.Read, 'Group'))
@HttpCode(HttpStatus.OK)
@Post('/info')
getGroup(
@ -57,7 +54,7 @@ export class GroupController {
}
@UseGuards(PoliciesGuard)
@CheckPolicies((ability: AppAbility) => ability.can(Action.Manage, Group))
@CheckPolicies((ability: AppAbility) => ability.can(Action.Manage, 'Group'))
@HttpCode(HttpStatus.OK)
@Post('create')
createGroup(
@ -69,7 +66,7 @@ export class GroupController {
}
@UseGuards(PoliciesGuard)
@CheckPolicies((ability: AppAbility) => ability.can(Action.Manage, Group))
@CheckPolicies((ability: AppAbility) => ability.can(Action.Manage, 'Group'))
@HttpCode(HttpStatus.OK)
@Post('update')
updateGroup(
@ -81,7 +78,7 @@ export class GroupController {
}
@UseGuards(PoliciesGuard)
@CheckPolicies((ability: AppAbility) => ability.can(Action.Read, GroupUser))
@CheckPolicies((ability: AppAbility) => ability.can(Action.Read, 'GroupUser'))
@HttpCode(HttpStatus.OK)
@Post('members')
getGroupMembers(
@ -97,7 +94,9 @@ export class GroupController {
}
@UseGuards(PoliciesGuard)
@CheckPolicies((ability: AppAbility) => ability.can(Action.Manage, GroupUser))
@CheckPolicies((ability: AppAbility) =>
ability.can(Action.Manage, 'GroupUser'),
)
@HttpCode(HttpStatus.OK)
@Post('members/add')
addGroupMember(
@ -113,7 +112,9 @@ export class GroupController {
}
@UseGuards(PoliciesGuard)
@CheckPolicies((ability: AppAbility) => ability.can(Action.Manage, GroupUser))
@CheckPolicies((ability: AppAbility) =>
ability.can(Action.Manage, 'GroupUser'),
)
@HttpCode(HttpStatus.OK)
@Post('members/remove')
removeGroupMember(
@ -129,7 +130,7 @@ export class GroupController {
}
@UseGuards(PoliciesGuard)
@CheckPolicies((ability: AppAbility) => ability.can(Action.Manage, Group))
@CheckPolicies((ability: AppAbility) => ability.can(Action.Manage, 'Group'))
@HttpCode(HttpStatus.OK)
@Post('delete')
deleteGroup(

View File

@ -1,22 +1,11 @@
import { Module } from '@nestjs/common';
import { GroupService } from './services/group.service';
import { GroupController } from './group.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
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])],
controllers: [GroupController],
providers: [
GroupService,
GroupUserService,
GroupRepository,
GroupUserRepository,
],
providers: [GroupService, GroupUserService],
exports: [GroupService, GroupUserService],
})
export class GroupModule {}

View File

@ -1,10 +0,0 @@
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());
}
}

View File

@ -1,10 +0,0 @@
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());
}
}

View File

@ -1,48 +1,35 @@
import {
BadRequestException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { DataSource, EntityManager } from 'typeorm';
import { GroupUserRepository } from '../respositories/group-user.repository';
import { BadRequestException, Injectable } from '@nestjs/common';
import { PaginationOptions } from '../../../helpers/pagination/pagination-options';
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';
import { Group } from '../entities/group.entity';
import { GroupService } from './group.service';
import { KyselyDB, KyselyTransaction } from '@docmost/db/types/kysely.types';
import { executeTx } from '@docmost/db/utils';
import { InjectKysely } from 'nestjs-kysely';
import { GroupRepo } from '@docmost/db/repos/group/group.repo';
import { GroupUserRepo } from '@docmost/db/repos/group/group-user.repo';
import { User } from '@docmost/db/types/entity.types';
@Injectable()
export class GroupUserService {
constructor(
private groupUserRepository: GroupUserRepository,
private groupRepo: GroupRepo,
private groupUserRepo: GroupUserRepo,
private groupService: GroupService,
private dataSource: DataSource,
@InjectKysely() private readonly db: KyselyDB,
) {}
async getGroupUsers(
groupId,
groupId: string,
workspaceId: string,
paginationOptions: PaginationOptions,
): Promise<PaginatedResult<User>> {
await this.groupService.findAndValidateGroup(groupId, workspaceId);
const [groupUsers, count] = await this.groupUserRepository.findAndCount({
relations: ['user'],
where: {
groupId: groupId,
group: {
workspaceId: workspaceId,
},
},
take: paginationOptions.limit,
skip: paginationOptions.skip,
});
const users = groupUsers.map((groupUser: GroupUser) => groupUser.user);
const { users, count } = await this.groupUserRepo.getGroupUsersPaginated(
groupId,
paginationOptions,
);
const paginationMeta = new PaginationMetaDto({ count, paginationOptions });
@ -52,23 +39,18 @@ export class GroupUserService {
async addUserToDefaultGroup(
userId: string,
workspaceId: string,
manager?: EntityManager,
trx?: KyselyTransaction,
): Promise<void> {
return await transactionWrapper(
async (manager) => {
const defaultGroup = await this.groupService.getDefaultGroup(
await executeTx(
this.db,
async (trx) => {
const defaultGroup = await this.groupRepo.getDefaultGroup(
workspaceId,
manager,
);
await this.addUserToGroup(
userId,
defaultGroup.id,
workspaceId,
manager,
trx,
);
await this.addUserToGroup(userId, defaultGroup.id, workspaceId, trx);
},
this.dataSource,
manager,
trx,
);
}
@ -76,46 +58,33 @@ export class GroupUserService {
userId: string,
groupId: string,
workspaceId: string,
manager?: EntityManager,
): Promise<GroupUser> {
return await transactionWrapper(
async (manager) => {
const group = await manager.findOneBy(Group, {
id: groupId,
workspaceId: workspaceId,
});
trx?: KyselyTransaction,
): Promise<void> {
await executeTx(
this.db,
async (trx) => {
await this.groupService.findAndValidateGroup(groupId, workspaceId);
const groupUserExists = await this.groupUserRepo.getGroupUserById(
userId,
groupId,
trx,
);
if (!group) {
throw new NotFoundException('Group not found');
}
const userExists = await manager.exists(User, {
where: { id: userId, workspaceId },
});
if (!userExists) {
throw new NotFoundException('User not found');
}
const existingGroupUser = await manager.findOneBy(GroupUser, {
userId: userId,
groupId: groupId,
});
if (existingGroupUser) {
if (groupUserExists) {
throw new BadRequestException(
'User is already a member of this group',
);
}
const groupUser = new GroupUser();
groupUser.userId = userId;
groupUser.groupId = groupId;
return manager.save(groupUser);
await this.groupUserRepo.insertGroupUser(
{
userId,
groupId,
},
trx,
);
},
this.dataSource,
manager,
trx,
);
}
@ -135,22 +104,15 @@ export class GroupUserService {
);
}
const groupUser = await this.getGroupUser(userId, groupId);
const groupUser = await this.groupUserRepo.getGroupUserById(
userId,
groupId,
);
if (!groupUser) {
throw new BadRequestException('Group member not found');
}
await this.groupUserRepository.delete({
userId,
groupId,
});
}
async getGroupUser(userId: string, groupId: string): Promise<GroupUser> {
return await this.groupUserRepository.findOneBy({
userId,
groupId,
});
await this.groupUserRepo.delete(userId, groupId);
}
}

View File

@ -4,87 +4,64 @@ import {
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';
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';
import { DataSource, EntityManager } from 'typeorm';
import { transactionWrapper } from '../../../helpers/db.helper';
import { KyselyTransaction } from '@docmost/db/types/kysely.types';
import { GroupRepo } from '@docmost/db/repos/group/group.repo';
import { Group, InsertableGroup, User } from '@docmost/db/types/entity.types';
@Injectable()
export class GroupService {
constructor(
private groupRepository: GroupRepository,
private dataSource: DataSource,
) {}
constructor(private groupRepo: GroupRepo) {}
async createGroup(
authUser: User,
workspaceId: string,
createGroupDto: CreateGroupDto,
trx?: KyselyTransaction,
): Promise<Group> {
const group = plainToInstance(Group, createGroupDto);
group.creatorId = authUser.id;
group.workspaceId = workspaceId;
const groupExists = await this.findGroupByName(
const groupExists = await this.groupRepo.findByName(
createGroupDto.name,
workspaceId,
);
if (groupExists) {
throw new BadRequestException('Group name already exists');
}
const insertableGroup: InsertableGroup = {
name: createGroupDto.name,
description: createGroupDto.description,
isDefault: false,
creatorId: authUser.id,
workspaceId: workspaceId,
};
return await this.groupRepository.save(group);
return await this.groupRepo.insertGroup(insertableGroup, trx);
}
async createDefaultGroup(
workspaceId: string,
userId?: string,
manager?: EntityManager,
trx?: KyselyTransaction,
): 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,
);
const insertableGroup: InsertableGroup = {
name: DefaultGroup.EVERYONE,
isDefault: true,
creatorId: userId ?? null,
workspaceId: workspaceId,
};
return await this.groupRepo.insertGroup(insertableGroup, trx);
}
async updateGroup(
workspaceId: string,
updateGroupDto: UpdateGroupDto,
): Promise<Group> {
const group = await this.groupRepository.findOneBy({
id: updateGroupDto.groupId,
workspaceId: workspaceId,
});
const group = await this.groupRepo.findById(
updateGroupDto.groupId,
workspaceId,
);
if (!group) {
throw new NotFoundException('Group not found');
@ -94,7 +71,7 @@ export class GroupService {
throw new BadRequestException('You cannot update a default group');
}
const groupExists = await this.findGroupByName(
const groupExists = await this.groupRepo.findByName(
updateGroupDto.name,
workspaceId,
);
@ -110,20 +87,21 @@ export class GroupService {
group.description = updateGroupDto.description;
}
return await this.groupRepository.save(group);
await this.groupRepo.update(
{
name: updateGroupDto.name,
description: updateGroupDto.description,
},
group.id,
workspaceId,
);
return group;
}
async getGroupInfo(groupId: string, workspaceId: string): Promise<Group> {
const group = await this.groupRepository
.createQueryBuilder('group')
.where('group.id = :groupId', { groupId })
.andWhere('group.workspaceId = :workspaceId', { workspaceId })
.loadRelationCountAndMap(
'group.memberCount',
'group.groupUsers',
'groupUsers',
)
.getOne();
// todo: add member count
const group = await this.groupRepo.findById(groupId, workspaceId);
if (!group) {
throw new NotFoundException('Group not found');
@ -136,17 +114,10 @@ export class GroupService {
workspaceId: string,
paginationOptions: PaginationOptions,
): Promise<PaginatedResult<Group>> {
const [groups, count] = await this.groupRepository
.createQueryBuilder('group')
.where('group.workspaceId = :workspaceId', { workspaceId })
.loadRelationCountAndMap(
'group.memberCount',
'group.groupUsers',
'groupUsers',
)
.take(paginationOptions.limit)
.skip(paginationOptions.skip)
.getManyAndCount();
const { groups, count } = await this.groupRepo.getGroupsPaginated(
workspaceId,
paginationOptions,
);
const paginationMeta = new PaginationMetaDto({ count, paginationOptions });
@ -158,34 +129,18 @@ export class GroupService {
if (group.isDefault) {
throw new BadRequestException('You cannot delete a default group');
}
await this.groupRepository.delete(groupId);
await this.groupRepo.delete(groupId, workspaceId);
}
async findAndValidateGroup(
groupId: string,
workspaceId: string,
): Promise<Group> {
const group = await this.groupRepository.findOne({
where: {
id: groupId,
workspaceId: workspaceId,
},
});
const group = await this.groupRepo.findById(groupId, workspaceId);
if (!group) {
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();
}
}