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

@ -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;