From 35ab0a18f7534b71fe66f58271806243df41bb2c Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Thu, 21 Mar 2024 11:00:01 +0100 Subject: [PATCH 1/6] Build remote server --- packages/twenty-server/.env.example | 1 + packages/twenty-server/.env.test | 2 + .../1711017124938-addRemoteServerTable.ts | 15 ++++ .../graphql/metadata-graphql-api.module.ts | 6 +- .../src/engine/core-modules/auth/auth.util.ts | 41 ++++++++++ .../environment/environment-variables.ts | 3 + .../metadata-engine.module.ts | 3 + .../dtos/create-remote-server.input.ts | 31 ++++++++ .../dtos/delete-remote-server.input.ts | 9 +++ .../remote-server/dtos/remote-server.dto.ts | 44 +++++++++++ .../remote-server/remote-server.entity.ts | 45 +++++++++++ .../remote-server/remote-server.module.ts | 17 ++++ .../remote-server/remote-server.resolver.ts | 32 ++++++++ .../remote-server/remote-server.service.ts | 77 +++++++++++++++++++ 14 files changed, 323 insertions(+), 3 deletions(-) create mode 100644 packages/twenty-server/src/database/typeorm/metadata/migrations/1711017124938-addRemoteServerTable.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/create-remote-server.input.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/delete-remote-server.input.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/remote-server.dto.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.entity.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.module.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.resolver.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.service.ts diff --git a/packages/twenty-server/.env.example b/packages/twenty-server/.env.example index 4959f2e8207f..f41c452eac0b 100644 --- a/packages/twenty-server/.env.example +++ b/packages/twenty-server/.env.example @@ -9,6 +9,7 @@ ACCESS_TOKEN_SECRET=replace_me_with_a_random_string_access LOGIN_TOKEN_SECRET=replace_me_with_a_random_string_login REFRESH_TOKEN_SECRET=replace_me_with_a_random_string_refresh FILE_TOKEN_SECRET=replace_me_with_a_random_string_refresh +IV_SECRET=replace_me_with_a_random_string_iv SIGN_IN_PREFILLED=true # ———————— Optional ———————— diff --git a/packages/twenty-server/.env.test b/packages/twenty-server/.env.test index 5e090b245f64..44253c93a3cd 100644 --- a/packages/twenty-server/.env.test +++ b/packages/twenty-server/.env.test @@ -9,6 +9,8 @@ FRONT_BASE_URL=http://localhost:3001 ACCESS_TOKEN_SECRET=secret_jwt LOGIN_TOKEN_SECRET=secret_login_tokens REFRESH_TOKEN_SECRET=secret_refresh_token +FILE_TOKEN_SECRET=replace_me_with_a_random_string_refresh +IV_SECRET=replace_me_with_a_random_string_iv # ———————— Optional ———————— # DEBUG_MODE=false diff --git a/packages/twenty-server/src/database/typeorm/metadata/migrations/1711017124938-addRemoteServerTable.ts b/packages/twenty-server/src/database/typeorm/metadata/migrations/1711017124938-addRemoteServerTable.ts new file mode 100644 index 000000000000..ba9a0915bfa1 --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/metadata/migrations/1711017124938-addRemoteServerTable.ts @@ -0,0 +1,15 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddRemoteServerTable1711017124938 implements MigrationInterface { + name = 'AddRemoteServerTable1711017124938'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "metadata"."remoteServer" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "fdwId" uuid NOT NULL DEFAULT uuid_generate_v4(), "host" character varying, "port" character varying, "database" character varying, "username" character varying, "password" character varying, "schema" character varying, "workspaceId" uuid NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_8e5d208498fa2c9710bb934023a" PRIMARY KEY ("id"))`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE "metadata"."remoteServer"`); + } +} diff --git a/packages/twenty-server/src/engine/api/graphql/metadata-graphql-api.module.ts b/packages/twenty-server/src/engine/api/graphql/metadata-graphql-api.module.ts index 5bfadcee7417..0ef80a6452c7 100644 --- a/packages/twenty-server/src/engine/api/graphql/metadata-graphql-api.module.ts +++ b/packages/twenty-server/src/engine/api/graphql/metadata-graphql-api.module.ts @@ -5,12 +5,12 @@ import { YogaDriverConfig, YogaDriver } from '@graphql-yoga/nestjs'; import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module'; import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module'; +import { MetadataEngineModule } from 'src/engine/metadata-modules/metadata-engine.module'; +import { CreateContextFactory } from 'src/engine/api/graphql/graphql-config/factories/create-context.factory'; +import { GraphQLConfigModule } from 'src/engine/api/graphql/graphql-config/graphql-config.module'; import { metadataModuleFactory } from 'src/engine/api/graphql/metadata.module-factory'; import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; import { ExceptionHandlerService } from 'src/engine/integrations/exception-handler/exception-handler.service'; -import { GraphQLConfigModule } from 'src/engine/api/graphql/graphql-config/graphql-config.module'; -import { CreateContextFactory } from 'src/engine/api/graphql/graphql-config/factories/create-context.factory'; -import { MetadataEngineModule } from 'src/engine/metadata-modules/metadata-engine.module'; @Module({ imports: [ diff --git a/packages/twenty-server/src/engine/core-modules/auth/auth.util.ts b/packages/twenty-server/src/engine/core-modules/auth/auth.util.ts index 6fe8a920b0c5..0a3346be4c16 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/auth.util.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/auth.util.ts @@ -1,3 +1,5 @@ +import { createCipheriv, createDecipheriv, createHash } from 'crypto'; + import * as bcrypt from 'bcrypt'; export const PASSWORD_REGEX = /^.{8,}$/; @@ -13,3 +15,42 @@ export const hashPassword = async (password: string) => { export const compareHash = async (password: string, passwordHash: string) => { return bcrypt.compare(password, passwordHash); }; + +export const encryptText = async ( + textToEncrypt: string, + key: string, + iv: string, +): Promise => { + const keyHash = createHash('sha512') + .update(key) + .digest('hex') + .substring(0, 32); + + const ivHash = createHash('sha512').update(iv).digest('hex').substring(0, 16); + + const cipher = createCipheriv('aes-256-ctr', keyHash, ivHash); + + return Buffer.concat([cipher.update(textToEncrypt), cipher.final()]).toString( + 'base64', + ); +}; + +export const decryptText = async ( + textToDecrypt: string, + key: string, + iv: string, +) => { + const keyHash = createHash('sha512') + .update(key) + .digest('hex') + .substring(0, 32); + + const ivHash = createHash('sha512').update(iv).digest('hex').substring(0, 16); + + const decipher = createDecipheriv('aes-256-ctr', keyHash, ivHash); + + return Buffer.concat([ + decipher.update(Buffer.from(textToDecrypt, 'base64')), + decipher.final(), + ]).toString(); +}; diff --git a/packages/twenty-server/src/engine/integrations/environment/environment-variables.ts b/packages/twenty-server/src/engine/integrations/environment/environment-variables.ts index 452c643352b9..ff20c19c963b 100644 --- a/packages/twenty-server/src/engine/integrations/environment/environment-variables.ts +++ b/packages/twenty-server/src/engine/integrations/environment/environment-variables.ts @@ -146,6 +146,9 @@ export class EnvironmentVariables { @IsOptional() FILE_TOKEN_EXPIRES_IN: string = '1d'; + @IsString() + IV_SECRET: string; + // Auth @IsUrl({ require_tld: false }) @IsOptional() diff --git a/packages/twenty-server/src/engine/metadata-modules/metadata-engine.module.ts b/packages/twenty-server/src/engine/metadata-modules/metadata-engine.module.ts index 9eb3ea468253..9e096c8e018f 100644 --- a/packages/twenty-server/src/engine/metadata-modules/metadata-engine.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/metadata-engine.module.ts @@ -4,6 +4,7 @@ import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-s import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module'; import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module'; import { RelationMetadataModule } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.module'; +import { RemoteServerModule } from 'src/engine/metadata-modules/remote-server/remote-server.module'; import { WorkspaceCacheVersionModule } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.module'; import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module'; @@ -15,6 +16,7 @@ import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace- RelationMetadataModule, WorkspaceCacheVersionModule, WorkspaceMigrationModule, + RemoteServerModule, ], providers: [], exports: [ @@ -22,6 +24,7 @@ import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace- FieldMetadataModule, ObjectMetadataModule, RelationMetadataModule, + RemoteServerModule, ], }) export class MetadataEngineModule {} diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/create-remote-server.input.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/create-remote-server.input.ts new file mode 100644 index 000000000000..934f732def2a --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/create-remote-server.input.ts @@ -0,0 +1,31 @@ +import { Field, InputType } from '@nestjs/graphql'; + +import { IsString } from 'class-validator'; + +@InputType() +export class CreateRemoteServerInput { + @Field(() => String) + @IsString() + host: string; + + @Field(() => String) + @IsString() + port: string; + + @Field(() => String) + @IsString() + database: string; + + @Field(() => String) + @IsString() + username: string; + + @Field(() => String) + @IsString() + password: string; + + // TODO: Decide if this should be added during the creation + @Field(() => String) + @IsString() + schema: string; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/delete-remote-server.input.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/delete-remote-server.input.ts new file mode 100644 index 000000000000..ad01166c8a79 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/delete-remote-server.input.ts @@ -0,0 +1,9 @@ +import { InputType, ID } from '@nestjs/graphql'; + +import { IDField } from '@ptc-org/nestjs-query-graphql'; + +@InputType() +export class DeleteRemoteServerInput { + @IDField(() => ID, { description: 'The id of the record to delete.' }) + id!: string; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/remote-server.dto.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/remote-server.dto.ts new file mode 100644 index 000000000000..59fada96b5b6 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/remote-server.dto.ts @@ -0,0 +1,44 @@ +import { ObjectType, Field, HideField, ID } from '@nestjs/graphql'; + +import { IDField, QueryOptions } from '@ptc-org/nestjs-query-graphql'; + +@ObjectType('RemoteServer') +@QueryOptions({ + defaultResultSize: 10, + disableSort: true, + maxResultsSize: 1000, +}) +export class RemoteServerDTO { + @IDField(() => ID) + id: string; + + @Field(() => ID) + fwdId: string; + + @Field(() => String) + host: string; + + @Field(() => String) + port: string; + + @Field(() => String) + database: string; + + @Field(() => String) + username: string; + + @Field(() => String) + password: string; + + @Field(() => String) + schema: string; + + @HideField() + workspaceId: string; + + @Field() + createdAt: Date; + + @Field() + updatedAt: Date; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.entity.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.entity.ts new file mode 100644 index 000000000000..26f15745ddb8 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.entity.ts @@ -0,0 +1,45 @@ +import { + Column, + CreateDateColumn, + Entity, + Generated, + PrimaryGeneratedColumn, + UpdateDateColumn, +} from 'typeorm'; + +@Entity('remoteServer') +export class RemoteServerEntity { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column() + @Generated('uuid') + fdwId: string; + + @Column({ nullable: true }) + host: string; + + @Column({ nullable: true }) + port: string; + + @Column({ nullable: true }) + database: string; + + @Column({ nullable: true }) + username: string; + + @Column({ nullable: true }) + password: string; + + @Column({ nullable: true }) + schema: string; + + @Column({ nullable: false, type: 'uuid' }) + workspaceId: string; + + @CreateDateColumn() + createdAt: Date; + + @UpdateDateColumn() + updatedAt: Date; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.module.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.module.ts new file mode 100644 index 000000000000..b0936e4b3b86 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.module.ts @@ -0,0 +1,17 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; +import { RemoteServerEntity } from 'src/engine/metadata-modules/remote-server/remote-server.entity'; +import { RemoteServerResolver } from 'src/engine/metadata-modules/remote-server/remote-server.resolver'; +import { RemoteServerService } from 'src/engine/metadata-modules/remote-server/remote-server.service'; + +@Module({ + imports: [ + TypeORMModule, + TypeOrmModule.forFeature([RemoteServerEntity], 'metadata'), + ], + providers: [RemoteServerService, RemoteServerResolver], + exports: [RemoteServerService], +}) +export class RemoteServerModule {} diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.resolver.ts new file mode 100644 index 000000000000..771cf83a7a18 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.resolver.ts @@ -0,0 +1,32 @@ +import { UseGuards } from '@nestjs/common'; +import { Resolver, Args, Mutation } from '@nestjs/graphql'; + +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; +import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard'; +import { CreateRemoteServerInput } from 'src/engine/metadata-modules/remote-server/dtos/create-remote-server.input'; +import { DeleteRemoteServerInput } from 'src/engine/metadata-modules/remote-server/dtos/delete-remote-server.input'; +import { RemoteServerDTO } from 'src/engine/metadata-modules/remote-server/dtos/remote-server.dto'; +import { RemoteServerService } from 'src/engine/metadata-modules/remote-server/remote-server.service'; + +@UseGuards(JwtAuthGuard) +@Resolver(() => RemoteServerDTO) +export class RemoteServerResolver { + constructor(private readonly remoteServerService: RemoteServerService) {} + + @Mutation(() => RemoteServerDTO) + async createOneRemoteServer( + @Args('input') input: CreateRemoteServerInput, + @AuthWorkspace() { id: workspaceId }: Workspace, + ) { + return this.remoteServerService.createOneRemoteServer(input, workspaceId); + } + + @Mutation(() => RemoteServerDTO) + async deleteOneRemoteServer( + @Args('input') { id }: DeleteRemoteServerInput, + @AuthWorkspace() { id: workspaceId }: Workspace, + ) { + return this.remoteServerService.deleteOneRemoteServer(id, workspaceId); + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.service.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.service.ts new file mode 100644 index 000000000000..2e68bcbc0915 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.service.ts @@ -0,0 +1,77 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm'; +import { Repository } from 'typeorm'; + +import { TypeORMService } from 'src/database/typeorm/typeorm.service'; +import { CreateRemoteServerInput } from 'src/engine/metadata-modules/remote-server/dtos/create-remote-server.input'; +import { RemoteServerEntity } from 'src/engine/metadata-modules/remote-server/remote-server.entity'; +import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; +import { encryptText } from 'src/engine/core-modules/auth/auth.util'; + +@Injectable() +export class RemoteServerService extends TypeOrmQueryService { + constructor( + @InjectRepository(RemoteServerEntity, 'metadata') + private readonly remoteServerRepository: Repository, + private readonly typeORMService: TypeORMService, + private readonly environmentService: EnvironmentService, + ) { + super(remoteServerRepository); + } + + async createOneRemoteServer( + remoteServerInput: CreateRemoteServerInput, + workspaceId: string, + ): Promise { + const mainDatasource = this.typeORMService.getMainDataSource(); + const key = this.environmentService.get('LOGIN_TOKEN_SECRET'); + const iv = this.environmentService.get('IV_SECRET'); + const encryptedPassword = await encryptText( + remoteServerInput.password, + key, + iv, + ); + + const createdRemoteServer = await super.createOne({ + ...remoteServerInput, + password: encryptedPassword, + workspaceId, + }); + + await mainDatasource.query( + `CREATE SERVER IF NOT EXISTS "${createdRemoteServer.fdwId}" FOREIGN DATA WRAPPER postgres_fdw OPTIONS (dbname '${remoteServerInput.database}', host '${remoteServerInput.host}', port '${remoteServerInput.port}')`, + ); + + await mainDatasource.query( + `CREATE USER MAPPING IF NOT EXISTS FOR ${remoteServerInput.username} SERVER "${createdRemoteServer.fdwId}" OPTIONS (user '${remoteServerInput.username}', password '${remoteServerInput.password}')`, + ); + + return createdRemoteServer; + } + + async deleteOneRemoteServer( + id: string, + workspaceId: string, + ): Promise { + const remoteServer = await this.remoteServerRepository.findOne({ + where: { + id, + workspaceId, + }, + }); + + if (!remoteServer) { + throw new NotFoundException('Object does not exist'); + } + + await this.remoteServerRepository.delete(id); + + const mainDatasource = this.typeORMService.getMainDataSource(); + + await mainDatasource.query(`DROP SERVER "${remoteServer.fdwId}" CASCADE`); + + return remoteServer; + } +} From 972c0255c754e21b3831a9957e8e2a43323a07be Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Thu, 21 Mar 2024 18:31:55 +0100 Subject: [PATCH 2/6] Add getters --- ... => 1711041448958-addRemoteServerTable.ts} | 6 ++-- .../dtos/create-remote-server.input.ts | 6 ++++ ...ver.input.ts => remote-server-id.input.ts} | 4 +-- .../dtos/remote-server-type.input.ts | 12 ++++++++ .../remote-server/dtos/remote-server.dto.ts | 8 ++++-- .../remote-server/remote-server.entity.ts | 5 ++++ .../remote-server/remote-server.resolver.ts | 28 ++++++++++++++++--- .../remote-server/remote-server.service.ts | 26 ++++++++++++++++- 8 files changed, 82 insertions(+), 13 deletions(-) rename packages/twenty-server/src/database/typeorm/metadata/migrations/{1711017124938-addRemoteServerTable.ts => 1711041448958-addRemoteServerTable.ts} (66%) rename packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/{delete-remote-server.input.ts => remote-server-id.input.ts} (55%) create mode 100644 packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/remote-server-type.input.ts diff --git a/packages/twenty-server/src/database/typeorm/metadata/migrations/1711017124938-addRemoteServerTable.ts b/packages/twenty-server/src/database/typeorm/metadata/migrations/1711041448958-addRemoteServerTable.ts similarity index 66% rename from packages/twenty-server/src/database/typeorm/metadata/migrations/1711017124938-addRemoteServerTable.ts rename to packages/twenty-server/src/database/typeorm/metadata/migrations/1711041448958-addRemoteServerTable.ts index ba9a0915bfa1..d03a4bb12197 100644 --- a/packages/twenty-server/src/database/typeorm/metadata/migrations/1711017124938-addRemoteServerTable.ts +++ b/packages/twenty-server/src/database/typeorm/metadata/migrations/1711041448958-addRemoteServerTable.ts @@ -1,11 +1,11 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; -export class AddRemoteServerTable1711017124938 implements MigrationInterface { - name = 'AddRemoteServerTable1711017124938'; +export class AddRemoteServerTable1711041448958 implements MigrationInterface { + name = 'AddRemoteServerTable1711041448958'; public async up(queryRunner: QueryRunner): Promise { await queryRunner.query( - `CREATE TABLE "metadata"."remoteServer" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "fdwId" uuid NOT NULL DEFAULT uuid_generate_v4(), "host" character varying, "port" character varying, "database" character varying, "username" character varying, "password" character varying, "schema" character varying, "workspaceId" uuid NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_8e5d208498fa2c9710bb934023a" PRIMARY KEY ("id"))`, + `CREATE TABLE "metadata"."remoteServer" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "fdwId" uuid NOT NULL DEFAULT uuid_generate_v4(), "host" character varying, "port" character varying, "database" character varying, "username" character varying, "password" character varying, "schema" character varying, "workspaceId" uuid NOT NULL, "type" character varying, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_8e5d208498fa2c9710bb934023a" PRIMARY KEY ("id"))`, ); } diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/create-remote-server.input.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/create-remote-server.input.ts index 934f732def2a..d9984201e78d 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/create-remote-server.input.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/create-remote-server.input.ts @@ -2,6 +2,8 @@ import { Field, InputType } from '@nestjs/graphql'; import { IsString } from 'class-validator'; +import { RemoteServerType } from 'src/engine/metadata-modules/remote-server/remote-server.entity'; + @InputType() export class CreateRemoteServerInput { @Field(() => String) @@ -24,6 +26,10 @@ export class CreateRemoteServerInput { @IsString() password: string; + @Field(() => String) + @IsString() + type: RemoteServerType; + // TODO: Decide if this should be added during the creation @Field(() => String) @IsString() diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/delete-remote-server.input.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/remote-server-id.input.ts similarity index 55% rename from packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/delete-remote-server.input.ts rename to packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/remote-server-id.input.ts index ad01166c8a79..958d0bae3f87 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/delete-remote-server.input.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/remote-server-id.input.ts @@ -3,7 +3,7 @@ import { InputType, ID } from '@nestjs/graphql'; import { IDField } from '@ptc-org/nestjs-query-graphql'; @InputType() -export class DeleteRemoteServerInput { - @IDField(() => ID, { description: 'The id of the record to delete.' }) +export class RemoteServerIdInput { + @IDField(() => ID, { description: 'The id of the record.' }) id!: string; } diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/remote-server-type.input.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/remote-server-type.input.ts new file mode 100644 index 000000000000..496e85abaeb0 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/remote-server-type.input.ts @@ -0,0 +1,12 @@ +import { InputType, Field } from '@nestjs/graphql'; + +import { IsString } from 'class-validator'; + +import { RemoteServerType } from 'src/engine/metadata-modules/remote-server/remote-server.entity'; + +@InputType() +export class RemoteServerTypeInput { + @Field(() => String) + @IsString() + type!: RemoteServerType; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/remote-server.dto.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/remote-server.dto.ts index 59fada96b5b6..db669ba286a3 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/remote-server.dto.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/remote-server.dto.ts @@ -2,6 +2,8 @@ import { ObjectType, Field, HideField, ID } from '@nestjs/graphql'; import { IDField, QueryOptions } from '@ptc-org/nestjs-query-graphql'; +import { RemoteServerType } from 'src/engine/metadata-modules/remote-server/remote-server.entity'; + @ObjectType('RemoteServer') @QueryOptions({ defaultResultSize: 10, @@ -27,15 +29,15 @@ export class RemoteServerDTO { @Field(() => String) username: string; - @Field(() => String) - password: string; - @Field(() => String) schema: string; @HideField() workspaceId: string; + @Field(() => String) + type: RemoteServerType; + @Field() createdAt: Date; diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.entity.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.entity.ts index 26f15745ddb8..1dd4863361d9 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.entity.ts @@ -7,6 +7,8 @@ import { UpdateDateColumn, } from 'typeorm'; +export type RemoteServerType = 'postgres'; + @Entity('remoteServer') export class RemoteServerEntity { @PrimaryGeneratedColumn('uuid') @@ -37,6 +39,9 @@ export class RemoteServerEntity { @Column({ nullable: false, type: 'uuid' }) workspaceId: string; + @Column({ nullable: true }) + type: RemoteServerType; + @CreateDateColumn() createdAt: Date; diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.resolver.ts index 771cf83a7a18..9590b72e34e5 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.resolver.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.resolver.ts @@ -1,16 +1,17 @@ import { UseGuards } from '@nestjs/common'; -import { Resolver, Args, Mutation } from '@nestjs/graphql'; +import { Resolver, Args, Mutation, Query } from '@nestjs/graphql'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard'; import { CreateRemoteServerInput } from 'src/engine/metadata-modules/remote-server/dtos/create-remote-server.input'; -import { DeleteRemoteServerInput } from 'src/engine/metadata-modules/remote-server/dtos/delete-remote-server.input'; +import { RemoteServerIdInput } from 'src/engine/metadata-modules/remote-server/dtos/remote-server-id.input'; +import { RemoteServerTypeInput } from 'src/engine/metadata-modules/remote-server/dtos/remote-server-type.input'; import { RemoteServerDTO } from 'src/engine/metadata-modules/remote-server/dtos/remote-server.dto'; import { RemoteServerService } from 'src/engine/metadata-modules/remote-server/remote-server.service'; @UseGuards(JwtAuthGuard) -@Resolver(() => RemoteServerDTO) +@Resolver() export class RemoteServerResolver { constructor(private readonly remoteServerService: RemoteServerService) {} @@ -24,9 +25,28 @@ export class RemoteServerResolver { @Mutation(() => RemoteServerDTO) async deleteOneRemoteServer( - @Args('input') { id }: DeleteRemoteServerInput, + @Args('input') { id }: RemoteServerIdInput, @AuthWorkspace() { id: workspaceId }: Workspace, ) { return this.remoteServerService.deleteOneRemoteServer(id, workspaceId); } + + @Query(() => RemoteServerDTO) + async findOneRemoteServerById( + @Args('input') { id }: RemoteServerIdInput, + @AuthWorkspace() { id: workspaceId }: Workspace, + ) { + return this.remoteServerService.findOneByIdWithinWorkspace(id, workspaceId); + } + + @Query(() => [RemoteServerDTO]) + async findManyRemoteServersByType( + @Args('input') { type }: RemoteServerTypeInput, + @AuthWorkspace() { id: workspaceId }: Workspace, + ) { + return this.remoteServerService.findManyByTypeWithinWorkspace( + type, + workspaceId, + ); + } } diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.service.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.service.ts index 2e68bcbc0915..bcf0dbe5cf13 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.service.ts @@ -6,7 +6,10 @@ import { Repository } from 'typeorm'; import { TypeORMService } from 'src/database/typeorm/typeorm.service'; import { CreateRemoteServerInput } from 'src/engine/metadata-modules/remote-server/dtos/create-remote-server.input'; -import { RemoteServerEntity } from 'src/engine/metadata-modules/remote-server/remote-server.entity'; +import { + RemoteServerEntity, + RemoteServerType, +} from 'src/engine/metadata-modules/remote-server/remote-server.entity'; import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; import { encryptText } from 'src/engine/core-modules/auth/auth.util'; @@ -74,4 +77,25 @@ export class RemoteServerService extends TypeOrmQueryService return remoteServer; } + + public async findOneByIdWithinWorkspace(id: string, workspaceId: string) { + return this.remoteServerRepository.findOne({ + where: { + id, + workspaceId, + }, + }); + } + + public async findManyByTypeWithinWorkspace( + type: RemoteServerType, + workspaceId: string, + ) { + return this.remoteServerRepository.find({ + where: { + type, + workspaceId, + }, + }); + } } From 73ced71aa8aa10d5219332887c62cd89c9bdac1f Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Fri, 22 Mar 2024 12:25:26 +0100 Subject: [PATCH 3/6] Migrate to json inputs --- .../1711041448958-addRemoteServerTable.ts | 15 ---- .../1711104284380-addRemoteServerTable.ts | 15 ++++ .../dtos/create-remote-server.input.ts | 41 +++------ .../dtos/remote-server-type.input.ts | 2 +- .../remote-server/dtos/remote-server.dto.ts | 26 +++--- .../remote-server/remote-server.entity.ts | 39 ++++---- .../remote-server/remote-server.resolver.ts | 4 +- .../remote-server/remote-server.service.ts | 88 +++++++++++++++---- 8 files changed, 134 insertions(+), 96 deletions(-) delete mode 100644 packages/twenty-server/src/database/typeorm/metadata/migrations/1711041448958-addRemoteServerTable.ts create mode 100644 packages/twenty-server/src/database/typeorm/metadata/migrations/1711104284380-addRemoteServerTable.ts diff --git a/packages/twenty-server/src/database/typeorm/metadata/migrations/1711041448958-addRemoteServerTable.ts b/packages/twenty-server/src/database/typeorm/metadata/migrations/1711041448958-addRemoteServerTable.ts deleted file mode 100644 index d03a4bb12197..000000000000 --- a/packages/twenty-server/src/database/typeorm/metadata/migrations/1711041448958-addRemoteServerTable.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class AddRemoteServerTable1711041448958 implements MigrationInterface { - name = 'AddRemoteServerTable1711041448958'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `CREATE TABLE "metadata"."remoteServer" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "fdwId" uuid NOT NULL DEFAULT uuid_generate_v4(), "host" character varying, "port" character varying, "database" character varying, "username" character varying, "password" character varying, "schema" character varying, "workspaceId" uuid NOT NULL, "type" character varying, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_8e5d208498fa2c9710bb934023a" PRIMARY KEY ("id"))`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP TABLE "metadata"."remoteServer"`); - } -} diff --git a/packages/twenty-server/src/database/typeorm/metadata/migrations/1711104284380-addRemoteServerTable.ts b/packages/twenty-server/src/database/typeorm/metadata/migrations/1711104284380-addRemoteServerTable.ts new file mode 100644 index 000000000000..257ac0a36bc4 --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/metadata/migrations/1711104284380-addRemoteServerTable.ts @@ -0,0 +1,15 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddRemoteServerTable1711104284380 implements MigrationInterface { + name = 'AddRemoteServerTable1711104284380'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "metadata"."remoteServer" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "fdwId" uuid NOT NULL DEFAULT uuid_generate_v4(), "fdwType" character varying, "fdwOptions" jsonb, "userMappingOptions" jsonb, "workspaceId" uuid NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_8e5d208498fa2c9710bb934023a" PRIMARY KEY ("id"))`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE "metadata"."remoteServer"`); + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/create-remote-server.input.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/create-remote-server.input.ts index d9984201e78d..153b39e133b2 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/create-remote-server.input.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/create-remote-server.input.ts @@ -1,37 +1,24 @@ import { Field, InputType } from '@nestjs/graphql'; -import { IsString } from 'class-validator'; +import { IsOptional } from 'class-validator'; +import GraphQLJSON from 'graphql-type-json'; -import { RemoteServerType } from 'src/engine/metadata-modules/remote-server/remote-server.entity'; +import { + FdwOptions, + RemoteServerType, + UserMappingOptions, +} from 'src/engine/metadata-modules/remote-server/remote-server.entity'; @InputType() export class CreateRemoteServerInput { @Field(() => String) - @IsString() - host: string; + fdwType: RemoteServerType; - @Field(() => String) - @IsString() - port: string; - - @Field(() => String) - @IsString() - database: string; - - @Field(() => String) - @IsString() - username: string; + @IsOptional() + @Field(() => GraphQLJSON) + fdwOptions: FdwOptions; - @Field(() => String) - @IsString() - password: string; - - @Field(() => String) - @IsString() - type: RemoteServerType; - - // TODO: Decide if this should be added during the creation - @Field(() => String) - @IsString() - schema: string; + @IsOptional() + @Field(() => GraphQLJSON, { nullable: true }) + userMappingOptions?: UserMappingOptions; } diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/remote-server-type.input.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/remote-server-type.input.ts index 496e85abaeb0..2b7f1afd3499 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/remote-server-type.input.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/remote-server-type.input.ts @@ -8,5 +8,5 @@ import { RemoteServerType } from 'src/engine/metadata-modules/remote-server/remo export class RemoteServerTypeInput { @Field(() => String) @IsString() - type!: RemoteServerType; + fdwType!: RemoteServerType; } diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/remote-server.dto.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/remote-server.dto.ts index db669ba286a3..8e5662671f03 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/remote-server.dto.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/remote-server.dto.ts @@ -1,8 +1,13 @@ import { ObjectType, Field, HideField, ID } from '@nestjs/graphql'; import { IDField, QueryOptions } from '@ptc-org/nestjs-query-graphql'; +import { IsOptional } from 'class-validator'; +import GraphQLJSON from 'graphql-type-json'; -import { RemoteServerType } from 'src/engine/metadata-modules/remote-server/remote-server.entity'; +import { + FdwOptions, + RemoteServerType, +} from 'src/engine/metadata-modules/remote-server/remote-server.entity'; @ObjectType('RemoteServer') @QueryOptions({ @@ -18,26 +23,15 @@ export class RemoteServerDTO { fwdId: string; @Field(() => String) - host: string; + fdwType: RemoteServerType; - @Field(() => String) - port: string; - - @Field(() => String) - database: string; - - @Field(() => String) - username: string; - - @Field(() => String) - schema: string; + @IsOptional() + @Field(() => GraphQLJSON, { nullable: true }) + fdwOptions?: FdwOptions; @HideField() workspaceId: string; - @Field(() => String) - type: RemoteServerType; - @Field() createdAt: Date; diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.entity.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.entity.ts index 1dd4863361d9..d070d1f378b6 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.entity.ts @@ -7,7 +7,22 @@ import { UpdateDateColumn, } from 'typeorm'; -export type RemoteServerType = 'postgres'; +export enum RemoteServerType { + POSTGRES_FDW = 'postgres_fdw', +} + +type PostgresFdwOptions = { + host: string; + port: number; + dbname: string; +}; + +export type FdwOptions = PostgresFdwOptions; + +export type UserMappingOptions = { + username: string; + password: string; +}; @Entity('remoteServer') export class RemoteServerEntity { @@ -19,29 +34,17 @@ export class RemoteServerEntity { fdwId: string; @Column({ nullable: true }) - host: string; - - @Column({ nullable: true }) - port: string; + fdwType: RemoteServerType; - @Column({ nullable: true }) - database: string; - - @Column({ nullable: true }) - username: string; - - @Column({ nullable: true }) - password: string; + @Column({ nullable: true, type: 'jsonb' }) + fdwOptions: FdwOptions; - @Column({ nullable: true }) - schema: string; + @Column({ nullable: true, type: 'jsonb' }) + userMappingOptions: UserMappingOptions; @Column({ nullable: false, type: 'uuid' }) workspaceId: string; - @Column({ nullable: true }) - type: RemoteServerType; - @CreateDateColumn() createdAt: Date; diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.resolver.ts index 9590b72e34e5..183fc68e06e2 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.resolver.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.resolver.ts @@ -41,11 +41,11 @@ export class RemoteServerResolver { @Query(() => [RemoteServerDTO]) async findManyRemoteServersByType( - @Args('input') { type }: RemoteServerTypeInput, + @Args('input') { fdwType }: RemoteServerTypeInput, @AuthWorkspace() { id: workspaceId }: Workspace, ) { return this.remoteServerService.findManyByTypeWithinWorkspace( - type, + fdwType, workspaceId, ); } diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.service.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.service.ts index bcf0dbe5cf13..6c2772e463d7 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.service.ts @@ -7,8 +7,10 @@ import { Repository } from 'typeorm'; import { TypeORMService } from 'src/database/typeorm/typeorm.service'; import { CreateRemoteServerInput } from 'src/engine/metadata-modules/remote-server/dtos/create-remote-server.input'; import { + FdwOptions, RemoteServerEntity, RemoteServerType, + UserMappingOptions, } from 'src/engine/metadata-modules/remote-server/remote-server.entity'; import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; import { encryptText } from 'src/engine/core-modules/auth/auth.util'; @@ -29,28 +31,49 @@ export class RemoteServerService extends TypeOrmQueryService workspaceId: string, ): Promise { const mainDatasource = this.typeORMService.getMainDataSource(); - const key = this.environmentService.get('LOGIN_TOKEN_SECRET'); - const iv = this.environmentService.get('IV_SECRET'); - const encryptedPassword = await encryptText( - remoteServerInput.password, - key, - iv, - ); - const createdRemoteServer = await super.createOne({ + let remoteServerToCreate = { ...remoteServerInput, - password: encryptedPassword, workspaceId, - }); + }; + + if (remoteServerInput.userMappingOptions) { + const key = this.environmentService.get('LOGIN_TOKEN_SECRET'); + const iv = this.environmentService.get('IV_SECRET'); + const encryptedPassword = await encryptText( + remoteServerInput.userMappingOptions.password, + key, + iv, + ); + + remoteServerToCreate = { + ...remoteServerToCreate, + userMappingOptions: { + ...remoteServerInput.userMappingOptions, + password: encryptedPassword, + }, + }; + } - await mainDatasource.query( - `CREATE SERVER IF NOT EXISTS "${createdRemoteServer.fdwId}" FOREIGN DATA WRAPPER postgres_fdw OPTIONS (dbname '${remoteServerInput.database}', host '${remoteServerInput.host}', port '${remoteServerInput.port}')`, - ); + const createdRemoteServer = await super.createOne(remoteServerToCreate); - await mainDatasource.query( - `CREATE USER MAPPING IF NOT EXISTS FOR ${remoteServerInput.username} SERVER "${createdRemoteServer.fdwId}" OPTIONS (user '${remoteServerInput.username}', password '${remoteServerInput.password}')`, + const fdwQuery = this.buildFDWQuery( + createdRemoteServer.fdwId, + remoteServerInput.fdwType, + remoteServerInput.fdwOptions, ); + await mainDatasource.query(fdwQuery); + + if (remoteServerInput.userMappingOptions) { + const userMappingQuery = this.buildUserMappingQuery( + createdRemoteServer.fdwId, + remoteServerInput.userMappingOptions, + ); + + await mainDatasource.query(userMappingQuery); + } + return createdRemoteServer; } @@ -88,14 +111,45 @@ export class RemoteServerService extends TypeOrmQueryService } public async findManyByTypeWithinWorkspace( - type: RemoteServerType, + fdwType: RemoteServerType, workspaceId: string, ) { return this.remoteServerRepository.find({ where: { - type, + fdwType, workspaceId, }, }); } + + // TODO: Move to a query builder once the logic is validated + private buildUserMappingQuery( + fdwId: string, + userMappingOptions: UserMappingOptions, + ) { + return `CREATE USER MAPPING IF NOT EXISTS FOR ${userMappingOptions.username} SERVER "${fdwId}" OPTIONS (user '${userMappingOptions.username}', password '${userMappingOptions.password}')`; + } + + // TODO: Move to a query builder once the logic is validated + private buildFDWQuery( + fdwId: string, + fdwType: RemoteServerType, + fdwOptions: FdwOptions, + ) { + let fdwQueryOptions = ''; + + switch (fdwType) { + case RemoteServerType.POSTGRES_FDW: + fdwQueryOptions = this.buildPostgresFDWQueryOptions(fdwOptions); + break; + default: + throw new Error('FDW type not supported'); + } + + return `CREATE SERVER IF NOT EXISTS "${fdwId}" FOREIGN DATA WRAPPER postgres_fdw OPTIONS (${fdwQueryOptions})`; + } + + private buildPostgresFDWQueryOptions(fdwOptions: FdwOptions) { + return `dbname '${fdwOptions.dbname}', host '${fdwOptions.host}', port '${fdwOptions.port}'`; + } } From c842c54f2b31248970c31967bfcc979dcd387647 Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Fri, 22 Mar 2024 17:17:26 +0100 Subject: [PATCH 4/6] Use extendable type --- ... => 1711124466598-addRemoteServerTable.ts} | 4 +- .../dtos/create-remote-server.input.ts | 6 +-- .../dtos/remote-server-type.input.ts | 4 +- .../remote-server/dtos/remote-server.dto.ts | 14 ++----- .../remote-server/remote-server.entity.ts | 12 ++++-- .../remote-server/remote-server.resolver.ts | 9 +++-- .../remote-server/remote-server.service.ts | 37 +++++++++---------- 7 files changed, 42 insertions(+), 44 deletions(-) rename packages/twenty-server/src/database/typeorm/metadata/migrations/{1711104284380-addRemoteServerTable.ts => 1711124466598-addRemoteServerTable.ts} (86%) diff --git a/packages/twenty-server/src/database/typeorm/metadata/migrations/1711104284380-addRemoteServerTable.ts b/packages/twenty-server/src/database/typeorm/metadata/migrations/1711124466598-addRemoteServerTable.ts similarity index 86% rename from packages/twenty-server/src/database/typeorm/metadata/migrations/1711104284380-addRemoteServerTable.ts rename to packages/twenty-server/src/database/typeorm/metadata/migrations/1711124466598-addRemoteServerTable.ts index 257ac0a36bc4..e2c287415594 100644 --- a/packages/twenty-server/src/database/typeorm/metadata/migrations/1711104284380-addRemoteServerTable.ts +++ b/packages/twenty-server/src/database/typeorm/metadata/migrations/1711124466598-addRemoteServerTable.ts @@ -1,7 +1,7 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; -export class AddRemoteServerTable1711104284380 implements MigrationInterface { - name = 'AddRemoteServerTable1711104284380'; +export class AddRemoteServerTable1711124466598 implements MigrationInterface { + name = 'AddRemoteServerTable1711124466598'; public async up(queryRunner: QueryRunner): Promise { await queryRunner.query( diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/create-remote-server.input.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/create-remote-server.input.ts index 153b39e133b2..e799fe681ced 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/create-remote-server.input.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/create-remote-server.input.ts @@ -10,13 +10,13 @@ import { } from 'src/engine/metadata-modules/remote-server/remote-server.entity'; @InputType() -export class CreateRemoteServerInput { +export class CreateRemoteServerInput { @Field(() => String) - fdwType: RemoteServerType; + fdwType: T; @IsOptional() @Field(() => GraphQLJSON) - fdwOptions: FdwOptions; + fdwOptions: FdwOptions; @IsOptional() @Field(() => GraphQLJSON, { nullable: true }) diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/remote-server-type.input.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/remote-server-type.input.ts index 2b7f1afd3499..b3cbf245ebea 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/remote-server-type.input.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/remote-server-type.input.ts @@ -5,8 +5,8 @@ import { IsString } from 'class-validator'; import { RemoteServerType } from 'src/engine/metadata-modules/remote-server/remote-server.entity'; @InputType() -export class RemoteServerTypeInput { +export class RemoteServerTypeInput { @Field(() => String) @IsString() - fdwType!: RemoteServerType; + fdwType!: T; } diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/remote-server.dto.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/remote-server.dto.ts index 8e5662671f03..7016126a1f25 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/remote-server.dto.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/remote-server.dto.ts @@ -1,6 +1,5 @@ import { ObjectType, Field, HideField, ID } from '@nestjs/graphql'; -import { IDField, QueryOptions } from '@ptc-org/nestjs-query-graphql'; import { IsOptional } from 'class-validator'; import GraphQLJSON from 'graphql-type-json'; @@ -10,24 +9,19 @@ import { } from 'src/engine/metadata-modules/remote-server/remote-server.entity'; @ObjectType('RemoteServer') -@QueryOptions({ - defaultResultSize: 10, - disableSort: true, - maxResultsSize: 1000, -}) -export class RemoteServerDTO { - @IDField(() => ID) +export class RemoteServerDTO { + @Field(() => ID) id: string; @Field(() => ID) fwdId: string; @Field(() => String) - fdwType: RemoteServerType; + fdwType: T; @IsOptional() @Field(() => GraphQLJSON, { nullable: true }) - fdwOptions?: FdwOptions; + fdwOptions?: FdwOptions; @HideField() workspaceId: string; diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.entity.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.entity.ts index d070d1f378b6..950098dfc695 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.entity.ts @@ -1,3 +1,5 @@ +import { ObjectType } from '@nestjs/graphql'; + import { Column, CreateDateColumn, @@ -17,7 +19,8 @@ type PostgresFdwOptions = { dbname: string; }; -export type FdwOptions = PostgresFdwOptions; +export type FdwOptions = + T extends RemoteServerType.POSTGRES_FDW ? PostgresFdwOptions : never; export type UserMappingOptions = { username: string; @@ -25,7 +28,8 @@ export type UserMappingOptions = { }; @Entity('remoteServer') -export class RemoteServerEntity { +@ObjectType('RemoteServer') +export class RemoteServerEntity { @PrimaryGeneratedColumn('uuid') id: string; @@ -34,10 +38,10 @@ export class RemoteServerEntity { fdwId: string; @Column({ nullable: true }) - fdwType: RemoteServerType; + fdwType: T; @Column({ nullable: true, type: 'jsonb' }) - fdwOptions: FdwOptions; + fdwOptions: FdwOptions; @Column({ nullable: true, type: 'jsonb' }) userMappingOptions: UserMappingOptions; diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.resolver.ts index 183fc68e06e2..af9192324e71 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.resolver.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.resolver.ts @@ -8,16 +8,19 @@ import { CreateRemoteServerInput } from 'src/engine/metadata-modules/remote-serv import { RemoteServerIdInput } from 'src/engine/metadata-modules/remote-server/dtos/remote-server-id.input'; import { RemoteServerTypeInput } from 'src/engine/metadata-modules/remote-server/dtos/remote-server-type.input'; import { RemoteServerDTO } from 'src/engine/metadata-modules/remote-server/dtos/remote-server.dto'; +import { RemoteServerType } from 'src/engine/metadata-modules/remote-server/remote-server.entity'; import { RemoteServerService } from 'src/engine/metadata-modules/remote-server/remote-server.service'; @UseGuards(JwtAuthGuard) @Resolver() export class RemoteServerResolver { - constructor(private readonly remoteServerService: RemoteServerService) {} + constructor( + private readonly remoteServerService: RemoteServerService, + ) {} @Mutation(() => RemoteServerDTO) async createOneRemoteServer( - @Args('input') input: CreateRemoteServerInput, + @Args('input') input: CreateRemoteServerInput, @AuthWorkspace() { id: workspaceId }: Workspace, ) { return this.remoteServerService.createOneRemoteServer(input, workspaceId); @@ -41,7 +44,7 @@ export class RemoteServerResolver { @Query(() => [RemoteServerDTO]) async findManyRemoteServersByType( - @Args('input') { fdwType }: RemoteServerTypeInput, + @Args('input') { fdwType }: RemoteServerTypeInput, @AuthWorkspace() { id: workspaceId }: Workspace, ) { return this.remoteServerService.findManyByTypeWithinWorkspace( diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.service.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.service.ts index 6c2772e463d7..9ef4db2eee07 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.service.ts @@ -1,7 +1,6 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm'; import { Repository } from 'typeorm'; import { TypeORMService } from 'src/database/typeorm/typeorm.service'; @@ -16,20 +15,20 @@ import { EnvironmentService } from 'src/engine/integrations/environment/environm import { encryptText } from 'src/engine/core-modules/auth/auth.util'; @Injectable() -export class RemoteServerService extends TypeOrmQueryService { +export class RemoteServerService { constructor( @InjectRepository(RemoteServerEntity, 'metadata') - private readonly remoteServerRepository: Repository, + private readonly remoteServerRepository: Repository< + RemoteServerEntity + >, private readonly typeORMService: TypeORMService, private readonly environmentService: EnvironmentService, - ) { - super(remoteServerRepository); - } + ) {} async createOneRemoteServer( - remoteServerInput: CreateRemoteServerInput, + remoteServerInput: CreateRemoteServerInput, workspaceId: string, - ): Promise { + ): Promise> { const mainDatasource = this.typeORMService.getMainDataSource(); let remoteServerToCreate = { @@ -55,7 +54,8 @@ export class RemoteServerService extends TypeOrmQueryService }; } - const createdRemoteServer = await super.createOne(remoteServerToCreate); + const createdRemoteServer = + await this.remoteServerRepository.create(remoteServerToCreate); const fdwQuery = this.buildFDWQuery( createdRemoteServer.fdwId, @@ -74,13 +74,15 @@ export class RemoteServerService extends TypeOrmQueryService await mainDatasource.query(userMappingQuery); } + await this.remoteServerRepository.save(createdRemoteServer); + return createdRemoteServer; } async deleteOneRemoteServer( id: string, workspaceId: string, - ): Promise { + ): Promise> { const remoteServer = await this.remoteServerRepository.findOne({ where: { id, @@ -92,11 +94,10 @@ export class RemoteServerService extends TypeOrmQueryService throw new NotFoundException('Object does not exist'); } - await this.remoteServerRepository.delete(id); - const mainDatasource = this.typeORMService.getMainDataSource(); await mainDatasource.query(`DROP SERVER "${remoteServer.fdwId}" CASCADE`); + await this.remoteServerRepository.delete(id); return remoteServer; } @@ -110,8 +111,8 @@ export class RemoteServerService extends TypeOrmQueryService }); } - public async findManyByTypeWithinWorkspace( - fdwType: RemoteServerType, + public async findManyByTypeWithinWorkspace( + fdwType: T, workspaceId: string, ) { return this.remoteServerRepository.find({ @@ -131,11 +132,7 @@ export class RemoteServerService extends TypeOrmQueryService } // TODO: Move to a query builder once the logic is validated - private buildFDWQuery( - fdwId: string, - fdwType: RemoteServerType, - fdwOptions: FdwOptions, - ) { + private buildFDWQuery(fdwId: string, fdwType: T, fdwOptions: FdwOptions) { let fdwQueryOptions = ''; switch (fdwType) { @@ -149,7 +146,7 @@ export class RemoteServerService extends TypeOrmQueryService return `CREATE SERVER IF NOT EXISTS "${fdwId}" FOREIGN DATA WRAPPER postgres_fdw OPTIONS (${fdwQueryOptions})`; } - private buildPostgresFDWQueryOptions(fdwOptions: FdwOptions) { + private buildPostgresFDWQueryOptions(fdwOptions: FdwOptions) { return `dbname '${fdwOptions.dbname}', host '${fdwOptions.host}', port '${fdwOptions.port}'`; } } From d344cf57111018596ae1a2739ac7587ff5e9e3f6 Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Mon, 25 Mar 2024 12:09:31 +0100 Subject: [PATCH 5/6] Use regex validation --- packages/twenty-server/.env.example | 1 - packages/twenty-server/.env.test | 1 - .../factories/factories.ts | 3 + .../foreign-data-wrapper-query.factory.ts | 40 +++++++++++++ .../environment/environment-variables.ts | 3 - .../remote-server/remote-server.module.ts | 7 ++- .../remote-server/remote-server.service.ts | 58 +++++++------------ .../utils/validate-remote-server-input.ts | 20 +++++++ 8 files changed, 91 insertions(+), 42 deletions(-) create mode 100644 packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/foreign-data-wrapper-query.factory.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/remote-server/utils/validate-remote-server-input.ts diff --git a/packages/twenty-server/.env.example b/packages/twenty-server/.env.example index f41c452eac0b..4959f2e8207f 100644 --- a/packages/twenty-server/.env.example +++ b/packages/twenty-server/.env.example @@ -9,7 +9,6 @@ ACCESS_TOKEN_SECRET=replace_me_with_a_random_string_access LOGIN_TOKEN_SECRET=replace_me_with_a_random_string_login REFRESH_TOKEN_SECRET=replace_me_with_a_random_string_refresh FILE_TOKEN_SECRET=replace_me_with_a_random_string_refresh -IV_SECRET=replace_me_with_a_random_string_iv SIGN_IN_PREFILLED=true # ———————— Optional ———————— diff --git a/packages/twenty-server/.env.test b/packages/twenty-server/.env.test index 44253c93a3cd..1c2c967fd411 100644 --- a/packages/twenty-server/.env.test +++ b/packages/twenty-server/.env.test @@ -10,7 +10,6 @@ ACCESS_TOKEN_SECRET=secret_jwt LOGIN_TOKEN_SECRET=secret_login_tokens REFRESH_TOKEN_SECRET=secret_refresh_token FILE_TOKEN_SECRET=replace_me_with_a_random_string_refresh -IV_SECRET=replace_me_with_a_random_string_iv # ———————— Optional ———————— # DEBUG_MODE=false diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/factories.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/factories.ts index dd1ec66d859c..db1255540ef1 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/factories.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/factories.ts @@ -1,3 +1,5 @@ +import { ForeignDataWrapperQueryFactory } from 'src/engine/api/graphql/workspace-query-builder/factories/foreign-data-wrapper-query.factory'; + import { ArgsAliasFactory } from './args-alias.factory'; import { ArgsStringFactory } from './args-string.factory'; import { RelationFieldAliasFactory } from './relation-field-alias.factory'; @@ -28,4 +30,5 @@ export const workspaceQueryBuilderFactories = [ UpdateOneQueryFactory, UpdateManyQueryFactory, DeleteManyQueryFactory, + ForeignDataWrapperQueryFactory, ]; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/foreign-data-wrapper-query.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/foreign-data-wrapper-query.factory.ts new file mode 100644 index 000000000000..fcdce5faaca3 --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/foreign-data-wrapper-query.factory.ts @@ -0,0 +1,40 @@ +import { Injectable } from '@nestjs/common'; + +import { + FdwOptions, + RemoteServerType, + UserMappingOptions, +} from 'src/engine/metadata-modules/remote-server/remote-server.entity'; + +@Injectable() +export class ForeignDataWrapperQueryFactory { + createFDW( + fdwId: string, + fdwType: RemoteServerType, + fdwOptions: FdwOptions, + ) { + let fdwName = ''; + let options = ''; + + switch (fdwType) { + case RemoteServerType.POSTGRES_FDW: + fdwName = 'postgres_fdw'; + options = this.buildPostgresFDWQueryOptions(fdwOptions); + break; + default: + throw new Error('FDW type not supported'); + } + + return `CREATE SERVER IF NOT EXISTS "${fdwId}" FOREIGN DATA WRAPPER ${fdwName} OPTIONS (${options})`; + } + + createUserMapping(fdwId: string, userMappingOptions: UserMappingOptions) { + return `CREATE USER MAPPING IF NOT EXISTS FOR ${userMappingOptions.username} SERVER "${fdwId}" OPTIONS (user '${userMappingOptions.username}', password '${userMappingOptions.password}')`; + } + + private buildPostgresFDWQueryOptions( + fdwOptions: FdwOptions, + ) { + return `dbname '${fdwOptions.dbname}', host '${fdwOptions.host}', port '${fdwOptions.port}'`; + } +} diff --git a/packages/twenty-server/src/engine/integrations/environment/environment-variables.ts b/packages/twenty-server/src/engine/integrations/environment/environment-variables.ts index ff20c19c963b..452c643352b9 100644 --- a/packages/twenty-server/src/engine/integrations/environment/environment-variables.ts +++ b/packages/twenty-server/src/engine/integrations/environment/environment-variables.ts @@ -146,9 +146,6 @@ export class EnvironmentVariables { @IsOptional() FILE_TOKEN_EXPIRES_IN: string = '1d'; - @IsString() - IV_SECRET: string; - // Auth @IsUrl({ require_tld: false }) @IsOptional() diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.module.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.module.ts index b0936e4b3b86..4041b4dc847d 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.module.ts @@ -2,6 +2,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; +import { ForeignDataWrapperQueryFactory } from 'src/engine/api/graphql/workspace-query-builder/factories/foreign-data-wrapper-query.factory'; import { RemoteServerEntity } from 'src/engine/metadata-modules/remote-server/remote-server.entity'; import { RemoteServerResolver } from 'src/engine/metadata-modules/remote-server/remote-server.resolver'; import { RemoteServerService } from 'src/engine/metadata-modules/remote-server/remote-server.service'; @@ -11,7 +12,11 @@ import { RemoteServerService } from 'src/engine/metadata-modules/remote-server/r TypeORMModule, TypeOrmModule.forFeature([RemoteServerEntity], 'metadata'), ], - providers: [RemoteServerService, RemoteServerResolver], + providers: [ + RemoteServerService, + RemoteServerResolver, + ForeignDataWrapperQueryFactory, + ], exports: [RemoteServerService], }) export class RemoteServerModule {} diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.service.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.service.ts index 9ef4db2eee07..738588d2a0ea 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.service.ts @@ -6,13 +6,16 @@ import { Repository } from 'typeorm'; import { TypeORMService } from 'src/database/typeorm/typeorm.service'; import { CreateRemoteServerInput } from 'src/engine/metadata-modules/remote-server/dtos/create-remote-server.input'; import { - FdwOptions, RemoteServerEntity, RemoteServerType, - UserMappingOptions, } from 'src/engine/metadata-modules/remote-server/remote-server.entity'; import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; import { encryptText } from 'src/engine/core-modules/auth/auth.util'; +import { + validateObject, + validateString, +} from 'src/engine/metadata-modules/remote-server/utils/validate-remote-server-input'; +import { ForeignDataWrapperQueryFactory } from 'src/engine/api/graphql/workspace-query-builder/factories/foreign-data-wrapper-query.factory'; @Injectable() export class RemoteServerService { @@ -23,12 +26,19 @@ export class RemoteServerService { >, private readonly typeORMService: TypeORMService, private readonly environmentService: EnvironmentService, + private readonly foreignDataWrapperQueryFactory: ForeignDataWrapperQueryFactory, ) {} async createOneRemoteServer( remoteServerInput: CreateRemoteServerInput, workspaceId: string, ): Promise> { + validateObject(remoteServerInput.fdwOptions); + + if (remoteServerInput.userMappingOptions) { + validateObject(remoteServerInput.userMappingOptions); + } + const mainDatasource = this.typeORMService.getMainDataSource(); let remoteServerToCreate = { @@ -38,11 +48,11 @@ export class RemoteServerService { if (remoteServerInput.userMappingOptions) { const key = this.environmentService.get('LOGIN_TOKEN_SECRET'); - const iv = this.environmentService.get('IV_SECRET'); const encryptedPassword = await encryptText( remoteServerInput.userMappingOptions.password, key, - iv, + // TODO: check if we should use a separated IV + key, ); remoteServerToCreate = { @@ -57,7 +67,7 @@ export class RemoteServerService { const createdRemoteServer = await this.remoteServerRepository.create(remoteServerToCreate); - const fdwQuery = this.buildFDWQuery( + const fdwQuery = this.foreignDataWrapperQueryFactory.createFDW( createdRemoteServer.fdwId, remoteServerInput.fdwType, remoteServerInput.fdwOptions, @@ -66,10 +76,11 @@ export class RemoteServerService { await mainDatasource.query(fdwQuery); if (remoteServerInput.userMappingOptions) { - const userMappingQuery = this.buildUserMappingQuery( - createdRemoteServer.fdwId, - remoteServerInput.userMappingOptions, - ); + const userMappingQuery = + this.foreignDataWrapperQueryFactory.createUserMapping( + createdRemoteServer.fdwId, + remoteServerInput.userMappingOptions, + ); await mainDatasource.query(userMappingQuery); } @@ -83,6 +94,8 @@ export class RemoteServerService { id: string, workspaceId: string, ): Promise> { + validateString(id); + const remoteServer = await this.remoteServerRepository.findOne({ where: { id, @@ -122,31 +135,4 @@ export class RemoteServerService { }, }); } - - // TODO: Move to a query builder once the logic is validated - private buildUserMappingQuery( - fdwId: string, - userMappingOptions: UserMappingOptions, - ) { - return `CREATE USER MAPPING IF NOT EXISTS FOR ${userMappingOptions.username} SERVER "${fdwId}" OPTIONS (user '${userMappingOptions.username}', password '${userMappingOptions.password}')`; - } - - // TODO: Move to a query builder once the logic is validated - private buildFDWQuery(fdwId: string, fdwType: T, fdwOptions: FdwOptions) { - let fdwQueryOptions = ''; - - switch (fdwType) { - case RemoteServerType.POSTGRES_FDW: - fdwQueryOptions = this.buildPostgresFDWQueryOptions(fdwOptions); - break; - default: - throw new Error('FDW type not supported'); - } - - return `CREATE SERVER IF NOT EXISTS "${fdwId}" FOREIGN DATA WRAPPER postgres_fdw OPTIONS (${fdwQueryOptions})`; - } - - private buildPostgresFDWQueryOptions(fdwOptions: FdwOptions) { - return `dbname '${fdwOptions.dbname}', host '${fdwOptions.host}', port '${fdwOptions.port}'`; - } } diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/utils/validate-remote-server-input.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/utils/validate-remote-server-input.ts new file mode 100644 index 000000000000..4847351c363b --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/utils/validate-remote-server-input.ts @@ -0,0 +1,20 @@ +const INPUT_REGEX = /^([A-Za-z0-9\-\_]+)$/; + +export const validateObject = (input: object) => { + for (const [key, value] of Object.entries(input)) { + // Password are encrypted so we don't need to validate them + if (key === 'password') { + continue; + } + + if (!INPUT_REGEX.test(value.toString())) { + throw new Error('Invalid remote server input'); + } + } +}; + +export const validateString = (input: string) => { + if (!INPUT_REGEX.test(input)) { + throw new Error('Invalid remote server input'); + } +}; From ea27f6c7efa8092feb9599c83414e4a90535f6d6 Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Mon, 25 Mar 2024 15:05:46 +0100 Subject: [PATCH 6/6] Remove acronymes --- .../1711124466598-addRemoteServerTable.ts | 15 ------ .../1711374137222-addRemoteServerTable.ts | 15 ++++++ .../foreign-data-wrapper-query.factory.ts | 48 +++++++++++-------- .../engine/core-modules/core-engine.module.ts | 2 + .../dtos/create-remote-server.input.ts | 6 +-- .../dtos/remote-server-type.input.ts | 2 +- .../remote-server/dtos/remote-server.dto.ts | 8 ++-- .../remote-server/remote-server.entity.ts | 14 +++--- .../remote-server/remote-server.resolver.ts | 5 +- .../remote-server/remote-server.service.ts | 28 ++++++----- 10 files changed, 81 insertions(+), 62 deletions(-) delete mode 100644 packages/twenty-server/src/database/typeorm/metadata/migrations/1711124466598-addRemoteServerTable.ts create mode 100644 packages/twenty-server/src/database/typeorm/metadata/migrations/1711374137222-addRemoteServerTable.ts diff --git a/packages/twenty-server/src/database/typeorm/metadata/migrations/1711124466598-addRemoteServerTable.ts b/packages/twenty-server/src/database/typeorm/metadata/migrations/1711124466598-addRemoteServerTable.ts deleted file mode 100644 index e2c287415594..000000000000 --- a/packages/twenty-server/src/database/typeorm/metadata/migrations/1711124466598-addRemoteServerTable.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class AddRemoteServerTable1711124466598 implements MigrationInterface { - name = 'AddRemoteServerTable1711124466598'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `CREATE TABLE "metadata"."remoteServer" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "fdwId" uuid NOT NULL DEFAULT uuid_generate_v4(), "fdwType" character varying, "fdwOptions" jsonb, "userMappingOptions" jsonb, "workspaceId" uuid NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_8e5d208498fa2c9710bb934023a" PRIMARY KEY ("id"))`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP TABLE "metadata"."remoteServer"`); - } -} diff --git a/packages/twenty-server/src/database/typeorm/metadata/migrations/1711374137222-addRemoteServerTable.ts b/packages/twenty-server/src/database/typeorm/metadata/migrations/1711374137222-addRemoteServerTable.ts new file mode 100644 index 000000000000..29cc5870c4b6 --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/metadata/migrations/1711374137222-addRemoteServerTable.ts @@ -0,0 +1,15 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddRemoteServerTable1711374137222 implements MigrationInterface { + name = 'AddRemoteServerTable1711374137222'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "metadata"."remoteServer" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "foreignDataWrapperId" uuid NOT NULL DEFAULT uuid_generate_v4(), "foreignDataWrapperType" character varying, "foreignDataWrapperOptions" jsonb, "userMappingOptions" jsonb, "workspaceId" uuid NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_8e5d208498fa2c9710bb934023a" PRIMARY KEY ("id"))`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE "metadata"."remoteServer"`); + } +} diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/foreign-data-wrapper-query.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/foreign-data-wrapper-query.factory.ts index fcdce5faaca3..83420c83e116 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/foreign-data-wrapper-query.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/foreign-data-wrapper-query.factory.ts @@ -1,40 +1,48 @@ import { Injectable } from '@nestjs/common'; import { - FdwOptions, + ForeignDataWrapperOptions, RemoteServerType, UserMappingOptions, } from 'src/engine/metadata-modules/remote-server/remote-server.entity'; @Injectable() export class ForeignDataWrapperQueryFactory { - createFDW( - fdwId: string, - fdwType: RemoteServerType, - fdwOptions: FdwOptions, + createForeignDataWrapper( + foreignDataWrapperId: string, + foreignDataWrapperType: RemoteServerType, + foreignDataWrapperOptions: ForeignDataWrapperOptions, ) { - let fdwName = ''; - let options = ''; + const [name, options] = this.buildNameAndOptionsFromType( + foreignDataWrapperType, + foreignDataWrapperOptions, + ); - switch (fdwType) { - case RemoteServerType.POSTGRES_FDW: - fdwName = 'postgres_fdw'; - options = this.buildPostgresFDWQueryOptions(fdwOptions); - break; - default: - throw new Error('FDW type not supported'); - } + return `CREATE SERVER "${foreignDataWrapperId}" FOREIGN DATA WRAPPER ${name} OPTIONS (${options})`; + } - return `CREATE SERVER IF NOT EXISTS "${fdwId}" FOREIGN DATA WRAPPER ${fdwName} OPTIONS (${options})`; + createUserMapping( + foreignDataWrapperId: string, + userMappingOptions: UserMappingOptions, + ) { + return `CREATE USER MAPPING IF NOT EXISTS FOR ${userMappingOptions.username} SERVER "${foreignDataWrapperId}" OPTIONS (user '${userMappingOptions.username}', password '${userMappingOptions.password}')`; } - createUserMapping(fdwId: string, userMappingOptions: UserMappingOptions) { - return `CREATE USER MAPPING IF NOT EXISTS FOR ${userMappingOptions.username} SERVER "${fdwId}" OPTIONS (user '${userMappingOptions.username}', password '${userMappingOptions.password}')`; + private buildNameAndOptionsFromType( + type: RemoteServerType, + options: ForeignDataWrapperOptions, + ) { + switch (type) { + case RemoteServerType.POSTGRES_FDW: + return ['postgres_fdw', this.buildPostgresFDWQueryOptions(options)]; + default: + throw new Error('Foreign data wrapper type not supported'); + } } private buildPostgresFDWQueryOptions( - fdwOptions: FdwOptions, + foreignDataWrapperOptions: ForeignDataWrapperOptions, ) { - return `dbname '${fdwOptions.dbname}', host '${fdwOptions.host}', port '${fdwOptions.port}'`; + return `dbname '${foreignDataWrapperOptions.dbname}', host '${foreignDataWrapperOptions.host}', port '${foreignDataWrapperOptions.port}'`; } } diff --git a/packages/twenty-server/src/engine/core-modules/core-engine.module.ts b/packages/twenty-server/src/engine/core-modules/core-engine.module.ts index c61f55b2f886..a74284d09883 100644 --- a/packages/twenty-server/src/engine/core-modules/core-engine.module.ts +++ b/packages/twenty-server/src/engine/core-modules/core-engine.module.ts @@ -10,6 +10,7 @@ import { TimelineMessagingModule } from 'src/engine/core-modules/messaging/timel import { TimelineCalendarEventModule } from 'src/engine/core-modules/calendar/timeline-calendar-event.module'; import { BillingModule } from 'src/engine/core-modules/billing/billing.module'; import { HealthModule } from 'src/engine/core-modules/health/health.module'; +import { RemoteServerModule } from 'src/engine/metadata-modules/remote-server/remote-server.module'; import { AnalyticsModule } from './analytics/analytics.module'; import { FileModule } from './file/file.module'; @@ -30,6 +31,7 @@ import { ClientConfigModule } from './client-config/client-config.module'; TimelineCalendarEventModule, UserModule, WorkspaceModule, + RemoteServerModule, ], exports: [ AnalyticsModule, diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/create-remote-server.input.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/create-remote-server.input.ts index e799fe681ced..35cacf89b737 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/create-remote-server.input.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/create-remote-server.input.ts @@ -4,7 +4,7 @@ import { IsOptional } from 'class-validator'; import GraphQLJSON from 'graphql-type-json'; import { - FdwOptions, + ForeignDataWrapperOptions, RemoteServerType, UserMappingOptions, } from 'src/engine/metadata-modules/remote-server/remote-server.entity'; @@ -12,11 +12,11 @@ import { @InputType() export class CreateRemoteServerInput { @Field(() => String) - fdwType: T; + foreignDataWrapperType: T; @IsOptional() @Field(() => GraphQLJSON) - fdwOptions: FdwOptions; + foreignDataWrapperOptions: ForeignDataWrapperOptions; @IsOptional() @Field(() => GraphQLJSON, { nullable: true }) diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/remote-server-type.input.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/remote-server-type.input.ts index b3cbf245ebea..9c7b75e10e82 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/remote-server-type.input.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/remote-server-type.input.ts @@ -8,5 +8,5 @@ import { RemoteServerType } from 'src/engine/metadata-modules/remote-server/remo export class RemoteServerTypeInput { @Field(() => String) @IsString() - fdwType!: T; + foreignDataWrapperType!: T; } diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/remote-server.dto.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/remote-server.dto.ts index 7016126a1f25..7773e948bd49 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/remote-server.dto.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/remote-server.dto.ts @@ -4,7 +4,7 @@ import { IsOptional } from 'class-validator'; import GraphQLJSON from 'graphql-type-json'; import { - FdwOptions, + ForeignDataWrapperOptions, RemoteServerType, } from 'src/engine/metadata-modules/remote-server/remote-server.entity'; @@ -14,14 +14,14 @@ export class RemoteServerDTO { id: string; @Field(() => ID) - fwdId: string; + foreignDataWrapperId: string; @Field(() => String) - fdwType: T; + foreignDataWrapperType: T; @IsOptional() @Field(() => GraphQLJSON, { nullable: true }) - fdwOptions?: FdwOptions; + foreignDataWrapperOptions?: ForeignDataWrapperOptions; @HideField() workspaceId: string; diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.entity.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.entity.ts index 950098dfc695..71b8d090e02c 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.entity.ts @@ -13,14 +13,16 @@ export enum RemoteServerType { POSTGRES_FDW = 'postgres_fdw', } -type PostgresFdwOptions = { +type PostgresForeignDataWrapperOptions = { host: string; port: number; dbname: string; }; -export type FdwOptions = - T extends RemoteServerType.POSTGRES_FDW ? PostgresFdwOptions : never; +export type ForeignDataWrapperOptions = + T extends RemoteServerType.POSTGRES_FDW + ? PostgresForeignDataWrapperOptions + : never; export type UserMappingOptions = { username: string; @@ -35,13 +37,13 @@ export class RemoteServerEntity { @Column() @Generated('uuid') - fdwId: string; + foreignDataWrapperId: string; @Column({ nullable: true }) - fdwType: T; + foreignDataWrapperType: T; @Column({ nullable: true, type: 'jsonb' }) - fdwOptions: FdwOptions; + foreignDataWrapperOptions: ForeignDataWrapperOptions; @Column({ nullable: true, type: 'jsonb' }) userMappingOptions: UserMappingOptions; diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.resolver.ts index af9192324e71..9165aa4082e5 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.resolver.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.resolver.ts @@ -44,11 +44,12 @@ export class RemoteServerResolver { @Query(() => [RemoteServerDTO]) async findManyRemoteServersByType( - @Args('input') { fdwType }: RemoteServerTypeInput, + @Args('input') + { foreignDataWrapperType }: RemoteServerTypeInput, @AuthWorkspace() { id: workspaceId }: Workspace, ) { return this.remoteServerService.findManyByTypeWithinWorkspace( - fdwType, + foreignDataWrapperType, workspaceId, ); } diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.service.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.service.ts index 738588d2a0ea..9064fc733c53 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.service.ts @@ -1,6 +1,7 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; +import { v4 } from 'uuid'; import { Repository } from 'typeorm'; import { TypeORMService } from 'src/database/typeorm/typeorm.service'; @@ -33,17 +34,19 @@ export class RemoteServerService { remoteServerInput: CreateRemoteServerInput, workspaceId: string, ): Promise> { - validateObject(remoteServerInput.fdwOptions); + validateObject(remoteServerInput.foreignDataWrapperOptions); if (remoteServerInput.userMappingOptions) { validateObject(remoteServerInput.userMappingOptions); } const mainDatasource = this.typeORMService.getMainDataSource(); + const foreignDataWrapperId = v4(); let remoteServerToCreate = { ...remoteServerInput, workspaceId, + foreignDataWrapperId, }; if (remoteServerInput.userMappingOptions) { @@ -67,18 +70,19 @@ export class RemoteServerService { const createdRemoteServer = await this.remoteServerRepository.create(remoteServerToCreate); - const fdwQuery = this.foreignDataWrapperQueryFactory.createFDW( - createdRemoteServer.fdwId, - remoteServerInput.fdwType, - remoteServerInput.fdwOptions, - ); + const foreignDataWrapperQuery = + this.foreignDataWrapperQueryFactory.createForeignDataWrapper( + createdRemoteServer.foreignDataWrapperId, + remoteServerInput.foreignDataWrapperType, + remoteServerInput.foreignDataWrapperOptions, + ); - await mainDatasource.query(fdwQuery); + await mainDatasource.query(foreignDataWrapperQuery); if (remoteServerInput.userMappingOptions) { const userMappingQuery = this.foreignDataWrapperQueryFactory.createUserMapping( - createdRemoteServer.fdwId, + createdRemoteServer.foreignDataWrapperId, remoteServerInput.userMappingOptions, ); @@ -109,7 +113,9 @@ export class RemoteServerService { const mainDatasource = this.typeORMService.getMainDataSource(); - await mainDatasource.query(`DROP SERVER "${remoteServer.fdwId}" CASCADE`); + await mainDatasource.query( + `DROP SERVER "${remoteServer.foreignDataWrapperId}" CASCADE`, + ); await this.remoteServerRepository.delete(id); return remoteServer; @@ -125,12 +131,12 @@ export class RemoteServerService { } public async findManyByTypeWithinWorkspace( - fdwType: T, + foreignDataWrapperType: T, workspaceId: string, ) { return this.remoteServerRepository.find({ where: { - fdwType, + foreignDataWrapperType, workspaceId, }, });