Skip to content

Commit

Permalink
Build stripe integration on backend side (twentyhq#5246)
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
thomtrp and Thomas Trompette committed May 2, 2024
1 parent 5128ea3 commit f9c19c8
Show file tree
Hide file tree
Showing 30 changed files with 394 additions and 281 deletions.
4 changes: 2 additions & 2 deletions packages/twenty-front/src/generated-metadata/gql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/
* Therefore it is highly recommended to use the babel or swc plugin for production.
*/
const documents = {
"\n fragment RemoteServerFields on RemoteServer {\n id\n createdAt\n foreignDataWrapperId\n foreignDataWrapperOptions\n foreignDataWrapperType\n userMappingOptions {\n username\n }\n updatedAt\n schema\n }\n": types.RemoteServerFieldsFragmentDoc,
"\n fragment RemoteServerFields on RemoteServer {\n id\n createdAt\n foreignDataWrapperId\n foreignDataWrapperOptions\n foreignDataWrapperType\n userMappingOptions {\n user\n }\n updatedAt\n schema\n }\n": types.RemoteServerFieldsFragmentDoc,
"\n fragment RemoteTableFields on RemoteTable {\n id\n name\n schema\n status\n }\n": types.RemoteTableFieldsFragmentDoc,
"\n \n mutation createServer($input: CreateRemoteServerInput!) {\n createOneRemoteServer(input: $input) {\n ...RemoteServerFields\n }\n }\n": types.CreateServerDocument,
"\n mutation deleteServer($input: RemoteServerIdInput!) {\n deleteOneRemoteServer(input: $input) {\n id\n }\n }\n": types.DeleteServerDocument,
Expand Down Expand Up @@ -50,7 +50,7 @@ export function graphql(source: string): unknown;
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment RemoteServerFields on RemoteServer {\n id\n createdAt\n foreignDataWrapperId\n foreignDataWrapperOptions\n foreignDataWrapperType\n userMappingOptions {\n username\n }\n updatedAt\n schema\n }\n"): (typeof documents)["\n fragment RemoteServerFields on RemoteServer {\n id\n createdAt\n foreignDataWrapperId\n foreignDataWrapperOptions\n foreignDataWrapperType\n userMappingOptions {\n username\n }\n updatedAt\n schema\n }\n"];
export function graphql(source: "\n fragment RemoteServerFields on RemoteServer {\n id\n createdAt\n foreignDataWrapperId\n foreignDataWrapperOptions\n foreignDataWrapperType\n userMappingOptions {\n user\n }\n updatedAt\n schema\n }\n"): (typeof documents)["\n fragment RemoteServerFields on RemoteServer {\n id\n createdAt\n foreignDataWrapperId\n foreignDataWrapperOptions\n foreignDataWrapperType\n userMappingOptions {\n user\n }\n updatedAt\n schema\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
Expand Down
42 changes: 21 additions & 21 deletions packages/twenty-front/src/generated-metadata/graphql.ts

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions packages/twenty-front/src/generated/graphql.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -577,14 +577,14 @@ export type RemoteServer = {
id: Scalars['ID'];
schema?: Maybe<Scalars['String']>;
updatedAt: Scalars['DateTime'];
userMappingOptions?: Maybe<UserMappingOptionsUsername>;
userMappingOptions?: Maybe<UserMappingOptionsUser>;
};

export type RemoteTable = {
__typename?: 'RemoteTable';
id?: Maybe<Scalars['UUID']>;
name: Scalars['String'];
schema: Scalars['String'];
schema?: Maybe<Scalars['String']>;
status: RemoteTableStatus;
};

Expand Down Expand Up @@ -772,9 +772,9 @@ export type UserExists = {
exists: Scalars['Boolean'];
};

export type UserMappingOptionsUsername = {
__typename?: 'UserMappingOptionsUsername';
username?: Maybe<Scalars['String']>;
export type UserMappingOptionsUser = {
__typename?: 'UserMappingOptionsUser';
user?: Maybe<Scalars['String']>;
};

export type UserWorkspace = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export const DATABASE_CONNECTION_FRAGMENT = gql`
foreignDataWrapperOptions
foreignDataWrapperType
userMappingOptions {
username
user
}
updatedAt
schema
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export const settingsIntegrationPostgreSQLConnectionFormSchema = z.object({
dbname: z.string().min(1),
host: z.string().min(1),
port: z.preprocess((val) => parseInt(val as string), z.number().positive()),
username: z.string().min(1),
user: z.string().min(1),
password: z.string().min(1),
schema: z.string().min(1),
});
Expand Down Expand Up @@ -52,9 +52,9 @@ export const SettingsIntegrationPostgreSQLConnectionForm = ({
{ name: 'host' as const, label: 'Host', placeholder: 'host' },
{ name: 'port' as const, label: 'Port', placeholder: '5432' },
{
name: 'username' as const,
label: 'Username',
placeholder: 'username',
name: 'user' as const,
label: 'User',
placeholder: 'user',
},
{
name: 'password' as const,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const getFormDefaultValuesFromConnection = ({
dbname: connection.foreignDataWrapperOptions.dbname,
host: connection.foreignDataWrapperOptions.host,
port: connection.foreignDataWrapperOptions.port,
username: connection.userMappingOptions?.username || undefined,
user: connection.userMappingOptions?.user || undefined,
schema: connection.schema || undefined,
password: '',
};
Expand All @@ -51,7 +51,7 @@ export const formatValuesForUpdate = ({
const formattedValues = {
userMappingOptions: pickBy(
{
username: formValues.username,
user: formValues.user,
password: formValues.password,
},
identity,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const createRemoteServerInputSchema = newConnectionSchema
},
userMappingOptions: {
password: values.password,
username: values.username,
user: values.user,
},
schema: values.schema,
}));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const mockedRemoteServers = [
foreignDataWrapperType: 'postgres_fdw',
userMappingOptions: {
__typename: 'UserMappingOptionsDTO',
username: 'twenty',
user: 'twenty',
},
updatedAt: '2024-04-30T13:41:25.858Z',
schema: 'public',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ export const seedFeatureFlags = async (
workspaceId: workspaceId,
value: true,
},
{
key: FeatureFlagKeys.IsStripeIntegrationEnabled,
workspaceId: workspaceId,
value: true,
},
])
.execute();
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
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 { ArgsAliasFactory } from './args-alias.factory';
import { ArgsStringFactory } from './args-string.factory';
Expand Down Expand Up @@ -30,5 +30,5 @@ export const workspaceQueryBuilderFactories = [
UpdateOneQueryFactory,
UpdateManyQueryFactory,
DeleteManyQueryFactory,
ForeignDataWrapperQueryFactory,
ForeignDataWrapperServerQueryFactory,
];

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { Injectable } from '@nestjs/common';

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

@Injectable()
export class ForeignDataWrapperServerQueryFactory {
createForeignDataWrapperServer(
foreignDataWrapperId: string,
foreignDataWrapperType: RemoteServerType,
foreignDataWrapperOptions: ForeignDataWrapperOptions<RemoteServerType>,
) {
const options = this.buildQueryOptions(foreignDataWrapperOptions, false);

return `CREATE SERVER "${foreignDataWrapperId}" FOREIGN DATA WRAPPER ${foreignDataWrapperType} OPTIONS (${options})`;
}

updateForeignDataWrapperServer({
foreignDataWrapperId,
foreignDataWrapperOptions,
}: {
foreignDataWrapperId: string;
foreignDataWrapperOptions: Partial<
ForeignDataWrapperOptions<RemoteServerType>
>;
}) {
const options = this.buildQueryOptions(foreignDataWrapperOptions, true);

return `ALTER SERVER "${foreignDataWrapperId}" OPTIONS (${options})`;
}

createUserMapping(
foreignDataWrapperId: string,
userMappingOptions: UserMappingOptions,
) {
const options = this.buildQueryOptions(userMappingOptions, false);

// CURRENT_USER works for now since we are using only one user. But if we switch to a user per workspace, we need to change this.
return `CREATE USER MAPPING IF NOT EXISTS FOR CURRENT_USER SERVER "${foreignDataWrapperId}" OPTIONS (${options})`;
}

updateUserMapping(
foreignDataWrapperId: string,
userMappingOptions: Partial<UserMappingOptions>,
) {
const options = this.buildQueryOptions(userMappingOptions, true);

// CURRENT_USER works for now since we are using only one user. But if we switch to a user per workspace, we need to change this.
return `ALTER USER MAPPING FOR CURRENT_USER SERVER "${foreignDataWrapperId}" OPTIONS (${options})`;
}

private buildQueryOptions(
options:
| ForeignDataWrapperOptions<RemoteServerType>
| Partial<ForeignDataWrapperOptions<RemoteServerType>>
| UserMappingOptions
| Partial<UserMappingOptions>,
isUpdate: boolean,
) {
const prefix = isUpdate ? 'SET ' : '';

return Object.entries(options)
.map(([key, value]) => `${prefix}${key} '${value}'`)
.join(', ');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export enum FeatureFlagKeys {
IsEventObjectEnabled = 'IS_EVENT_OBJECT_ENABLED',
IsAirtableIntegrationEnabled = 'IS_AIRTABLE_INTEGRATION_ENABLED',
IsPostgreSQLIntegrationEnabled = 'IS_POSTGRESQL_INTEGRATION_ENABLED',
IsStripeIntegrationEnabled = 'IS_STRIPE_INTEGRATION_ENABLED',
IsMultiSelectEnabled = 'IS_MULTI_SELECT_ENABLED',
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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')
Expand Down

0 comments on commit f9c19c8

Please sign in to comment.