From a8b6edd4a8a6463b434c3b863ad27e47ee6e752a Mon Sep 17 00:00:00 2001 From: gitstart-twenty <140154534+gitstart-twenty@users.noreply.github.com> Date: Sat, 18 Nov 2023 05:41:46 +0800 Subject: [PATCH] chore(server): Migrate workspace (#2530) * Migrate workspace Co-authored-by: v1b3m * Migrate workspace Co-authored-by: v1b3m * Migrate workspace to TypeORM Co-authored-by: v1b3m * Migrate workspace to TypeORM Co-authored-by: v1b3m --------- Co-authored-by: v1b3m Co-authored-by: gitstart-twenty --- server/src/core/core.module.ts | 3 + .../workspace/dtos/update-workspace-input.ts | 31 ++++++ .../services/workspace.service.spec.ts | 28 +++++ .../workspace/services/workspace.service.ts | 103 ++++++++++++++++++ .../workspace/workspace.auto-resolver-opts.ts | 40 +++++++ .../src/coreV2/workspace/workspace.entity.ts | 46 ++++++++ .../src/coreV2/workspace/workspace.module.ts | 31 ++++++ .../coreV2/workspace/workspace.resolver.ts | 66 +++++++++++ 8 files changed, 348 insertions(+) create mode 100644 server/src/coreV2/workspace/dtos/update-workspace-input.ts create mode 100644 server/src/coreV2/workspace/services/workspace.service.spec.ts create mode 100644 server/src/coreV2/workspace/services/workspace.service.ts create mode 100644 server/src/coreV2/workspace/workspace.auto-resolver-opts.ts create mode 100644 server/src/coreV2/workspace/workspace.entity.ts create mode 100644 server/src/coreV2/workspace/workspace.module.ts create mode 100644 server/src/coreV2/workspace/workspace.resolver.ts diff --git a/server/src/core/core.module.ts b/server/src/core/core.module.ts index 678d9134e..3cce419bc 100644 --- a/server/src/core/core.module.ts +++ b/server/src/core/core.module.ts @@ -3,6 +3,7 @@ import { Module } from '@nestjs/common'; import { WebHookModule } from 'src/core/web-hook/web-hook.module'; import { UserModule as UserV2Module } from 'src/coreV2/user/user.module'; import { RefreshTokenModule as RefreshTokenV2Module } from 'src/coreV2/refresh-token/refresh-token.module'; +import { WorkspaceModule as WorkspaceV2Module } from 'src/coreV2/workspace/workspace.module'; import { UserModule } from './user/user.module'; import { CommentModule } from './comment/comment.module'; @@ -38,6 +39,7 @@ import { ApiKeyModule } from './api-key/api-key.module'; WebHookModule, UserV2Module, RefreshTokenV2Module, + WorkspaceV2Module, ], exports: [ AuthModule, @@ -54,6 +56,7 @@ import { ApiKeyModule } from './api-key/api-key.module'; WebHookModule, UserV2Module, RefreshTokenV2Module, + WorkspaceV2Module, ], }) export class CoreModule {} diff --git a/server/src/coreV2/workspace/dtos/update-workspace-input.ts b/server/src/coreV2/workspace/dtos/update-workspace-input.ts new file mode 100644 index 000000000..e950ed2c7 --- /dev/null +++ b/server/src/coreV2/workspace/dtos/update-workspace-input.ts @@ -0,0 +1,31 @@ +import { Field, InputType } from '@nestjs/graphql'; + +import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; + +// FIXME: We might not need this +@InputType() +export class UpdateWorkspaceInput { + @Field() + @IsString() + @IsNotEmpty() + @IsOptional() + domainName?: string; + + @Field() + @IsString() + @IsNotEmpty() + @IsOptional() + displayName?: string; + + @Field() + @IsString() + @IsNotEmpty() + @IsOptional() + logo?: string; + + @Field() + @IsString() + @IsNotEmpty() + @IsOptional() + inviteHash?: string; +} diff --git a/server/src/coreV2/workspace/services/workspace.service.spec.ts b/server/src/coreV2/workspace/services/workspace.service.spec.ts new file mode 100644 index 000000000..a39a7ef86 --- /dev/null +++ b/server/src/coreV2/workspace/services/workspace.service.spec.ts @@ -0,0 +1,28 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; + +import { Workspace } from 'src/coreV2/workspace/workspace.entity'; + +import { WorkspaceService } from './workspace.service'; + +describe('WorkspaceService', () => { + let service: WorkspaceService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + WorkspaceService, + { + provide: getRepositoryToken(Workspace), + useValue: {}, + }, + ], + }).compile(); + + service = module.get(WorkspaceService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/server/src/coreV2/workspace/services/workspace.service.ts b/server/src/coreV2/workspace/services/workspace.service.ts new file mode 100644 index 000000000..769a456fd --- /dev/null +++ b/server/src/coreV2/workspace/services/workspace.service.ts @@ -0,0 +1,103 @@ +import { InjectRepository } from '@nestjs/typeorm'; + +import assert from 'assert'; + +import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm'; +import { Repository } from 'typeorm'; + +import { Workspace } from 'src/coreV2/workspace/workspace.entity'; +import { TenantManagerService } from 'src/tenant-manager/tenant-manager.service'; + +export class WorkspaceService extends TypeOrmQueryService { + constructor( + @InjectRepository(Workspace) + private readonly workspaceRepository: Repository, + private readonly tenantManagerService: TenantManagerService, + ) { + super(workspaceRepository); + } + + async deleteWorkspace(id: string) { + const workspace = await this.workspaceRepository.findOneBy({ id }); + assert(workspace, 'Workspace not found'); + + // await this.deleteWorkspaceRelations(id); + + await this.tenantManagerService.delete(id); + + return workspace; + } + + // // FIXME: The rest of the entities are not defined so we can't use this + // async deleteWorkspaceRelations(workspaceId: string) { + // const queryRunner = + // this.workspaceRepository.manager.connection.createQueryRunner(); + // await queryRunner.connect(); + + // await queryRunner.startTransaction(); + + // try { + // await queryRunner.manager.delete(PipelineProgress, { + // workspaceId, + // }); + + // await queryRunner.manager.delete(Company, { + // workspaceId, + // }); + + // await queryRunner.manager.delete(Person, { + // workspaceId, + // }); + + // await queryRunner.manager.delete(PipelineStage, { + // workspaceId, + // }); + + // await queryRunner.manager.delete(WorkspaceMember, { + // workspaceId, + // }); + + // await queryRunner.manager.delete(Attachment, { + // workspaceId, + // }); + + // await queryRunner.manager.delete(Comment, { + // workspaceId, + // }); + + // await queryRunner.manager.delete(ActivityTarget, { + // workspaceId, + // }); + + // await queryRunner.manager.delete(Activity, { + // workspaceId, + // }); + + // await queryRunner.manager.delete(ApiKey, { + // workspaceId, + // }); + + // await queryRunner.manager.delete(Favorite, { + // workspaceId, + // }); + + // await queryRunner.manager.delete(WebHook, { + // workspaceId, + // }); + + // await queryRunner.manager.delete(WebHook, { + // workspaceId, + // }); + + // await queryRunner.manager.delete(Workspace, { + // id: workspaceId, + // }); + + // await queryRunner.commitTransaction(); + // } catch { + // await queryRunner.rollbackTransaction(); + // } finally { + // await queryRunner.release(); + // } + // } +} diff --git a/server/src/coreV2/workspace/workspace.auto-resolver-opts.ts b/server/src/coreV2/workspace/workspace.auto-resolver-opts.ts new file mode 100644 index 000000000..f94a17ff1 --- /dev/null +++ b/server/src/coreV2/workspace/workspace.auto-resolver-opts.ts @@ -0,0 +1,40 @@ +import { + AutoResolverOpts, + PagingStrategies, + ReadResolverOpts, +} from '@ptc-org/nestjs-query-graphql'; +import { SortDirection } from '@ptc-org/nestjs-query-core'; + +import { JwtAuthGuard } from 'src/guards/jwt.auth.guard'; +import { UpdateWorkspaceInput } from 'src/coreV2/workspace/dtos/update-workspace-input'; + +import { Workspace } from './workspace.entity'; + +export const workspaceAutoResolverOpts: AutoResolverOpts< + any, + any, + unknown, + unknown, + ReadResolverOpts, + PagingStrategies +>[] = [ + { + EntityClass: Workspace, + DTOClass: Workspace, + UpdateDTOClass: UpdateWorkspaceInput, + enableTotalCount: true, + pagingStrategy: PagingStrategies.CURSOR, + read: { + defaultSort: [{ field: 'id', direction: SortDirection.DESC }], + }, + create: { + many: { disabled: true }, + one: { disabled: true }, + }, + update: { + many: { disabled: true }, + }, + delete: { many: { disabled: true }, one: { disabled: true } }, + guards: [JwtAuthGuard], + }, +]; diff --git a/server/src/coreV2/workspace/workspace.entity.ts b/server/src/coreV2/workspace/workspace.entity.ts new file mode 100644 index 000000000..3a614a206 --- /dev/null +++ b/server/src/coreV2/workspace/workspace.entity.ts @@ -0,0 +1,46 @@ +import { Field, ID, ObjectType } from '@nestjs/graphql'; + +import { IDField } from '@ptc-org/nestjs-query-graphql'; +import { + Column, + CreateDateColumn, + Entity, + PrimaryGeneratedColumn, + UpdateDateColumn, +} from 'typeorm'; + +@Entity('workspaces') +@ObjectType('workspace') +export class Workspace { + @IDField(() => ID) + @PrimaryGeneratedColumn('uuid') + id: string; + + @Field({ nullable: true }) + @Column({ nullable: true }) + domainName?: string; + + @Field({ nullable: true }) + @Column({ nullable: true }) + displayName?: string; + + @Field({ nullable: true }) + @Column({ nullable: true }) + logo?: string; + + @Field({ nullable: true }) + @Column({ nullable: true }) + inviteHash?: string; + + @Field({ nullable: true }) + @Column({ nullable: true }) + deletedAt?: Date; + + @Field() + @CreateDateColumn({ type: 'timestamp with time zone' }) + createdAt: Date; + + @Field() + @UpdateDateColumn({ type: 'timestamp with time zone' }) + updatedAt: Date; +} diff --git a/server/src/coreV2/workspace/workspace.module.ts b/server/src/coreV2/workspace/workspace.module.ts new file mode 100644 index 000000000..d11b5dd44 --- /dev/null +++ b/server/src/coreV2/workspace/workspace.module.ts @@ -0,0 +1,31 @@ +import { Module } from '@nestjs/common'; + +import { NestjsQueryGraphQLModule } from '@ptc-org/nestjs-query-graphql'; +import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm'; + +import { TenantManagerModule } from 'src/tenant-manager/tenant-manager.module'; +import { WorkspaceResolver } from 'src/coreV2/workspace/workspace.resolver'; +import { FileModule } from 'src/core/file/file.module'; +import { AbilityModule } from 'src/ability/ability.module'; + +import { Workspace } from './workspace.entity'; +import { workspaceAutoResolverOpts } from './workspace.auto-resolver-opts'; + +import { WorkspaceService } from './services/workspace.service'; + +@Module({ + imports: [ + NestjsQueryGraphQLModule.forFeature({ + imports: [ + NestjsQueryTypeOrmModule.forFeature([Workspace]), + TenantManagerModule, + FileModule, + AbilityModule, + ], + services: [WorkspaceService], + resolvers: workspaceAutoResolverOpts, + }), + ], + providers: [WorkspaceResolver], +}) +export class WorkspaceModule {} diff --git a/server/src/coreV2/workspace/workspace.resolver.ts b/server/src/coreV2/workspace/workspace.resolver.ts new file mode 100644 index 000000000..35bec01f8 --- /dev/null +++ b/server/src/coreV2/workspace/workspace.resolver.ts @@ -0,0 +1,66 @@ +import { Resolver, Query, Args, Mutation } from '@nestjs/graphql'; +import { UseGuards } from '@nestjs/common'; + +import { FileUpload, GraphQLUpload } from 'graphql-upload'; + +import { FileFolder } from 'src/core/file/interfaces/file-folder.interface'; + +import { streamToBuffer } from 'src/utils/stream-to-buffer'; +import { FileUploadService } from 'src/core/file/services/file-upload.service'; +import { AuthWorkspace } from 'src/decorators/auth-workspace.decorator'; +import { assert } from 'src/utils/assert'; +import { JwtAuthGuard } from 'src/guards/jwt.auth.guard'; +import { DeleteWorkspaceAbilityHandler } from 'src/ability/handlers/workspace.ability-handler'; +import { CheckAbilities } from 'src/decorators/check-abilities.decorator'; +import { AbilityGuard } from 'src/guards/ability.guard'; + +import { Workspace } from './workspace.entity'; + +import { WorkspaceService } from './services/workspace.service'; + +@UseGuards(JwtAuthGuard) +@Resolver(() => Workspace) +export class WorkspaceResolver { + constructor( + private readonly workspaceService: WorkspaceService, + private readonly fileUploadService: FileUploadService, + ) {} + + @Query(() => Workspace) + async currentWorkspace(@AuthWorkspace() { id }: Workspace) { + const workspace = await this.workspaceService.findById(id); + assert(workspace, 'User not found'); + return workspace; + } + + @Mutation(() => String) + async uploadWorkspaceLogo( + @AuthWorkspace() { id }: Workspace, + @Args({ name: 'file', type: () => GraphQLUpload }) + { createReadStream, filename, mimetype }: FileUpload, + ): Promise { + const stream = createReadStream(); + const buffer = await streamToBuffer(stream); + const fileFolder = FileFolder.WorkspaceLogo; + + const { paths } = await this.fileUploadService.uploadImage({ + file: buffer, + filename, + mimeType: mimetype, + fileFolder, + }); + + await this.workspaceService.updateOne(id, { + logo: paths[0], + }); + + return paths[0]; + } + + @Mutation(() => Workspace) + @UseGuards(AbilityGuard) + @CheckAbilities(DeleteWorkspaceAbilityHandler) + async deleteCurrentWorkspace(@AuthWorkspace() { id }: Workspace) { + return this.workspaceService.deleteWorkspace(id); + } +}