mirror of
https://github.com/docmost/docmost.git
synced 2025-11-20 13:31:08 +10:00
Refactoring
* replace TypeORM with Kysely query builder * refactor migrations * other changes and fixes
This commit is contained in:
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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 });
|
||||
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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 {}
|
||||
|
||||
Reference in New Issue
Block a user