switch to nx monorepo

This commit is contained in:
Philipinho
2024-01-09 18:58:26 +01:00
parent e1bb2632b8
commit 093e634c0b
273 changed files with 11419 additions and 31 deletions

View File

@ -0,0 +1,20 @@
import { Test, TestingModule } from '@nestjs/testing';
import { WorkspaceController } from './workspace.controller';
import { WorkspaceService } from '../services/workspace.service';
describe('WorkspaceController', () => {
let controller: WorkspaceController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [WorkspaceController],
providers: [WorkspaceService],
}).compile();
controller = module.get<WorkspaceController>(WorkspaceController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@ -0,0 +1,120 @@
import {
Body,
Controller,
Delete,
Get,
HttpCode,
HttpStatus,
Post,
Req,
UseGuards,
} from '@nestjs/common';
import { WorkspaceService } from '../services/workspace.service';
import { FastifyRequest } from 'fastify';
import { JwtGuard } from '../../auth/guards/JwtGuard';
import { UpdateWorkspaceDto } from '../dto/update-workspace.dto';
import { CreateWorkspaceDto } from '../dto/create-workspace.dto';
import { DeleteWorkspaceDto } from '../dto/delete-workspace.dto';
import { UpdateWorkspaceUserRoleDto } from '../dto/update-workspace-user-role.dto';
import { RemoveWorkspaceUserDto } from '../dto/remove-workspace-user.dto';
import { AddWorkspaceUserDto } from '../dto/add-workspace-user.dto';
@UseGuards(JwtGuard)
@Controller('workspace')
export class WorkspaceController {
constructor(private readonly workspaceService: WorkspaceService) {}
@HttpCode(HttpStatus.OK)
@Post('create')
async createWorkspace(
@Req() req: FastifyRequest,
@Body() createWorkspaceDto: CreateWorkspaceDto,
) {
const jwtPayload = req['user'];
const userId = jwtPayload.sub;
return this.workspaceService.create(userId, createWorkspaceDto);
}
@HttpCode(HttpStatus.OK)
@Post('update')
async updateWorkspace(
@Req() req: FastifyRequest,
@Body() updateWorkspaceDto: UpdateWorkspaceDto,
) {
const jwtPayload = req['user'];
const workspaceId = (
await this.workspaceService.getUserCurrentWorkspace(jwtPayload.sub)
).id;
return this.workspaceService.update(workspaceId, updateWorkspaceDto);
}
@HttpCode(HttpStatus.OK)
@Post('delete')
async deleteWorkspace(@Body() deleteWorkspaceDto: DeleteWorkspaceDto) {
return this.workspaceService.delete(deleteWorkspaceDto);
}
@HttpCode(HttpStatus.OK)
@Get('members')
async getWorkspaceMembers(@Req() req: FastifyRequest) {
const jwtPayload = req['user'];
const workspaceId = (
await this.workspaceService.getUserCurrentWorkspace(jwtPayload.sub)
).id;
return this.workspaceService.getWorkspaceUsers(workspaceId);
}
@HttpCode(HttpStatus.OK)
@Post('members/add')
async addWorkspaceMember(
@Req() req: FastifyRequest,
@Body() addWorkspaceUserDto: AddWorkspaceUserDto,
) {
const jwtPayload = req['user'];
const workspaceId = (
await this.workspaceService.getUserCurrentWorkspace(jwtPayload.sub)
).id;
return this.workspaceService.addUserToWorkspace(
addWorkspaceUserDto.userId,
workspaceId,
addWorkspaceUserDto.role,
);
}
@HttpCode(HttpStatus.OK)
@Delete('members/delete')
async removeWorkspaceMember(
@Req() req: FastifyRequest,
@Body() removeWorkspaceUserDto: RemoveWorkspaceUserDto,
) {
const jwtPayload = req['user'];
const workspaceId = (
await this.workspaceService.getUserCurrentWorkspace(jwtPayload.sub)
).id;
return this.workspaceService.removeUserFromWorkspace(
removeWorkspaceUserDto.userId,
workspaceId,
);
}
@HttpCode(HttpStatus.OK)
@Post('members/role')
async updateWorkspaceMemberRole(
@Req() req: FastifyRequest,
@Body() workspaceUserRoleDto: UpdateWorkspaceUserRoleDto,
) {
const jwtPayload = req['user'];
const workspaceId = (
await this.workspaceService.getUserCurrentWorkspace(jwtPayload.sub)
).id;
return this.workspaceService.updateWorkspaceUserRole(
workspaceUserRoleDto,
workspaceId,
);
}
}

View File

@ -0,0 +1,11 @@
import { IsNotEmpty, IsString, IsUUID } from 'class-validator';
export class AddWorkspaceUserDto {
@IsNotEmpty()
@IsUUID()
userId: string;
@IsNotEmpty()
@IsString()
role: string;
}

View File

@ -0,0 +1,18 @@
import { IsOptional, IsString, MaxLength, MinLength } from 'class-validator';
export class CreateWorkspaceDto {
@MinLength(4)
@MaxLength(64)
@IsString()
name: string;
@IsOptional()
@MinLength(4)
@MaxLength(30)
@IsString()
hostname?: string;
@IsOptional()
@IsString()
description?: string;
}

View File

@ -0,0 +1,6 @@
import { IsString } from 'class-validator';
export class DeleteWorkspaceDto {
@IsString()
workspaceId: string;
}

View File

@ -0,0 +1,7 @@
import { IsNotEmpty, IsUUID } from 'class-validator';
export class RemoveWorkspaceUserDto {
@IsNotEmpty()
@IsUUID()
userId: string;
}

View File

@ -0,0 +1,11 @@
import { IsNotEmpty, IsString, IsUUID } from 'class-validator';
export class UpdateWorkspaceUserRoleDto {
@IsNotEmpty()
@IsUUID()
userId: string;
@IsNotEmpty()
@IsString()
role: string;
}

View File

@ -0,0 +1,9 @@
import { PartialType } from '@nestjs/mapped-types';
import { CreateWorkspaceDto } from './create-workspace.dto';
import { IsOptional, IsString } from 'class-validator';
export class UpdateWorkspaceDto extends PartialType(CreateWorkspaceDto) {
@IsOptional()
@IsString()
logo: string;
}

View File

@ -0,0 +1,48 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { Workspace } from './workspace.entity';
import { User } from '../../user/entities/user.entity';
@Entity('workspace_invitations')
export class WorkspaceInvitation {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
workspaceId: string;
@ManyToOne(() => Workspace, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'workspaceId' })
workspace: Workspace;
@Column()
invitedById: string;
@ManyToOne(() => User)
@JoinColumn({ name: 'invitedById' })
invitedBy: User;
@Column({ length: 255 })
email: string;
@Column({ length: 100, nullable: true })
role: string;
@Column({ length: 100, nullable: true })
status: string;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}

