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

@ -7,7 +7,7 @@ import {
ForeignDataWrapperOptions,
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';
@InputType()
export class CreateRemoteServerInput<T extends RemoteServerType> {

View File

@ -10,7 +10,7 @@ import {
import {
UserMappingOptions,
UserMappingOptionsUpdateInput,
} from 'src/engine/metadata-modules/remote-server/utils/user-mapping-options.utils';
} from 'src/engine/metadata-modules/remote-server/types/user-mapping-options';
@InputType()
export class UpdateRemoteServerInput<T extends RemoteServerType> {

View File

@ -2,9 +2,9 @@ import { ObjectType, Field } from '@nestjs/graphql';
import { IsOptional } from 'class-validator';
@ObjectType('UserMappingOptionsUsername')
@ObjectType('UserMappingOptionsUser')
export class UserMappingOptionsDTO {
@IsOptional()
@Field(() => String, { nullable: true })
username: string;
user: string;
}

View File

@ -10,11 +10,12 @@ import {
} from 'typeorm';
import { RemoteTableEntity } from 'src/engine/metadata-modules/remote-server/remote-table/remote-table.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';
import { DistantTables } from 'src/engine/metadata-modules/remote-server/remote-table/distant-table/types/distant-table';
export enum RemoteServerType {
POSTGRES_FDW = 'postgres_fdw',
STRIPE_FDW = 'stripe_fdw',
}
type PostgresForeignDataWrapperOptions = {
@ -23,10 +24,17 @@ type PostgresForeignDataWrapperOptions = {
dbname: string;
};
type StripeForeignDataWrapperOptions = {
api_key: string;
};
export type ForeignDataWrapperOptions<T extends RemoteServerType> =
T extends RemoteServerType.POSTGRES_FDW
? PostgresForeignDataWrapperOptions
: never;
: T extends RemoteServerType.STRIPE_FDW
? StripeForeignDataWrapperOptions
: never;
@Entity('remoteServer')
export class RemoteServerEntity<T extends RemoteServerType> {
@PrimaryGeneratedColumn('uuid')

View File

@ -1,7 +1,7 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ForeignDataWrapperQueryFactory } from 'src/engine/api/graphql/workspace-query-builder/factories/foreign-data-wrapper-query.factory';
import { ForeignDataWrapperServerQueryFactory } from 'src/engine/api/graphql/workspace-query-builder/factories/foreign-data-wrapper-server-query.factory';
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { RemoteServerEntity } from 'src/engine/metadata-modules/remote-server/remote-server.entity';
import { RemoteServerResolver } from 'src/engine/metadata-modules/remote-server/remote-server.resolver';
@ -19,7 +19,7 @@ import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/works
providers: [
RemoteServerService,
RemoteServerResolver,
ForeignDataWrapperQueryFactory,
ForeignDataWrapperServerQueryFactory,
],
})
export class RemoteServerModule {}

View File

@ -20,11 +20,11 @@ import {
validateObjectAgainstInjections,
validateStringAgainstInjections,
} from 'src/engine/metadata-modules/remote-server/utils/validate-remote-server-input.utils';
import { ForeignDataWrapperQueryFactory } from 'src/engine/api/graphql/workspace-query-builder/factories/foreign-data-wrapper-query.factory';
import { ForeignDataWrapperServerQueryFactory } from 'src/engine/api/graphql/workspace-query-builder/factories/foreign-data-wrapper-server-query.factory';
import { RemoteTableService } from 'src/engine/metadata-modules/remote-server/remote-table/remote-table.service';
import { UpdateRemoteServerInput } from 'src/engine/metadata-modules/remote-server/dtos/update-remote-server.input';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
import { updateRemoteServerRawQuery } from 'src/engine/metadata-modules/remote-server/utils/build-update-remote-server-raw-query.utils';
import { buildUpdateRemoteServerRawQuery } from 'src/engine/metadata-modules/remote-server/utils/build-update-remote-server-raw-query.utils';
import { validateRemoteServerType } from 'src/engine/metadata-modules/remote-server/utils/validate-remote-server-type.util';
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
@ -38,7 +38,7 @@ export class RemoteServerService<T extends RemoteServerType> {
@InjectDataSource('metadata')
private readonly metadataDataSource: DataSource,
private readonly environmentService: EnvironmentService,
private readonly foreignDataWrapperQueryFactory: ForeignDataWrapperQueryFactory,
private readonly foreignDataWrapperServerQueryFactory: ForeignDataWrapperServerQueryFactory,
private readonly remoteTableService: RemoteTableService,
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
@InjectRepository(FeatureFlagEntity, 'core')
@ -85,7 +85,7 @@ export class RemoteServerService<T extends RemoteServerType> {
);
const foreignDataWrapperQuery =
this.foreignDataWrapperQueryFactory.createForeignDataWrapper(
this.foreignDataWrapperServerQueryFactory.createForeignDataWrapperServer(
createdRemoteServer.foreignDataWrapperId,
remoteServerInput.foreignDataWrapperType,
remoteServerInput.foreignDataWrapperOptions,
@ -95,7 +95,7 @@ export class RemoteServerService<T extends RemoteServerType> {
if (remoteServerInput.userMappingOptions) {
const userMappingQuery =
this.foreignDataWrapperQueryFactory.createUserMapping(
this.foreignDataWrapperServerQueryFactory.createUserMapping(
createdRemoteServer.foreignDataWrapperId,
remoteServerInput.userMappingOptions,
);
@ -167,18 +167,20 @@ export class RemoteServerService<T extends RemoteServerType> {
!isEmpty(partialRemoteServerWithUpdates.foreignDataWrapperOptions)
) {
const foreignDataWrapperQuery =
this.foreignDataWrapperQueryFactory.updateForeignDataWrapper({
foreignDataWrapperId,
foreignDataWrapperOptions:
partialRemoteServerWithUpdates.foreignDataWrapperOptions,
});
this.foreignDataWrapperServerQueryFactory.updateForeignDataWrapperServer(
{
foreignDataWrapperId,
foreignDataWrapperOptions:
partialRemoteServerWithUpdates.foreignDataWrapperOptions,
},
);
await entityManager.query(foreignDataWrapperQuery);
}
if (!isEmpty(partialRemoteServerWithUpdates.userMappingOptions)) {
const userMappingQuery =
this.foreignDataWrapperQueryFactory.updateUserMapping(
this.foreignDataWrapperServerQueryFactory.updateUserMapping(
foreignDataWrapperId,
partialRemoteServerWithUpdates.userMappingOptions,
);
@ -254,7 +256,7 @@ export class RemoteServerService<T extends RemoteServerType> {
Pick<RemoteServerEntity<RemoteServerType>, 'workspaceId' | 'id'>,
): Promise<RemoteServerEntity<RemoteServerType>> {
const [parameters, rawQuery] =
updateRemoteServerRawQuery(remoteServerToUpdate);
buildUpdateRemoteServerRawQuery(remoteServerToUpdate);
const updateResult = await this.workspaceDataSourceService.executeRawQuery(
rawQuery,

View File

@ -11,6 +11,7 @@ import {
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
import { DistantTableColumn } from 'src/engine/metadata-modules/remote-server/remote-table/distant-table/types/distant-table-column';
import { DistantTables } from 'src/engine/metadata-modules/remote-server/remote-table/distant-table/types/distant-table';
import { STRIPE_DISTANT_TABLES } from 'src/engine/metadata-modules/remote-server/remote-table/distant-table/util/stripe-distant-tables.util';
@Injectable()
export class DistantTableService {
@ -27,7 +28,9 @@ export class DistantTableService {
tableName: string,
): Promise<DistantTableColumn[]> {
if (!remoteServer.availableTables) {
throw new Error('Remote server available tables are not defined');
throw new BadRequestException(
'Remote server available tables are not defined',
);
}
return remoteServer.availableTables[tableName];
@ -47,6 +50,20 @@ export class DistantTableService {
private async createAvailableTables(
remoteServer: RemoteServerEntity<RemoteServerType>,
workspaceId: string,
): Promise<DistantTables> {
if (remoteServer.schema) {
return this.createAvailableTablesFromDynamicSchema(
remoteServer,
workspaceId,
);
}
return this.createAvailableTablesFromStaticSchema(remoteServer);
}
private async createAvailableTablesFromDynamicSchema(
remoteServer: RemoteServerEntity<RemoteServerType>,
workspaceId: string,
): Promise<DistantTables> {
if (!remoteServer.schema) {
throw new BadRequestException('Remote server schema is not defined');
@ -99,4 +116,21 @@ export class DistantTableService {
return availableTables;
}
private async createAvailableTablesFromStaticSchema(
remoteServer: RemoteServerEntity<RemoteServerType>,
): Promise<DistantTables> {
switch (remoteServer.foreignDataWrapperType) {
case RemoteServerType.STRIPE_FDW:
this.remoteServerRepository.update(remoteServer.id, {
availableTables: STRIPE_DISTANT_TABLES,
});
return STRIPE_DISTANT_TABLES;
default:
throw new BadRequestException(
`Type ${remoteServer.foreignDataWrapperType} does not have a static schema.`,
);
}
}
}

View File

@ -0,0 +1,91 @@
import { DistantTables } from 'src/engine/metadata-modules/remote-server/remote-table/distant-table/types/distant-table';
export const STRIPE_DISTANT_TABLES: DistantTables = {
balance_transactions: [
{ columnName: 'id', dataType: 'text', udtName: 'text' },
{ columnName: 'amount', dataType: 'bigint', udtName: 'int8' },
{ columnName: 'currency', dataType: 'text', udtName: 'text' },
{ columnName: 'description', dataType: 'text', udtName: 'text' },
{ columnName: 'fee', dataType: 'bigint', udtName: 'int8' },
{ columnName: 'net', dataType: 'bigint', udtName: 'int8' },
{ columnName: 'status', dataType: 'text', udtName: 'text' },
{ columnName: 'type', dataType: 'text', udtName: 'text' },
{ columnName: 'created', dataType: 'timestamp', udtName: 'timestamp' },
],
customers: [
{ columnName: 'id', dataType: 'text', udtName: 'text' },
{ columnName: 'email', dataType: 'text', udtName: 'text' },
{ columnName: 'name', dataType: 'text', udtName: 'text' },
{ columnName: 'description', dataType: 'text', udtName: 'text' },
{ columnName: 'created', dataType: 'timestamp', udtName: 'timestamp' },
],
disputes: [
{ columnName: 'id', dataType: 'text', udtName: 'text' },
{ columnName: 'amount', dataType: 'bigint', udtName: 'int8' },
{ columnName: 'currency', dataType: 'text', udtName: 'text' },
{ columnName: 'charge', dataType: 'text', udtName: 'text' },
{ columnName: 'payment_intent', dataType: 'text', udtName: 'text' },
{ columnName: 'reason', dataType: 'text', udtName: 'text' },
{ columnName: 'status', dataType: 'text', udtName: 'text' },
{ columnName: 'created', dataType: 'timestamp', udtName: 'timestamp' },
],
files: [
{ columnName: 'id', dataType: 'text', udtName: 'text' },
{ columnName: 'filename', dataType: 'text', udtName: 'text' },
{ columnName: 'purpose', dataType: 'text', udtName: 'text' },
{ columnName: 'title', dataType: 'text', udtName: 'text' },
{ columnName: 'size', dataType: 'bigint', udtName: 'int8' },
{ columnName: 'type', dataType: 'text', udtName: 'text' },
{ columnName: 'url', dataType: 'text', udtName: 'text' },
{ columnName: 'created', dataType: 'timestamp', udtName: 'timestamp' },
{ columnName: 'expires_at', dataType: 'timestamp', udtName: 'timestamp' },
],
file_links: [
{ columnName: 'id', dataType: 'text', udtName: 'text' },
{ columnName: 'file', dataType: 'text', udtName: 'text' },
{ columnName: 'url', dataType: 'text', udtName: 'text' },
{ columnName: 'created', dataType: 'timestamp', udtName: 'timestamp' },
{ columnName: 'expired', dataType: 'bool', udtName: 'boolean' },
{ columnName: 'expires_at', dataType: 'timestamp', udtName: 'timestamp' },
],
mandates: [
{ columnName: 'id', dataType: 'text', udtName: 'text' },
{ columnName: 'payment_method', dataType: 'text', udtName: 'text' },
{ columnName: 'status', dataType: 'text', udtName: 'text' },
{ columnName: 'type', dataType: 'text', udtName: 'text' },
],
payouts: [
{ columnName: 'id', dataType: 'text', udtName: 'text' },
{ columnName: 'amount', dataType: 'bigint', udtName: 'int8' },
{ columnName: 'currency', dataType: 'text', udtName: 'text' },
{ columnName: 'description', dataType: 'text', udtName: 'text' },
{ columnName: 'status', dataType: 'text', udtName: 'text' },
{ columnName: 'created', dataType: 'timestamp', udtName: 'timestamp' },
],
refunds: [
{ columnName: 'id', dataType: 'text', udtName: 'text' },
{ columnName: 'amount', dataType: 'bigint', udtName: 'int8' },
{ columnName: 'currency', dataType: 'text', udtName: 'text' },
{ columnName: 'charge', dataType: 'text', udtName: 'text' },
{ columnName: 'payment_intent', dataType: 'text', udtName: 'text' },
{ columnName: 'reason', dataType: 'text', udtName: 'text' },
{ columnName: 'status', dataType: 'text', udtName: 'text' },
{ columnName: 'created', dataType: 'timestamp', udtName: 'timestamp' },
],
topups: [
{ columnName: 'id', dataType: 'text', udtName: 'text' },
{ columnName: 'amount', dataType: 'bigint', udtName: 'int8' },
{ columnName: 'currency', dataType: 'text', udtName: 'text' },
{ columnName: 'description', dataType: 'text', udtName: 'text' },
{ columnName: 'status', dataType: 'text', udtName: 'text' },
{ columnName: 'created', dataType: 'timestamp', udtName: 'timestamp' },
],
transfers: [
{ columnName: 'id', dataType: 'text', udtName: 'text' },
{ columnName: 'amount', dataType: 'bigint', udtName: 'int8' },
{ columnName: 'currency', dataType: 'text', udtName: 'text' },
{ columnName: 'description', dataType: 'text', udtName: 'text' },
{ columnName: 'destination', dataType: 'text', udtName: 'text' },
{ columnName: 'created', dataType: 'timestamp', udtName: 'timestamp' },
],
};

View File

@ -1,7 +1,7 @@
import { ObjectType, Field, registerEnumType } from '@nestjs/graphql';
import { IDField } from '@ptc-org/nestjs-query-graphql';
import { IsEnum } from 'class-validator';
import { IsEnum, IsOptional } from 'class-validator';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
@ -27,6 +27,7 @@ export class RemoteTableDTO {
@Field(() => RemoteTableStatus)
status: RemoteTableStatus;
@Field(() => String)
@IsOptional()
@Field(() => String, { nullable: true })
schema?: string;
}

View File

@ -26,6 +26,7 @@ import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace
import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service';
import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util';
import {
ReferencedTable,
WorkspaceMigrationForeignColumnDefinition,
WorkspaceMigrationForeignTable,
WorkspaceMigrationTableActionType,
@ -338,6 +339,11 @@ export class RemoteTableService {
remoteServer: RemoteServerEntity<RemoteServerType>,
distantTableColumns: DistantTableColumn[],
) {
const referencedTable: ReferencedTable = this.buildReferencedTable(
remoteServer,
remoteTableInput,
);
const workspaceMigration =
await this.workspaceMigrationService.createCustomMigration(
generateMigrationName(`create-foreign-table-${localTableName}`),
@ -355,8 +361,7 @@ export class RemoteTableService {
distantColumnName: column.columnName,
}) satisfies WorkspaceMigrationForeignColumnDefinition,
),
referencedTableName: remoteTableInput.name,
referencedTableSchema: remoteServer.schema,
referencedTable,
foreignDataWrapperId: remoteServer.foreignDataWrapperId,
} satisfies WorkspaceMigrationForeignTable,
},
@ -430,4 +435,21 @@ export class RemoteTableService {
}
}
}
private buildReferencedTable(
remoteServer: RemoteServerEntity<RemoteServerType>,
remoteTableInput: RemoteTableInput,
): ReferencedTable {
switch (remoteServer.foreignDataWrapperType) {
case RemoteServerType.POSTGRES_FDW:
return {
table_name: remoteTableInput.name,
schema_name: remoteServer.schema,
};
case RemoteServerType.STRIPE_FDW:
return { object: remoteTableInput.name };
default:
throw new BadRequestException('Foreign data wrapper not supported');
}
}
}

View File

@ -7,6 +7,9 @@ export const mapUdtNameToFieldType = (udtName: string): FieldMetadataType => {
case 'uuid':
return FieldMetadataType.UUID;
case 'varchar':
case 'text':
case 'bigint':
case 'int8':
return FieldMetadataType.TEXT;
case 'bool':
return FieldMetadataType.BOOLEAN;

View File

@ -7,7 +7,7 @@ import { IsOptional } from 'class-validator';
export class UserMappingOptions {
@IsOptional()
@Field(() => String, { nullable: true })
username: string;
user: string;
@IsOptional()
@Field(() => String, { nullable: true })
@ -18,7 +18,7 @@ export class UserMappingOptions {
export class UserMappingOptionsUpdateInput {
@IsOptional()
@Field(() => String, { nullable: true })
username?: string;
user?: string;
@IsOptional()
@Field(() => String, { nullable: true })

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

@ -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.`,

View File

@ -67,10 +67,20 @@ export type WorkspaceMigrationForeignColumnDefinition =
distantColumnName: string;
};
type ReferencedObject = {
object: string;
};
type ReferencedTableWithSchema = {
table_name: string;
schema_name: string;
};
export type ReferencedTable = ReferencedObject | ReferencedTableWithSchema;
export type WorkspaceMigrationForeignTable = {
columns: WorkspaceMigrationForeignColumnDefinition[];
referencedTableName: string;
referencedTableSchema: string;
referencedTable: ReferencedObject | ReferencedTableWithSchema;
foreignDataWrapperId: string;
};