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,69 +0,0 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
ManyToOne,
JoinColumn,
Unique,
Check,
} from 'typeorm';
import { User } from '../../user/entities/user.entity';
import { Space } from './space.entity';
import { Group } from '../../group/entities/group.entity';
@Entity('space_members')
// allow either userId or groupId
@Check(
'CHK_allow_userId_or_groupId',
`("userId" IS NOT NULL AND "groupId" IS NULL) OR ("userId" IS NULL AND "groupId" IS NOT NULL)`,
)
@Unique(['spaceId', 'userId'])
@Unique(['spaceId', 'groupId'])
export class SpaceMember {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ nullable: true })
userId: string;
@ManyToOne(() => User, (user) => user.spaces, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'userId' })
user: User;
@Column({ nullable: true })
groupId: string;
@ManyToOne(() => Group, (group) => group.spaces, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'groupId' })
group: Group;
@Column()
spaceId: string;
@ManyToOne(() => Space, (space) => space.spaceMembers, {
onDelete: 'CASCADE',
})
space: Space;
@Column({ length: 100 })
role: string;
@Column({ nullable: true })
creatorId: string;
@ManyToOne(() => User)
@JoinColumn({ name: 'creatorId' })
creator: User;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}

View File

@ -1,69 +0,0 @@
import {
Column,
CreateDateColumn,
Entity,
JoinColumn,
ManyToOne,
OneToMany,
PrimaryGeneratedColumn,
Unique,
UpdateDateColumn,
} from 'typeorm';
import { User } from '../../user/entities/user.entity';
import { Workspace } from '../../workspace/entities/workspace.entity';
import { Page } from '../../page/entities/page.entity';
import { SpaceVisibility, SpaceRole } from '../../../helpers/types/permission';
import { SpaceMember } from './space-member.entity';
@Entity('spaces')
@Unique(['slug', 'workspaceId'])
export class Space {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ length: 255, nullable: true })
name: string;
@Column({ type: 'text', nullable: true })
description: string;
@Column({ nullable: true })
slug: string;
@Column({ length: 255, nullable: true })
icon: string;
@Column({ length: 100, default: SpaceVisibility.OPEN })
visibility: string;
@Column({ length: 100, default: SpaceRole.WRITER })
defaultRole: string;
@Column({ nullable: true })
creatorId: string;
@ManyToOne(() => User)
@JoinColumn({ name: 'creatorId' })
creator: User;
@Column()
workspaceId: string;
@ManyToOne(() => Workspace, (workspace) => workspace.spaces, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'workspaceId' })
workspace: Workspace;
@OneToMany(() => SpaceMember, (spaceMember) => spaceMember.space)
spaceMembers: SpaceMember[];
@OneToMany(() => Page, (page) => page.space)
pages: Page[];
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}

View File

@ -1,10 +0,0 @@
import { Injectable } from '@nestjs/common';
import { DataSource, Repository } from 'typeorm';
import { SpaceMember } from '../entities/space-member.entity';
@Injectable()
export class SpaceMemberRepository extends Repository<SpaceMember> {
constructor(private dataSource: DataSource) {
super(SpaceMember, dataSource.createEntityManager());
}
}

View File

@ -1,18 +0,0 @@
import { Injectable } from '@nestjs/common';
import { DataSource, Repository } from 'typeorm';
import { Space } from '../entities/space.entity';
@Injectable()
export class SpaceRepository extends Repository<Space> {
constructor(private dataSource: DataSource) {
super(Space, dataSource.createEntityManager());
}
async findById(spaceId: string, workspaceId: string): Promise<Space> {
const queryBuilder = this.dataSource.createQueryBuilder(Space, 'space');
return await queryBuilder
.where('space.id = :id', { id: spaceId })
.andWhere('space.workspaceId = :workspaceId', { workspaceId })
.getOne();
}
}

View File