View File

@ -0,0 +1,46 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
ManyToOne,
JoinColumn,
Unique,
} from 'typeorm';
import { Workspace } from './workspace.entity';
import { User } from '../../user/entities/user.entity';
@Entity('workspace_users')
@Unique(['workspaceId', 'userId'])
export class WorkspaceUser {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
userId: string;
@ManyToOne(() => User, (user) => user.workspaceUsers, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'userId' })
user: User;
@Column()
workspaceId: string;
@ManyToOne(() => Workspace, (workspace) => workspace.workspaceUsers, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'workspaceId' })
workspace: Workspace;
@Column({ length: 100, nullable: true })
role: string;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}

View File

@ -0,0 +1,73 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
ManyToOne,
OneToMany,
JoinColumn,
} from 'typeorm';
import { User } from '../../user/entities/user.entity';
import { WorkspaceUser } from './workspace-user.entity';
import { Page } from '../../page/entities/page.entity';
import { WorkspaceInvitation } from './workspace-invitation.entity';
import { Comment } from '../../comment/entities/comment.entity';
@Entity('workspaces')
export class Workspace {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ length: 255, nullable: true })
name: string;
@Column({ type: 'text', nullable: true })
description: string;
@Column({ length: 255, nullable: true })
logo: string;
@Column({ length: 255, nullable: true, unique: true })
hostname: string;
@Column({ length: 255, nullable: true })
customDomain: string;
@Column({ type: 'boolean', default: true })
enableInvite: boolean;
@Column({ length: 255, unique: true, nullable: true })
inviteCode: string;
@Column({ type: 'jsonb', nullable: true })
settings: any;
@Column()
creatorId: string;
@ManyToOne(() => User, (user) => user.workspaces)
@JoinColumn({ name: 'creatorId' })
creator: User;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
@OneToMany(() => WorkspaceUser, (workspaceUser) => workspaceUser.workspace)
workspaceUsers: WorkspaceUser[];
@OneToMany(
() => WorkspaceInvitation,
(workspaceInvitation) => workspaceInvitation.workspace,
)
workspaceInvitations: WorkspaceInvitation[];
@OneToMany(() => Page, (page) => page.workspace)
pages: Page[];
@OneToMany(() => Comment, (comment) => comment.workspace)
comments: Comment[];
}

