[feat] Add updateRemoteServer endpoint (#5148)

## Context
#4765 

Following investigations
([#5083](https://github.com/twentyhq/twenty/issues/5083)) we decided to
restrict updates of server from which zero tables have been synchronized
only

## How was it tested
Locally with /metadata
1. Updating a database that already has synchronized tables
<img width="1072" alt="Capture d’écran 2024-04-24 à 16 16 05"
src="https://github.com/twentyhq/twenty/assets/51697796/f9a84c34-2dcd-4f3c-b0bc-b710abae5021">

2. Updating a database that has no synchronized tables
<img width="843" alt="Capture d’écran 2024-04-24 à 16 17 28"
src="https://github.com/twentyhq/twenty/assets/51697796/f320fe03-a6bc-4724-bcd0-4e89d3ac31f5">
+ tested that the connection works well
This commit is contained in:
Marie
2024-04-26 18:12:08 +02:00
committed by GitHub
parent b15533e4b3
commit 76d4188ba8
12 changed files with 430 additions and 50 deletions

View File

@ -0,0 +1,138 @@
import { isDefined } from 'class-validator';
import {
RemoteServerEntity,
RemoteServerType,
UserMappingOptions,
} from 'src/engine/metadata-modules/remote-server/remote-server.entity';
export type DeepPartial<T> = {
[P in keyof T]?: DeepPartial<T[P]>;
};
const buildUserMappingOptionsQuery = (
parameters: any[],
parametersPositions: object,
userMappingOptions: DeepPartial<UserMappingOptions>,
): string | null => {
const shouldUpdateUserMappingOptionsPassword = isDefined(
userMappingOptions?.password,
);
if (shouldUpdateUserMappingOptionsPassword) {
parameters.push(userMappingOptions?.password);
parametersPositions['password'] = parameters.length;
}
const shouldUpdateUserMappingOptionsUsername = isDefined(
userMappingOptions?.username,
);
if (shouldUpdateUserMappingOptionsUsername) {
parameters.push(userMappingOptions?.username);
parametersPositions['username'] = parameters.length;
}
if (
shouldUpdateUserMappingOptionsPassword ||
shouldUpdateUserMappingOptionsUsername
) {
return `"userMappingOptions" = jsonb_set(${
shouldUpdateUserMappingOptionsPassword &&
shouldUpdateUserMappingOptionsUsername
? `jsonb_set(
"userMappingOptions",
'{username}',
to_jsonb($${parametersPositions['username']}::text)
),
'{password}',
to_jsonb($${parametersPositions['password']}::text)
`
: shouldUpdateUserMappingOptionsPassword
? `"userMappingOptions",
'{password}',
to_jsonb($${parametersPositions['password']}::text)
`
: `"userMappingOptions",
'{username}',
to_jsonb($${parametersPositions['username']}::text)
`
})`;
}
return null;
};
// TO DO This only works for postgres_fdw type for now, lets make it more generic when we have a different type
export const updateRemoteServerRawQuery = (
remoteServerToUpdate: DeepPartial<RemoteServerEntity<RemoteServerType>> &
Pick<RemoteServerEntity<RemoteServerType>, 'workspaceId' | 'id'>,
): [any[], string] => {
const parameters: any[] = [remoteServerToUpdate.id];
const parametersPositions = {};
const options: string[] = [];
if (remoteServerToUpdate.userMappingOptions) {
const userMappingOptionsQuery = buildUserMappingOptionsQuery(
parameters,
parametersPositions,
remoteServerToUpdate.userMappingOptions,
);
if (userMappingOptionsQuery) options.push(userMappingOptionsQuery);
}
const shouldUpdateFdwDbname = isDefined(
remoteServerToUpdate.foreignDataWrapperOptions?.dbname,
);
if (shouldUpdateFdwDbname) {
parameters.push(remoteServerToUpdate?.foreignDataWrapperOptions?.dbname);
parametersPositions['dbname'] = parameters.length;
}
const shouldUpdateFdwHost = isDefined(
remoteServerToUpdate.foreignDataWrapperOptions?.host,
);
if (shouldUpdateFdwHost) {
parameters.push(remoteServerToUpdate?.foreignDataWrapperOptions?.host);
parametersPositions['host'] = parameters.length;
}
const shouldUpdateFdwPort = isDefined(
remoteServerToUpdate.foreignDataWrapperOptions?.port,
);
if (shouldUpdateFdwPort) {
parameters.push(remoteServerToUpdate?.foreignDataWrapperOptions?.port);
parametersPositions['port'] = parameters.length;
}
if (shouldUpdateFdwDbname || shouldUpdateFdwHost || shouldUpdateFdwPort) {
const fwdOptionsQuery = `"foreignDataWrapperOptions" = jsonb_set(${
shouldUpdateFdwDbname && shouldUpdateFdwHost && shouldUpdateFdwPort
? `jsonb_set(jsonb_set("foreignDataWrapperOptions", '{dbname}', to_jsonb($${parametersPositions['dbname']}::text)), '{host}', to_jsonb($${parametersPositions['host']}::text)), '{port}', to_jsonb($${parametersPositions['port']}::text)`
: shouldUpdateFdwDbname && shouldUpdateFdwHost
? `jsonb_set("foreignDataWrapperOptions", '{dbname}', to_jsonb($${parametersPositions['dbname']}::text)), '{host}', to_jsonb($${parametersPositions['host']}::text)`
: shouldUpdateFdwDbname && shouldUpdateFdwPort
? `jsonb_set("foreignDataWrapperOptions", '{dbname}', to_jsonb($${parametersPositions['dbname']}::text)), '{port}', to_jsonb($${parametersPositions['port']}::text)`
: shouldUpdateFdwHost && shouldUpdateFdwPort
? `jsonb_set("foreignDataWrapperOptions", '{host}', to_jsonb($${parametersPositions['host']}::text)), '{port}', to_jsonb($${parametersPositions['port']}::text)`
: shouldUpdateFdwDbname
? `"foreignDataWrapperOptions", '{dbname}', to_jsonb($${parametersPositions['dbname']}::text)`
: shouldUpdateFdwHost
? `"foreignDataWrapperOptions", '{host}', to_jsonb($${parametersPositions['host']}::text)`
: `"foreignDataWrapperOptions", '{port}', to_jsonb($${parametersPositions['port']}::text)`
})`;
options.push(fwdOptionsQuery);
}
const rawQuery = `UPDATE metadata."remoteServer" SET ${options.join(
', ',
)} WHERE "id"= $1 RETURNING *`;
return [parameters, rawQuery];
};

View File

@ -0,0 +1,14 @@
import { InputType, Field } from '@nestjs/graphql';
import { IsOptional } from 'class-validator';
@InputType()
export class UserMappingOptionsInput {
@IsOptional()
@Field(() => String, { nullable: true })
username: string;
@IsOptional()
@Field(() => String, { nullable: true })
password: string;
}

View File

@ -1,20 +0,0 @@
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');
}
};

View File

@ -0,0 +1,26 @@
import { BadRequestException } from '@nestjs/common';
import { isDefined } from 'class-validator';
const INPUT_REGEX = /^([A-Za-z0-9\-_.@]+)$/;
export const validateObjectAgainstInjections = (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 (!isDefined(value)) {
continue;
}
validateStringAgainstInjections(value.toString());
}
};
export const validateStringAgainstInjections = (input: string) => {
if (!INPUT_REGEX.test(input)) {
throw new BadRequestException('Invalid remote server input');
}
};