@ -1,65 +1,62 @@
import {
BadRequestException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { SpaceRepository } from '../repositories/space.repository';
import { transactionWrapper } from '../../../helpers/db.helper';
import { DataSource, EntityManager, IsNull, Not } from 'typeorm';
import { User } from '../../user/entities/user.entity';
import { Injectable } from '@nestjs/common';
import { PaginationOptions } from '../../../helpers/pagination/pagination-options';
import { PaginationMetaDto } from '../../../helpers/pagination/pagination-meta-dto';
import { PaginatedResult } from '../../../helpers/pagination/paginated-result';
import { Group } from '../../group/entities/group.entity';
import { SpaceMemberRepository } from '../repositories/space-member.repository';
import { SpaceMember } from '../entities/space-member.entity';
import { KyselyTransaction } from '@docmost/db/types/kysely.types';
import { SpaceMemberRepo } from '@docmost/db/repos/space/space-member.repo';
import { SpaceMember } from '@docmost/db/types/entity.types';
@Injectable()
export class SpaceMemberService {
constructor(
private spaceRepository: SpaceRepository,
private spaceMemberRepository: SpaceMemberRepository,
private dataSource: DataSource,
) {}
constructor(private spaceMemberRepo: SpaceMemberRepo) {}
async addUserToSpace(
userId: string,
spaceId: string,
role: string,
workspaceId,
manager?: EntityManager,
workspaceId: string,
trx?: KyselyTransaction,
): Promise<SpaceMember> {
return await transactionWrapper(
async (manager: EntityManager) => {
const userExists = await manager.exists(User, {
where: { id: userId, workspaceId },
});
if (!userExists) {
throw new NotFoundException('User not found');
}
const existingSpaceUser = await manager.findOneBy(SpaceMember, {
userId: userId,
spaceId: spaceId,
});
if (existingSpaceUser) {
throw new BadRequestException('User already added to this space');
}
const spaceMember = new SpaceMember();
spaceMember.userId = userId;
spaceMember.spaceId = spaceId;
spaceMember.role = role;
await manager.save(spaceMember);
return spaceMember;
//if (existingSpaceUser) {
// throw new BadRequestException('User already added to this space');
// }
return await this.spaceMemberRepo.insertSpaceMember(
{
userId: userId,
spaceId: spaceId,
role: role,
},
this.dataSource,
manager,
trx,
);
}
async addGroupToSpace(
groupId: string,
spaceId: string,
role: string,
workspaceId: string,
trx?: KyselyTransaction,
): Promise<SpaceMember> {
//const existingSpaceUser = await manager.findOneBy(SpaceMember, {
// userId: userId,
// spaceId: spaceId,
// });
// validations?
return await this.spaceMemberRepo.insertSpaceMember(
{
groupId: groupId,
spaceId: spaceId,
role: role,
},
trx,
);
}
/*
* get spaces a user is a member of
* either by direct membership or via groups
*/
/*
async getUserSpaces(
userId: string,
workspaceId: string,
@ -79,152 +76,31 @@ export class SpaceMemberService {
.skip(paginationOptions.skip)
.getManyAndCount();
/*
const getUserSpacesViaGroup = this.spaceRepository
.createQueryBuilder('space')
.leftJoin('space.spaceGroups', 'spaceGroup')
.leftJoin('spaceGroup.group', 'group')
.leftJoin('group.groupUsers', 'groupUser')
.where('groupUser.userId = :userId', { userId })
.andWhere('space.workspaceId = :workspaceId', { workspaceId })
.getManyAndCount();
console.log(await getUserSpacesViaGroup);
*/
const spaces = userSpaces.map((userSpace) => userSpace.space);
const paginationMeta = new PaginationMetaDto({ count, paginationOptions });
return new PaginatedResult(spaces, paginationMeta);
}
*/
/*
* get members of a space.
* can be a group or user
*/
async getSpaceMembers(
spaceId: string,
workspaceId: string,
paginationOptions: PaginationOptions,
) {
const [spaceMembers, count] = await this.spaceMemberRepository.findAndCount(
{
relations: ['user', 'group'],
where: {
space: {
id: spaceId,
workspaceId,
},
},
order: {
createdAt: 'ASC',
},
take: paginationOptions.limit,
skip: paginationOptions.skip,
},
);
const members = await Promise.all(
spaceMembers.map(async (member) => {
let memberInfo = {};
if (member.user) {
memberInfo = {
id: member.user.id,
name: member.user.name,
email: member.user.email,
avatarUrl: member.user.avatarUrl,
type: 'user',
};
} else if (member.group) {
const memberCount = await this.dataSource.getRepository(Group).count({
where: {
id: member.groupId,
workspaceId,
},
});
memberInfo = {
id: member.group.id,
name: member.group.name,
isDefault: member.group.isDefault,
memberCount: memberCount,
type: 'group',
};
}
return {
...memberInfo,
role: member.role,
};
}),
);
//todo: validate the space is inside the workspace
const { members, count } =
await this.spaceMemberRepo.getSpaceMembersPaginated(
spaceId,
paginationOptions,
);
const paginationMeta = new PaginationMetaDto({ count, paginationOptions });
return new PaginatedResult(members, paginationMeta);
}
async addGroupToSpace(
groupId: string,
spaceId: string,
role: string,
workspaceId,
manager?: EntityManager,
): Promise<SpaceMember> {
return await transactionWrapper(
async (manager: EntityManager) => {
const groupExists = await manager.exists(Group, {
where: { id: groupId, workspaceId },
});
if (!groupExists) {
throw new NotFoundException('Group not found');
}
const existingSpaceGroup = await manager.findOneBy(SpaceMember, {
groupId: groupId,
spaceId: spaceId,
});
if (existingSpaceGroup) {
throw new BadRequestException('Group already added to this space');
}
const spaceMember = new SpaceMember();
spaceMember.groupId = groupId;
spaceMember.spaceId = spaceId;
spaceMember.role = role;
await manager.save(spaceMember);
return spaceMember;
},
this.dataSource,
manager,
);
}
async getSpaceGroup(
spaceId: string,
workspaceId: string,
paginationOptions: PaginationOptions,
) {
const [spaceGroups, count] = await this.spaceMemberRepository.findAndCount({
relations: ['group'],
where: {
groupId: Not(IsNull()),
space: {
id: spaceId,
workspaceId,
},
},
take: paginationOptions.limit,
skip: paginationOptions.skip,
});
// TODO: add group memberCount
const groups = spaceGroups.map((spaceGroup) => {
return {
...spaceGroup.group,
spaceRole: spaceGroup.role,
};
});
const paginationMeta = new PaginationMetaDto({ count, paginationOptions });
return new PaginatedResult(groups, paginationMeta);
}
}
// 231 lines

View File

@ -1,59 +1,46 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { CreateSpaceDto } from '../dto/create-space.dto';
import { Space } from '../entities/space.entity';
import { SpaceRepository } from '../repositories/space.repository';
import { transactionWrapper } from '../../../helpers/db.helper';
import { DataSource, EntityManager } from 'typeorm';
import { PaginationOptions } from '../../../helpers/pagination/pagination-options';
import { PaginationMetaDto } from '../../../helpers/pagination/pagination-meta-dto';
import { PaginatedResult } from '../../../helpers/pagination/paginated-result';
import { SpaceMemberRepository } from '../repositories/space-member.repository';
import slugify from 'slugify';
import { SpaceRepo } from '@docmost/db/repos/space/space.repo';
import { KyselyTransaction } from '@docmost/db/types/kysely.types';
import { getRandomInt } from '../../../helpers/utils';
import { Space } from '@docmost/db/types/entity.types';
@Injectable()
export class SpaceService {
constructor(
private spaceRepository: SpaceRepository,
private spaceMemberRepository: SpaceMemberRepository,
private dataSource: DataSource,
) {}
constructor(private spaceRepo: SpaceRepo) {}
async create(
userId: string,
workspaceId: string,
createSpaceDto?: CreateSpaceDto,
manager?: EntityManager,
createSpaceDto: CreateSpaceDto,
trx?: KyselyTransaction,
): Promise<Space> {
return await transactionWrapper(
async (manager: EntityManager) => {
const space = new Space();
space.name = createSpaceDto.name ?? 'untitled space ';
space.description = createSpaceDto.description ?? '';
space.creatorId = userId;
space.workspaceId = workspaceId;
// until we allow slug in dto
let slug = slugify(createSpaceDto.name.toLowerCase());
const slugExists = await this.spaceRepo.slugExists(slug, workspaceId);
if (slugExists) {
slug = `${slug}-${getRandomInt()}`;
}
space.slug = slugify(space.name.toLowerCase()); // TODO: check for duplicate
await manager.save(space);
return space;
return await this.spaceRepo.insertSpace(
{
name: createSpaceDto.name ?? 'untitled space',
description: createSpaceDto.description ?? '',
creatorId: userId,
workspaceId: workspaceId,
slug: slug,
},
this.dataSource,
manager,
trx,
);
}
async getSpaceInfo(spaceId: string, workspaceId: string): Promise<Space> {
const space = await this.spaceRepository
.createQueryBuilder('space')
.where('space.id = :spaceId', { spaceId })
.andWhere('space.workspaceId = :workspaceId', { workspaceId })
.loadRelationCountAndMap(
'space.memberCount',
'space.spaceMembers',
'spaceMembers',
) // TODO: add groups to memberCount
.getOne();
// TODO: add memberCount
const space = await this.spaceRepo.findById(spaceId, workspaceId);
if (!space) {
throw new NotFoundException('Space not found');
}
@ -65,17 +52,10 @@ export class SpaceService {
workspaceId: string,
paginationOptions: PaginationOptions,
): Promise<PaginatedResult<Space>> {
const [spaces, count] = await this.spaceRepository
.createQueryBuilder('space')
.where('space.workspaceId = :workspaceId', { workspaceId })
.loadRelationCountAndMap(
'space.memberCount',
'space.spaceMembers',
'spaceMembers',
) // TODO: add groups to memberCount
.take(paginationOptions.limit)
.skip(paginationOptions.skip)
.getManyAndCount();
const { spaces, count } = await this.spaceRepo.getSpacesInWorkspace(
workspaceId,
paginationOptions,
);
const paginationMeta = new PaginationMetaDto({ count, paginationOptions });

View File

@ -8,13 +8,12 @@ import {
} from '@nestjs/common';
import { SpaceService } from './services/space.service';
import { AuthUser } from '../../decorators/auth-user.decorator';
import { User } from '../user/entities/user.entity';
import { AuthWorkspace } from '../../decorators/auth-workspace.decorator';
import { Workspace } from '../workspace/entities/workspace.entity';
import { JwtAuthGuard } from '../../guards/jwt-auth.guard';
import { SpaceIdDto } from './dto/space-id.dto';
import { PaginationOptions } from '../../helpers/pagination/pagination-options';
import { SpaceMemberService } from './services/space-member.service';
import { User, Workspace } from '@docmost/db/types/entity.types';
@UseGuards(JwtAuthGuard)
@Controller('spaces')
@ -37,6 +36,7 @@ export class SpaceController {
}
// get all spaces user is a member of
/*
@HttpCode(HttpStatus.OK)
@Post('user')
async getUserSpaces(
@ -50,7 +50,7 @@ export class SpaceController {
workspace.id,
pagination,
);
}
}*/
@HttpCode(HttpStatus.OK)
@Post('info')

View File

@ -1,22 +1,11 @@
import { Module } from '@nestjs/common';
import { SpaceService } from './services/space.service';
import { SpaceController } from './space.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Space } from './entities/space.entity';
import { SpaceRepository } from './repositories/space.repository';
import { SpaceMember } from './entities/space-member.entity';
import { SpaceMemberRepository } from './repositories/space-member.repository';
import { SpaceMemberService } from './services/space-member.service';
@Module({
imports: [TypeOrmModule.forFeature([Space, SpaceMember])],
controllers: [SpaceController],
providers: [
SpaceService,
SpaceMemberService,
SpaceRepository,
SpaceMemberRepository,
],
providers: [SpaceService, SpaceMemberService],
exports: [SpaceService, SpaceMemberService],
})
export class SpaceModule {}