Skip to content

Commit

Permalink
Add endpoints to create and delete remote server (#4606)
Browse files Browse the repository at this point in the history
* Build remote server

* Add getters

* Migrate to json inputs

* Use extendable type

* Use regex validation

* Remove acronymes

---------

Co-authored-by: Thomas Trompette <[email protected]>
  • Loading branch information
thomtrp and Thomas Trompette authored Mar 25, 2024
1 parent e2af5b8 commit 9e70f5b
Show file tree
Hide file tree
Showing 17 changed files with 496 additions and 3 deletions.
1 change: 1 addition & 0 deletions packages/twenty-server/.env.test
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ 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

# ———————— Optional ————————
# DEBUG_MODE=false
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class AddRemoteServerTable1711374137222 implements MigrationInterface {
name = 'AddRemoteServerTable1711374137222';

public async up(queryRunner: QueryRunner): Promise<void> {
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<void> {
await queryRunner.query(`DROP TABLE "metadata"."remoteServer"`);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -28,4 +30,5 @@ export const workspaceQueryBuilderFactories = [
UpdateOneQueryFactory,
UpdateManyQueryFactory,
DeleteManyQueryFactory,
ForeignDataWrapperQueryFactory,
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Injectable } from '@nestjs/common';

import {
ForeignDataWrapperOptions,
RemoteServerType,
UserMappingOptions,
} from 'src/engine/metadata-modules/remote-server/remote-server.entity';

@Injectable()
export class ForeignDataWrapperQueryFactory {
createForeignDataWrapper(
foreignDataWrapperId: string,
foreignDataWrapperType: RemoteServerType,
foreignDataWrapperOptions: ForeignDataWrapperOptions<RemoteServerType>,
) {
const [name, options] = this.buildNameAndOptionsFromType(
foreignDataWrapperType,
foreignDataWrapperOptions,
);

return `CREATE SERVER "${foreignDataWrapperId}" FOREIGN DATA WRAPPER ${name} 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}')`;
}

private buildNameAndOptionsFromType(
type: RemoteServerType,
options: ForeignDataWrapperOptions<RemoteServerType>,
) {
switch (type) {
case RemoteServerType.POSTGRES_FDW:
return ['postgres_fdw', this.buildPostgresFDWQueryOptions(options)];
default:
throw new Error('Foreign data wrapper type not supported');
}
}

private buildPostgresFDWQueryOptions(
foreignDataWrapperOptions: ForeignDataWrapperOptions<RemoteServerType>,
) {
return `dbname '${foreignDataWrapperOptions.dbname}', host '${foreignDataWrapperOptions.host}', port '${foreignDataWrapperOptions.port}'`;
}
}
41 changes: 41 additions & 0 deletions packages/twenty-server/src/engine/core-modules/auth/auth.util.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { createCipheriv, createDecipheriv, createHash } from 'crypto';

import * as bcrypt from 'bcrypt';

export const PASSWORD_REGEX = /^.{8,}$/;
Expand All @@ -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<string> => {
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();
};
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -30,6 +31,7 @@ import { ClientConfigModule } from './client-config/client-config.module';
TimelineCalendarEventModule,
UserModule,
WorkspaceModule,
RemoteServerModule,
],
exports: [
AnalyticsModule,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -15,13 +16,15 @@ import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-
RelationMetadataModule,
WorkspaceCacheVersionModule,
WorkspaceMigrationModule,
RemoteServerModule,
],
providers: [],
exports: [
DataSourceModule,
FieldMetadataModule,
ObjectMetadataModule,
RelationMetadataModule,
RemoteServerModule,
],
})
export class MetadataEngineModule {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Field, InputType } from '@nestjs/graphql';

import { IsOptional } from 'class-validator';
import GraphQLJSON from 'graphql-type-json';

import {
ForeignDataWrapperOptions,
RemoteServerType,
UserMappingOptions,
} from 'src/engine/metadata-modules/remote-server/remote-server.entity';

@InputType()
export class CreateRemoteServerInput<T extends RemoteServerType> {
@Field(() => String)
foreignDataWrapperType: T;

@IsOptional()
@Field(() => GraphQLJSON)
foreignDataWrapperOptions: ForeignDataWrapperOptions<T>;

@IsOptional()
@Field(() => GraphQLJSON, { nullable: true })
userMappingOptions?: UserMappingOptions;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { InputType, ID } from '@nestjs/graphql';

import { IDField } from '@ptc-org/nestjs-query-graphql';

@InputType()
export class RemoteServerIdInput {
@IDField(() => ID, { description: 'The id of the record.' })
id!: string;
}
Original file line number Diff line number Diff line change
@@ -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<T extends RemoteServerType> {
@Field(() => String)
@IsString()
foreignDataWrapperType!: T;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { ObjectType, Field, HideField, ID } from '@nestjs/graphql';

import { IsOptional } from 'class-validator';
import GraphQLJSON from 'graphql-type-json';

import {
ForeignDataWrapperOptions,
RemoteServerType,
} from 'src/engine/metadata-modules/remote-server/remote-server.entity';

@ObjectType('RemoteServer')
export class RemoteServerDTO<T extends RemoteServerType> {
@Field(() => ID)
id: string;

@Field(() => ID)
foreignDataWrapperId: string;

@Field(() => String)
foreignDataWrapperType: T;

@IsOptional()
@Field(() => GraphQLJSON, { nullable: true })
foreignDataWrapperOptions?: ForeignDataWrapperOptions<T>;

@HideField()
workspaceId: string;

@Field()
createdAt: Date;

@Field()
updatedAt: Date;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { ObjectType } from '@nestjs/graphql';

import {
Column,
CreateDateColumn,
Entity,
Generated,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';

export enum RemoteServerType {
POSTGRES_FDW = 'postgres_fdw',
}

type PostgresForeignDataWrapperOptions = {
host: string;
port: number;
dbname: string;
};

export type ForeignDataWrapperOptions<T extends RemoteServerType> =
T extends RemoteServerType.POSTGRES_FDW
? PostgresForeignDataWrapperOptions
: never;

export type UserMappingOptions = {
username: string;
password: string;
};

@Entity('remoteServer')
@ObjectType('RemoteServer')
export class RemoteServerEntity<T extends RemoteServerType> {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column()
@Generated('uuid')
foreignDataWrapperId: string;

@Column({ nullable: true })
foreignDataWrapperType: T;

@Column({ nullable: true, type: 'jsonb' })
foreignDataWrapperOptions: ForeignDataWrapperOptions<T>;

@Column({ nullable: true, type: 'jsonb' })
userMappingOptions: UserMappingOptions;

@Column({ nullable: false, type: 'uuid' })
workspaceId: string;

@CreateDateColumn()
createdAt: Date;

@UpdateDateColumn()
updatedAt: Date;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
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';

@Module({
imports: [
TypeORMModule,
TypeOrmModule.forFeature([RemoteServerEntity], 'metadata'),
],
providers: [
RemoteServerService,
RemoteServerResolver,
ForeignDataWrapperQueryFactory,
],
exports: [RemoteServerService],
})
export class RemoteServerModule {}
Loading

0 comments on commit 9e70f5b

Please sign in to comment.