View File

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

View File

@ -0,0 +1,14 @@
import { Injectable } from '@nestjs/common';
import { DataSource, Repository } from 'typeorm';
import { Workspace } from '../entities/workspace.entity';
@Injectable()
export class WorkspaceRepository extends Repository<Workspace> {
constructor(private dataSource: DataSource) {
super(Workspace, dataSource.createEntityManager());
}
async findById(workspaceId: string) {
return this.findOneBy({ id: workspaceId });
}
}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { WorkspaceService } from './workspace.service';
describe('WorkspaceService', () => {
let service: WorkspaceService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [WorkspaceService],
}).compile();
service = module.get<WorkspaceService>(WorkspaceService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -0,0 +1,201 @@
import {
BadRequestException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { CreateWorkspaceDto } from '../dto/create-workspace.dto';
import { WorkspaceRepository } from '../repositories/workspace.repository';
import { WorkspaceUserRepository } from '../repositories/workspace-user.repository';
import { WorkspaceUser } from '../entities/workspace-user.entity';
import { Workspace } from '../entities/workspace.entity';
import { plainToInstance } from 'class-transformer';
import { v4 as uuid } from 'uuid';
import { generateHostname } from '../workspace.util';
import { UpdateWorkspaceDto } from '../dto/update-workspace.dto';
import { DeleteWorkspaceDto } from '../dto/delete-workspace.dto';
import { UpdateWorkspaceUserRoleDto } from '../dto/update-workspace-user-role.dto';
@Injectable()
export class WorkspaceService {
constructor(
private workspaceRepository: WorkspaceRepository,
private workspaceUserRepository: WorkspaceUserRepository,
) {}
async findById(workspaceId: string): Promise<Workspace> {
return this.workspaceRepository.findById(workspaceId);
}
async save(workspace: Workspace) {
return this.workspaceRepository.save(workspace);
}
async create(
userId: string,
createWorkspaceDto?: CreateWorkspaceDto,
): Promise<Workspace> {
let workspace: Workspace;
if (createWorkspaceDto) {
workspace = plainToInstance(Workspace, createWorkspaceDto);
} else {
workspace = new Workspace();
}
workspace.inviteCode = uuid();
workspace.creatorId = userId;
if (workspace.name && !workspace.hostname?.trim()) {
workspace.hostname = generateHostname(createWorkspaceDto.name);
}
workspace = await this.workspaceRepository.save(workspace);
await this.addUserToWorkspace(userId, workspace.id, 'owner');
return workspace;
}
async update(
workspaceId: string,
updateWorkspaceDto: UpdateWorkspaceDto,
): Promise<Workspace> {
const workspace = await this.workspaceRepository.findById(workspaceId);
if (!workspace) {
throw new NotFoundException('Workspace not found');
}
if (updateWorkspaceDto.name) {
workspace.name = updateWorkspaceDto.name;
}
if (updateWorkspaceDto.logo) {
workspace.logo = updateWorkspaceDto.logo;
}
return this.workspaceRepository.save(workspace);
}
async delete(deleteWorkspaceDto: DeleteWorkspaceDto): Promise<void> {
const workspace = await this.workspaceRepository.findById(
deleteWorkspaceDto.workspaceId,
);
if (!workspace) {
throw new NotFoundException('Workspace not found');
}
//TODO
// remove all existing users from workspace
// delete workspace
}
async addUserToWorkspace(
userId: string,
workspaceId: string,
role: string,
): Promise<WorkspaceUser> {
const existingWorkspaceUser = await this.workspaceUserRepository.findOne({
where: { userId: userId, workspaceId: workspaceId },
});
if (existingWorkspaceUser) {
throw new BadRequestException('User already added to this workspace');
}
const workspaceUser = new WorkspaceUser();
workspaceUser.userId = userId;
workspaceUser.workspaceId = workspaceId;
workspaceUser.role = role;
return this.workspaceUserRepository.save(workspaceUser);
}
async updateWorkspaceUserRole(
workspaceUserRoleDto: UpdateWorkspaceUserRoleDto,
workspaceId: string,
) {
const workspaceUser = await this.workspaceUserRepository.findOne({
where: { userId: workspaceUserRoleDto.userId, workspaceId: workspaceId },
});
if (!workspaceUser) {
throw new BadRequestException('user is not a member of this workspace');
}
if (workspaceUser.role === workspaceUserRoleDto.role) {
return workspaceUser;
}
workspaceUser.role = workspaceUserRoleDto.role;
// TODO: if there is only one workspace owner, prevent the role change
return this.workspaceUserRepository.save(workspaceUser);
}
async removeUserFromWorkspace(
userId: string,
workspaceId: string,
): Promise<void> {
await this.validateWorkspaceMember(userId, workspaceId);
await this.workspaceUserRepository.delete({
userId,
workspaceId,
});
}
async getUserCurrentWorkspace(userId: string): Promise<Workspace> {
// TODO: use workspaceId and fetch workspace based on the id
// we currently assume the user belongs to one workspace
const userWorkspace = await this.workspaceUserRepository.findOne({
where: { userId: userId },
relations: ['workspace'],
});
return userWorkspace.workspace;
}
async getUserWorkspaces(userId: string): Promise<Workspace[]> {
const workspaces = await this.workspaceUserRepository.find({
where: { userId: userId },
relations: ['workspace'],
});
return workspaces.map(
(userWorkspace: WorkspaceUser) => userWorkspace.workspace,
);
}
async getWorkspaceUsers(workspaceId: string) {
const workspace = await this.workspaceRepository.findOne({
where: { id: workspaceId },
relations: ['workspaceUsers', 'workspaceUsers.user'],
});
if (!workspace) {
throw new BadRequestException('Invalid workspace');
}
const users = workspace.workspaceUsers.map((workspaceUser) => {
workspaceUser.user.password = '';
return {
...workspaceUser.user,
workspaceRole: workspaceUser.role,
};
});
return { users };
}
async validateWorkspaceMember(
userId: string,
workspaceId: string,
): Promise<void> {
const workspaceUser = await this.workspaceUserRepository.findOne({
where: { userId, workspaceId },
});
if (!workspaceUser) {
throw new BadRequestException('User is not a member of this workspace');
}
}
}

View File

@ -0,0 +1,21 @@
import { Module } from '@nestjs/common';
import { WorkspaceService } from './services/workspace.service';
import { WorkspaceController } from './controllers/workspace.controller';
import { WorkspaceRepository } from './repositories/workspace.repository';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Workspace } from './entities/workspace.entity';
import { WorkspaceUser } from './entities/workspace-user.entity';
import { WorkspaceInvitation } from './entities/workspace-invitation.entity';
import { WorkspaceUserRepository } from './repositories/workspace-user.repository';
import { AuthModule } from '../auth/auth.module';
@Module({
imports: [
TypeOrmModule.forFeature([Workspace, WorkspaceUser, WorkspaceInvitation]),
AuthModule,
],
controllers: [WorkspaceController],
providers: [WorkspaceService, WorkspaceRepository, WorkspaceUserRepository],
exports: [WorkspaceService, WorkspaceRepository, WorkspaceUserRepository],
})
export class WorkspaceModule {}

View File

@ -0,0 +1,5 @@
export function generateHostname(name: string): string {
let hostname = name.replace(/[^a-z0-9]/gi, '').toLowerCase();
hostname = hostname.substring(0, 30);
return hostname;
}