diff --git a/packages/twenty-front/src/generated-metadata/gql.ts b/packages/twenty-front/src/generated-metadata/gql.ts index 6b8a4bc97e..d86971caa3 100644 --- a/packages/twenty-front/src/generated-metadata/gql.ts +++ b/packages/twenty-front/src/generated-metadata/gql.ts @@ -13,6 +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 mutation createServer($input: CreateRemoteServerInput!) {\n createOneRemoteServer(input: $input) {\n id\n foreignDataWrapperId\n foreignDataWrapperOptions\n foreignDataWrapperType\n }\n }\n": types.CreateServerDocument, "\n query GetManyDatabaseConnections($input: RemoteServerTypeInput!) {\n findManyRemoteServersByType(input: $input) {\n id\n createdAt\n foreignDataWrapperId\n foreignDataWrapperOptions\n foreignDataWrapperType\n updatedAt\n }\n }\n": types.GetManyDatabaseConnectionsDocument, "\n mutation CreateOneObjectMetadataItem($input: CreateOneObjectInput!) {\n createOneObject(input: $input) {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isActive\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n }\n }\n": types.CreateOneObjectMetadataItemDocument, "\n mutation CreateOneFieldMetadataItem($input: CreateOneFieldMetadataInput!) {\n createOneField(input: $input) {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isNullable\n createdAt\n updatedAt\n defaultValue\n options\n }\n }\n": types.CreateOneFieldMetadataItemDocument, @@ -38,6 +39,10 @@ const documents = { */ 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 mutation createServer($input: CreateRemoteServerInput!) {\n createOneRemoteServer(input: $input) {\n id\n foreignDataWrapperId\n foreignDataWrapperOptions\n foreignDataWrapperType\n }\n }\n"): (typeof documents)["\n mutation createServer($input: CreateRemoteServerInput!) {\n createOneRemoteServer(input: $input) {\n id\n foreignDataWrapperId\n foreignDataWrapperOptions\n foreignDataWrapperType\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 84a39ee046..d9a5140dc6 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -1188,6 +1188,13 @@ export type RelationEdge = { node: Relation; }; +export type CreateServerMutationVariables = Exact<{ + input: CreateRemoteServerInput; +}>; + + +export type CreateServerMutation = { __typename?: 'Mutation', createOneRemoteServer: { __typename?: 'RemoteServer', id: string, foreignDataWrapperId: string, foreignDataWrapperOptions?: any | null, foreignDataWrapperType: string } }; + export type GetManyDatabaseConnectionsQueryVariables = Exact<{ input: RemoteServerTypeInput; }>; @@ -1255,6 +1262,7 @@ export type ObjectMetadataItemsQueryVariables = Exact<{ export type ObjectMetadataItemsQuery = { __typename?: 'Query', objects: { __typename?: 'ObjectConnection', edges: Array<{ __typename?: 'objectEdge', node: { __typename?: 'object', id: string, dataSourceId: string, nameSingular: string, namePlural: string, labelSingular: string, labelPlural: string, description?: string | null, icon?: string | null, isCustom: boolean, isRemote: boolean, isActive: boolean, isSystem: boolean, createdAt: any, updatedAt: any, labelIdentifierFieldMetadataId?: string | null, imageIdentifierFieldMetadataId?: string | null, fields: { __typename?: 'ObjectFieldsConnection', edges: Array<{ __typename?: 'fieldEdge', node: { __typename?: 'field', id: string, type: FieldMetadataType, name: string, label: string, description?: string | null, icon?: string | null, isCustom?: boolean | null, isActive?: boolean | null, isSystem?: boolean | null, isNullable?: boolean | null, createdAt: any, updatedAt: any, defaultValue?: any | null, options?: any | null, fromRelationMetadata?: { __typename?: 'relation', id: string, relationType: RelationMetadataType, toFieldMetadataId: string, toObjectMetadata: { __typename?: 'object', id: string, dataSourceId: string, nameSingular: string, namePlural: string, isSystem: boolean } } | null, toRelationMetadata?: { __typename?: 'relation', id: string, relationType: RelationMetadataType, fromFieldMetadataId: string, fromObjectMetadata: { __typename?: 'object', id: string, dataSourceId: string, nameSingular: string, namePlural: string, isSystem: boolean } } | null, relationDefinition?: { __typename?: 'RelationDefinition', direction: RelationDefinitionType, sourceObjectMetadata: { __typename?: 'object', id: string, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'field', id: string, name: string }, targetObjectMetadata: { __typename?: 'object', id: string, nameSingular: string, namePlural: string }, targetFieldMetadata: { __typename?: 'field', id: string, name: string } } | null } }>, pageInfo: { __typename?: 'PageInfo', hasNextPage?: boolean | null, hasPreviousPage?: boolean | null, startCursor?: any | null, endCursor?: any | null } } } }>, pageInfo: { __typename?: 'PageInfo', hasNextPage?: boolean | null, hasPreviousPage?: boolean | null, startCursor?: any | null, endCursor?: any | null } } }; +export const CreateServerDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"createServer"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateRemoteServerInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createOneRemoteServer"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"foreignDataWrapperId"}},{"kind":"Field","name":{"kind":"Name","value":"foreignDataWrapperOptions"}},{"kind":"Field","name":{"kind":"Name","value":"foreignDataWrapperType"}}]}}]}}]} as unknown as DocumentNode; export const GetManyDatabaseConnectionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetManyDatabaseConnections"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteServerTypeInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findManyRemoteServersByType"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"foreignDataWrapperId"}},{"kind":"Field","name":{"kind":"Name","value":"foreignDataWrapperOptions"}},{"kind":"Field","name":{"kind":"Name","value":"foreignDataWrapperType"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode; export const CreateOneObjectMetadataItemDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateOneObjectMetadataItem"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateOneObjectInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createOneObject"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"dataSourceId"}},{"kind":"Field","name":{"kind":"Name","value":"nameSingular"}},{"kind":"Field","name":{"kind":"Name","value":"namePlural"}},{"kind":"Field","name":{"kind":"Name","value":"labelSingular"}},{"kind":"Field","name":{"kind":"Name","value":"labelPlural"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"isCustom"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"labelIdentifierFieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"imageIdentifierFieldMetadataId"}}]}}]}}]} as unknown as DocumentNode; export const CreateOneFieldMetadataItemDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateOneFieldMetadataItem"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateOneFieldMetadataInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createOneField"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"isCustom"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"isNullable"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"defaultValue"}},{"kind":"Field","name":{"kind":"Name","value":"options"}}]}}]}}]} as unknown as DocumentNode; diff --git a/packages/twenty-front/src/modules/databases/graphql/mutations/createOneDatabaseConnection.ts b/packages/twenty-front/src/modules/databases/graphql/mutations/createOneDatabaseConnection.ts new file mode 100644 index 0000000000..d64129e015 --- /dev/null +++ b/packages/twenty-front/src/modules/databases/graphql/mutations/createOneDatabaseConnection.ts @@ -0,0 +1,12 @@ +import { gql } from '@apollo/client'; + +export const CREATE_ONE_DATABASE_CONNECTION = gql` + mutation createServer($input: CreateRemoteServerInput!) { + createOneRemoteServer(input: $input) { + id + foreignDataWrapperId + foreignDataWrapperOptions + foreignDataWrapperType + } + } +`; diff --git a/packages/twenty-front/src/modules/databases/hooks/useCreateOneDatabaseConnection.ts b/packages/twenty-front/src/modules/databases/hooks/useCreateOneDatabaseConnection.ts new file mode 100644 index 0000000000..3221c305c7 --- /dev/null +++ b/packages/twenty-front/src/modules/databases/hooks/useCreateOneDatabaseConnection.ts @@ -0,0 +1,38 @@ +import { ApolloClient, useMutation } from '@apollo/client'; +import { getOperationName } from '@apollo/client/utilities'; + +import { CREATE_ONE_DATABASE_CONNECTION } from '@/databases/graphql/mutations/createOneDatabaseConnection'; +import { GET_MANY_DATABASE_CONNECTIONS } from '@/databases/graphql/queries/findManyDatabaseConnections'; +import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient'; +import { + CreateRemoteServerInput, + CreateServerMutation, + CreateServerMutationVariables, +} from '~/generated-metadata/graphql'; + +export const useCreateOneDatabaseConnection = () => { + const apolloMetadataClient = useApolloMetadataClient(); + + const [mutate] = useMutation< + CreateServerMutation, + CreateServerMutationVariables + >(CREATE_ONE_DATABASE_CONNECTION, { + client: apolloMetadataClient ?? ({} as ApolloClient), + }); + + const createOneDatabaseConnection = async ( + input: CreateRemoteServerInput, + ) => { + return await mutate({ + variables: { + input, + }, + awaitRefetchQueries: true, + refetchQueries: [getOperationName(GET_MANY_DATABASE_CONNECTIONS) ?? ''], + }); + }; + + return { + createOneDatabaseConnection, + }; +}; diff --git a/packages/twenty-front/src/modules/databases/hooks/useGetDatabaseConnections.ts b/packages/twenty-front/src/modules/databases/hooks/useGetDatabaseConnections.ts index 4939f57240..979249183d 100644 --- a/packages/twenty-front/src/modules/databases/hooks/useGetDatabaseConnections.ts +++ b/packages/twenty-front/src/modules/databases/hooks/useGetDatabaseConnections.ts @@ -1,6 +1,7 @@ import { useQuery } from '@apollo/client'; import { GET_MANY_DATABASE_CONNECTIONS } from '@/databases/graphql/queries/findManyDatabaseConnections'; +import { getForeignDataWrapperType } from '@/databases/utils/getForeignDataWrapperType'; import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient'; import { GetManyDatabaseConnectionsQuery, @@ -17,16 +18,17 @@ export const useGetDatabaseConnections = ({ skip, }: UseGetDatabaseConnectionsParams) => { const apolloMetadataClient = useApolloMetadataClient(); + const foreignDataWrapperType = getForeignDataWrapperType(databaseKey); const { data } = useQuery< GetManyDatabaseConnectionsQuery, GetManyDatabaseConnectionsQueryVariables >(GET_MANY_DATABASE_CONNECTIONS, { client: apolloMetadataClient ?? undefined, - skip: skip || !apolloMetadataClient || databaseKey !== 'postgresql', + skip: skip || !apolloMetadataClient || !foreignDataWrapperType, variables: { input: { - foreignDataWrapperType: 'postgres_fdw', + foreignDataWrapperType: foreignDataWrapperType || '', }, }, }); diff --git a/packages/twenty-front/src/modules/databases/utils/getForeignDataWrapperType.ts b/packages/twenty-front/src/modules/databases/utils/getForeignDataWrapperType.ts new file mode 100644 index 0000000000..af50a62419 --- /dev/null +++ b/packages/twenty-front/src/modules/databases/utils/getForeignDataWrapperType.ts @@ -0,0 +1,8 @@ +export const getForeignDataWrapperType = (databaseKey: string) => { + switch (databaseKey) { + case 'postgresql': + return 'postgres_fdw'; + default: + return null; + } +}; diff --git a/packages/twenty-front/src/modules/settings/integrations/components/SettingsIntegrationDatabaseConnectionForm.tsx b/packages/twenty-front/src/modules/settings/integrations/components/SettingsIntegrationDatabaseConnectionForm.tsx index 747405f0cf..1efcab86b5 100644 --- a/packages/twenty-front/src/modules/settings/integrations/components/SettingsIntegrationDatabaseConnectionForm.tsx +++ b/packages/twenty-front/src/modules/settings/integrations/components/SettingsIntegrationDatabaseConnectionForm.tsx @@ -7,7 +7,7 @@ import { TextInput } from '@/ui/input/components/TextInput'; export const settingsIntegrationPostgreSQLConnectionFormSchema = z.object({ dbname: z.string().min(1), host: z.string().min(1), - port: z.number().positive(), + port: z.preprocess((val) => parseInt(val as string), z.number().positive()), username: z.string().min(1), password: z.string().min(1), }); diff --git a/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrationNewDatabaseConnection.tsx b/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrationNewDatabaseConnection.tsx index bb7dda87b8..b513352d39 100644 --- a/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrationNewDatabaseConnection.tsx +++ b/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrationNewDatabaseConnection.tsx @@ -5,6 +5,10 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { IconSettings } from 'twenty-ui'; import { z } from 'zod'; +import { useCreateOneDatabaseConnection } from '@/databases/hooks/useCreateOneDatabaseConnection'; +import { getForeignDataWrapperType } from '@/databases/utils/getForeignDataWrapperType'; +import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons'; +import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { SettingsIntegrationPostgreSQLConnectionForm, @@ -15,13 +19,32 @@ import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; import { AppPath } from '@/types/AppPath'; import { SettingsPath } from '@/types/SettingsPath'; import { H2Title } from '@/ui/display/typography/components/H2Title'; +import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'; import { Section } from '@/ui/layout/section/components/Section'; import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb'; import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; +import { CreateRemoteServerInput } from '~/generated-metadata/graphql'; const newConnectionSchema = settingsIntegrationPostgreSQLConnectionFormSchema; +const createRemoteServerInputSchema = newConnectionSchema + .extend({ + foreignDataWrapperType: z.string().min(1), + }) + .transform((values) => ({ + foreignDataWrapperType: values.foreignDataWrapperType, + foreignDataWrapperOptions: { + dbname: values.dbname, + host: values.host, + port: values.port, + }, + userMappingOptions: { + password: values.password, + user: values.username, + }, + })); + type SettingsIntegrationNewConnectionFormValues = z.infer< typeof newConnectionSchema >; @@ -35,6 +58,9 @@ export const SettingsIntegrationNewDatabaseConnection = () => { ({ from: { key } }) => key === databaseKey, ); + const { createOneDatabaseConnection } = useCreateOneDatabaseConnection(); + const { enqueueSnackBar } = useSnackBar(); + const isAirtableIntegrationEnabled = useIsFeatureEnabled( 'IS_AIRTABLE_INTEGRATION_ENABLED', ); @@ -63,24 +89,54 @@ export const SettingsIntegrationNewDatabaseConnection = () => { SettingsPath.Integrations, ); + const canSave = formConfig.formState.isValid; + + const handleSave = async () => { + const formValues = formConfig.getValues(); + + try { + await createOneDatabaseConnection( + createRemoteServerInputSchema.parse({ + ...formValues, + foreignDataWrapperType: getForeignDataWrapperType(databaseKey), + }), + ); + + navigate(`${settingsIntegrationsPagePath}/${databaseKey}`); + } catch (error) { + enqueueSnackBar((error as Error).message, { + variant: 'error', + }); + } + }; + return ( // eslint-disable-next-line react/jsx-props-no-spreading - + + + + navigate(`${settingsIntegrationsPagePath}/${databaseKey}`) + } + onSave={handleSave} + /> + {databaseKey === 'postgresql' ? (