Build stripe integration on backend side (#5246)

Adding stripe integration by making the server logic independent of the
input fields:
- query factories (remote server, foreign data wrapper, foreign table)
to loop on fields and values without hardcoding the names of the fields
- adding stripe input and type
- add the logic to handle static schema. Simply creating a big object to
store into the server

Additional work:
- rename username field to user. This is the input intended for postgres
user mapping and we now need a matching by name

---------

Co-authored-by: Thomas Trompette <thomast@twenty.com>
This commit is contained in:
Thomas Trompette
2024-05-02 17:13:15 +02:00
committed by GitHub
parent 5128ea3ffb
commit f9c19c839b
30 changed files with 396 additions and 283 deletions

View File

@ -1,135 +1,43 @@
import { BadRequestException } from '@nestjs/common';
import { isDefined } from 'class-validator';
import {
ForeignDataWrapperOptions,
RemoteServerEntity,
RemoteServerType,
} from 'src/engine/metadata-modules/remote-server/remote-server.entity';
import { UserMappingOptions } from 'src/engine/metadata-modules/remote-server/utils/user-mapping-options.utils';
import { UserMappingOptions } from 'src/engine/metadata-modules/remote-server/types/user-mapping-options';
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 = (
export const buildUpdateRemoteServerRawQuery = (
remoteServerToUpdate: DeepPartial<RemoteServerEntity<RemoteServerType>> &
Pick<RemoteServerEntity<RemoteServerType>, 'workspaceId' | 'id'>,
): [any[], string] => {
const parameters: any[] = [remoteServerToUpdate.id];
const parametersPositions = {};
const options: string[] = [];
const [parameters, parametersPositions] =
buildParametersAndPositions(remoteServerToUpdate);
if (remoteServerToUpdate.userMappingOptions) {
const userMappingOptionsQuery = buildUserMappingOptionsQuery(
parameters,
parametersPositions,
const userMappingOptionsQuery = buildJsonRawQuery(
remoteServerToUpdate.userMappingOptions,
parametersPositions,
'userMappingOptions',
);
if (userMappingOptionsQuery) options.push(userMappingOptionsQuery);
options.push(userMappingOptionsQuery);
}
const shouldUpdateFdwDbname = isDefined(
remoteServerToUpdate.foreignDataWrapperOptions?.dbname,
);
if (remoteServerToUpdate.foreignDataWrapperOptions) {
const foreignDataWrapperOptionsQuery = buildJsonRawQuery(
remoteServerToUpdate.foreignDataWrapperOptions,
parametersPositions,
'foreignDataWrapperOptions',
);
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);
options.push(foreignDataWrapperOptionsQuery);
}
if (options.length < 1) {
@ -142,3 +50,57 @@ export const updateRemoteServerRawQuery = (
return [parameters, rawQuery];
};
const buildParametersAndPositions = (
remoteServerToUpdate: DeepPartial<RemoteServerEntity<RemoteServerType>> &
Pick<RemoteServerEntity<RemoteServerType>, 'workspaceId' | 'id'>,
): [any[], object] => {
const parameters: any[] = [remoteServerToUpdate.id];
const parametersPositions = {};
if (remoteServerToUpdate.userMappingOptions) {
Object.entries(remoteServerToUpdate.userMappingOptions).forEach(
([key, value]) => {
parameters.push(value);
parametersPositions[key] = parameters.length;
},
);
}
if (remoteServerToUpdate.foreignDataWrapperOptions) {
Object.entries(remoteServerToUpdate.foreignDataWrapperOptions).forEach(
([key, value]) => {
parameters.push(value);
parametersPositions[key] = parameters.length;
},
);
}
return [parameters, parametersPositions];
};
const buildJsonRawQuery = (
options:
| Partial<UserMappingOptions>
| Partial<ForeignDataWrapperOptions<RemoteServerType>>,
parametersPositions: object,
objectName: string,
): string => {
const buildJsonSet = (
opts:
| Partial<UserMappingOptions>
| Partial<ForeignDataWrapperOptions<RemoteServerType>>,
): string => {
const [[firstKey, _], ...followingOptions] = Object.entries(opts);
let query = `jsonb_set("${objectName}", '{${firstKey}}', to_jsonb($${parametersPositions[firstKey]}::text))`;
followingOptions.forEach(([key, _]) => {
query = `jsonb_set(${query}, '{${key}}', to_jsonb($${parametersPositions[key]}::text))`;
});
return query;
};
return `"${objectName}" = ${buildJsonSet(options)}`;
};

View File

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

View File

@ -32,6 +32,8 @@ const getFeatureFlagKey = (remoteServerType: RemoteServerType) => {
switch (remoteServerType) {
case RemoteServerType.POSTGRES_FDW:
return FeatureFlagKeys.IsPostgreSQLIntegrationEnabled;
case RemoteServerType.STRIPE_FDW:
return FeatureFlagKeys.IsStripeIntegrationEnabled;
default:
throw new BadRequestException(
`Type ${remoteServerType} is not supported.`,