diff --git a/packages/twenty-front/src/modules/databases/hooks/useGetDatabaseConnectionTables.ts b/packages/twenty-front/src/modules/databases/hooks/useGetDatabaseConnectionTables.ts index 6548222e6..dd50582bb 100644 --- a/packages/twenty-front/src/modules/databases/hooks/useGetDatabaseConnectionTables.ts +++ b/packages/twenty-front/src/modules/databases/hooks/useGetDatabaseConnectionTables.ts @@ -18,7 +18,7 @@ export const useGetDatabaseConnectionTables = ({ }: UseGetDatabaseConnectionTablesParams) => { const apolloMetadataClient = useApolloMetadataClient(); - const { data } = useQuery< + const { data, error } = useQuery< GetManyRemoteTablesQuery, GetManyRemoteTablesQueryVariables >(GET_MANY_REMOTE_TABLES, { @@ -33,5 +33,6 @@ export const useGetDatabaseConnectionTables = ({ return { tables: data?.findAvailableRemoteTablesByServerId || [], + error, }; }; diff --git a/packages/twenty-front/src/modules/object-record/components/RecordChip.tsx b/packages/twenty-front/src/modules/object-record/components/RecordChip.tsx index 1c167e194..60eeea769 100644 --- a/packages/twenty-front/src/modules/object-record/components/RecordChip.tsx +++ b/packages/twenty-front/src/modules/object-record/components/RecordChip.tsx @@ -1,6 +1,8 @@ +import { useContext } from 'react'; import { EntityChip } from 'twenty-ui'; import { useMapToObjectRecordIdentifier } from '@/object-metadata/hooks/useMapToObjectRecordIdentifier'; +import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64'; @@ -21,8 +23,16 @@ export const RecordChip = ({ objectNameSingular, }); + // Will only exists if the chip is inside a record table. + // This is temporary until we have the show page for remote objects. + const { isReadOnly } = useContext(RecordTableRowContext); + const objectRecordIdentifier = mapToObjectRecordIdentifier(record); + const linkToEntity = isReadOnly + ? undefined + : objectRecordIdentifier.linkToShowPage; + return ( diff --git a/packages/twenty-front/src/modules/settings/integrations/components/SettingsIntegrationDatabaseConnectionSyncStatus.tsx b/packages/twenty-front/src/modules/settings/integrations/components/SettingsIntegrationDatabaseConnectionSyncStatus.tsx index a13aede90..b50a048a2 100644 --- a/packages/twenty-front/src/modules/settings/integrations/components/SettingsIntegrationDatabaseConnectionSyncStatus.tsx +++ b/packages/twenty-front/src/modules/settings/integrations/components/SettingsIntegrationDatabaseConnectionSyncStatus.tsx @@ -1,6 +1,7 @@ import { useGetDatabaseConnectionTables } from '@/databases/hooks/useGetDatabaseConnectionTables'; import { Status } from '@/ui/display/status/components/Status'; import { RemoteTableStatus } from '~/generated-metadata/graphql'; +import { isDefined } from '~/utils/isDefined'; type SettingsIntegrationDatabaseConnectionSyncStatusProps = { connectionId: string; @@ -11,11 +12,15 @@ export const SettingsIntegrationDatabaseConnectionSyncStatus = ({ connectionId, skip, }: SettingsIntegrationDatabaseConnectionSyncStatusProps) => { - const { tables } = useGetDatabaseConnectionTables({ + const { tables, error } = useGetDatabaseConnectionTables({ connectionId, skip, }); + if (isDefined(error)) { + return ; + } + const syncedTables = tables.filter( (table) => table.status === RemoteTableStatus.Synced, ); diff --git a/packages/twenty-front/src/pages/object-record/RecordIndexPageHeader.tsx b/packages/twenty-front/src/pages/object-record/RecordIndexPageHeader.tsx index f92acab85..5039ec7a3 100644 --- a/packages/twenty-front/src/pages/object-record/RecordIndexPageHeader.tsx +++ b/packages/twenty-front/src/pages/object-record/RecordIndexPageHeader.tsx @@ -35,8 +35,11 @@ export const RecordIndexPageHeader = ({ const canAddRecord = recordIndexViewType === ViewType.Table && !objectMetadataItem?.isRemote; + const pageHeaderTitle = + objectMetadataItem?.labelPlural ?? capitalize(objectNamePlural); + return ( - + {canAddRecord && } diff --git a/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrationNewDatabaseConnection.tsx b/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrationNewDatabaseConnection.tsx index d8b27084d..fba7e9885 100644 --- a/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrationNewDatabaseConnection.tsx +++ b/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrationNewDatabaseConnection.tsx @@ -95,14 +95,18 @@ export const SettingsIntegrationNewDatabaseConnection = () => { const formValues = formConfig.getValues(); try { - await createOneDatabaseConnection( + const createdConnection = await createOneDatabaseConnection( createRemoteServerInputSchema.parse({ ...formValues, foreignDataWrapperType: getForeignDataWrapperType(databaseKey), }), ); - navigate(`${settingsIntegrationsPagePath}/${databaseKey}`); + const connectionId = createdConnection.data?.createOneRemoteServer.id; + + navigate( + `${settingsIntegrationsPagePath}/${databaseKey}/${connectionId}`, + ); } catch (error) { enqueueSnackBar((error as Error).message, { variant: 'error', diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts index f9e92d284..1f9f239ec 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts @@ -361,7 +361,7 @@ export class ObjectMetadataService extends TypeOrmQueryService { @@ -117,21 +116,18 @@ export class RemoteServerService { throw new NotFoundException('Object does not exist'); } - const remoteTablesToRemove = ( - await this.remoteTableService.findAvailableRemoteTablesByServerId( - id, + const foreignTablesToRemove = + await this.remoteTableService.fetchForeignTableNamesWithinWorkspace( workspaceId, - ) - ).filter((remoteTable) => remoteTable.status === RemoteTableStatus.SYNCED); + remoteServer.foreignDataWrapperId, + ); - if (remoteTablesToRemove.length) { - for (const remoteTable of remoteTablesToRemove) { - await this.remoteTableService.unsyncRemoteTable( - { - remoteServerId: id, - name: remoteTable.name, - }, + if (foreignTablesToRemove.length) { + for (const foreignTableName of foreignTablesToRemove) { + await this.remoteTableService.removeForeignTableAndMetadata( + foreignTableName, workspaceId, + remoteServer, ); } } diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table.service.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table.service.ts index ce9a8a392..d2279380d 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table.service.ts @@ -26,7 +26,7 @@ import { RemotePostgresTableService } from 'src/engine/metadata-modules/remote-s import { WorkspaceCacheVersionService } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.service'; import { camelCase } from 'src/utils/camel-case'; import { camelToTitleCase } from 'src/utils/camel-to-title-case'; -import { getRemoteTableName } from 'src/engine/metadata-modules/remote-server/remote-table/utils/get-remote-table-name.util'; +import { getRemoteTableLocalName } from 'src/engine/metadata-modules/remote-server/remote-table/utils/get-remote-table-local-name.util'; import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service'; 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'; @@ -73,7 +73,10 @@ export class RemoteTableService { } const currentForeignTableNames = - await this.fetchForeignTableNamesWithinWorkspace(workspaceId); + await this.fetchForeignTableNamesWithinWorkspace( + workspaceId, + remoteServer.foreignDataWrapperId, + ); const tableInRemoteSchema = await this.fetchTablesFromRemoteSchema(remoteServer); @@ -82,7 +85,7 @@ export class RemoteTableService { name: remoteTable.tableName, schema: remoteTable.tableSchema, status: currentForeignTableNames.includes( - getRemoteTableName(remoteTable.tableName), + getRemoteTableLocalName(remoteTable.tableName), ) ? RemoteTableStatus.SYNCED : RemoteTableStatus.NOT_SYNCED, @@ -107,20 +110,95 @@ export class RemoteTableService { workspaceId, ); - await this.workspaceCacheVersionService.incrementVersion(workspaceId); - return remoteTable; } public async unsyncRemoteTable(input: RemoteTableInput, workspaceId: string) { - const remoteTable = await this.removeForeignTableAndMetadata( - input, + const remoteServer = await this.remoteServerRepository.findOne({ + where: { + id: input.remoteServerId, + workspaceId, + }, + }); + + if (!remoteServer) { + throw new NotFoundException('Remote server does not exist'); + } + + const remoteTableLocalName = getRemoteTableLocalName(input.name); + + await this.removeForeignTableAndMetadata( + remoteTableLocalName, + workspaceId, + remoteServer, + ); + + return { + name: input.name, + schema: input.schema, + status: RemoteTableStatus.NOT_SYNCED, + }; + } + + public async fetchForeignTableNamesWithinWorkspace( + workspaceId: string, + foreignDataWrapperId: string, + ): Promise { + const workspaceDataSource = + await this.workspaceDataSourceService.connectToWorkspaceDataSource( + workspaceId, + ); + + return ( + await workspaceDataSource.query( + `SELECT foreign_table_name, foreign_server_name FROM information_schema.foreign_tables WHERE foreign_server_name = '${foreignDataWrapperId}'`, + ) + ).map((foreignTable) => foreignTable.foreign_table_name); + } + + public async removeForeignTableAndMetadata( + remoteTableLocalName: string, + workspaceId: string, + remoteServer: RemoteServerEntity, + ) { + const currentForeignTableNames = + await this.fetchForeignTableNamesWithinWorkspace( + workspaceId, + remoteServer.foreignDataWrapperId, + ); + + if (!currentForeignTableNames.includes(remoteTableLocalName)) { + throw new Error('Remote table does not exist'); + } + + const objectMetadata = + await this.objectMetadataService.findOneWithinWorkspace(workspaceId, { + where: { nameSingular: remoteTableLocalName }, + }); + + if (objectMetadata) { + await this.objectMetadataService.deleteOneObject( + { id: objectMetadata.id }, + workspaceId, + ); + } + + await this.workspaceMigrationService.createCustomMigration( + generateMigrationName(`drop-foreign-table-${remoteTableLocalName}`), + workspaceId, + [ + { + name: remoteTableLocalName, + action: WorkspaceMigrationTableActionType.DROP_FOREIGN_TABLE, + }, + ], + ); + + await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations( workspaceId, ); await this.workspaceCacheVersionService.incrementVersion(workspaceId); - - return remoteTable; } private async createForeignTableAndMetadata( @@ -133,9 +211,14 @@ export class RemoteTableService { } const currentForeignTableNames = - await this.fetchForeignTableNamesWithinWorkspace(workspaceId); + await this.fetchForeignTableNamesWithinWorkspace( + workspaceId, + remoteServer.foreignDataWrapperId, + ); - if (currentForeignTableNames.includes(getRemoteTableName(input.name))) { + if ( + currentForeignTableNames.includes(getRemoteTableLocalName(input.name)) + ) { throw new Error('Remote table already exists'); } @@ -145,8 +228,8 @@ export class RemoteTableService { input.schema, ); - const remoteTableName = getRemoteTableName(input.name); - const remoteTableLabel = camelToTitleCase(remoteTableName); + const remoteTableLocalName = getRemoteTableLocalName(input.name); + const remoteTableLabel = camelToTitleCase(remoteTableLocalName); // We only support remote tables with an id column for now. const remoteTableIdColumn = remoteTableColumns.filter( @@ -163,11 +246,11 @@ export class RemoteTableService { ); await this.workspaceMigrationService.createCustomMigration( - generateMigrationName(`create-foreign-table-${remoteTableName}`), + generateMigrationName(`create-foreign-table-${remoteTableLocalName}`), workspaceId, [ { - name: remoteTableName, + name: remoteTableLocalName, action: WorkspaceMigrationTableActionType.CREATE_FOREIGN_TABLE, foreignTable: { columns: remoteTableColumns.map( @@ -190,8 +273,8 @@ export class RemoteTableService { ); const objectMetadata = await this.objectMetadataService.createOne({ - nameSingular: remoteTableName, - namePlural: `${remoteTableName}s`, + nameSingular: remoteTableLocalName, + namePlural: `${remoteTableLocalName}s`, labelSingular: remoteTableLabel, labelPlural: `${remoteTableLabel}s`, description: 'Remote table', @@ -223,6 +306,8 @@ export class RemoteTableService { } } + await this.workspaceCacheVersionService.incrementVersion(workspaceId); + return { name: input.name, schema: input.schema, @@ -230,53 +315,6 @@ export class RemoteTableService { }; } - private async removeForeignTableAndMetadata( - input: RemoteTableInput, - workspaceId: string, - ): Promise { - const remoteTableName = getRemoteTableName(input.name); - - const currentForeignTableNames = - await this.fetchForeignTableNamesWithinWorkspace(workspaceId); - - if (!currentForeignTableNames.includes(remoteTableName)) { - throw new Error('Remote table does not exist'); - } - - const objectMetadata = - await this.objectMetadataService.findOneWithinWorkspace(workspaceId, { - where: { nameSingular: remoteTableName }, - }); - - if (objectMetadata) { - await this.objectMetadataService.deleteOneObject( - { id: objectMetadata.id }, - workspaceId, - ); - } - - await this.workspaceMigrationService.createCustomMigration( - generateMigrationName(`drop-foreign-table-${input.name}`), - workspaceId, - [ - { - name: remoteTableName, - action: WorkspaceMigrationTableActionType.DROP_FOREIGN_TABLE, - }, - ], - ); - - await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations( - workspaceId, - ); - - return { - name: input.name, - schema: input.schema, - status: RemoteTableStatus.NOT_SYNCED, - }; - } - private async fetchTableColumnsSchema( remoteServer: RemoteServerEntity, tableName: string, @@ -299,19 +337,6 @@ export class RemoteTableService { } } - private async fetchForeignTableNamesWithinWorkspace(workspaceId: string) { - const workspaceDataSource = - await this.workspaceDataSourceService.connectToWorkspaceDataSource( - workspaceId, - ); - - return ( - await workspaceDataSource.query( - `SELECT foreign_table_name FROM information_schema.foreign_tables`, - ) - ).map((foreignTable) => foreignTable.foreign_table_name); - } - private async fetchTablesFromRemoteSchema( remoteServer: RemoteServerEntity, ): Promise { diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/utils/get-remote-table-name.util.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/utils/get-remote-table-local-name.util.ts similarity index 57% rename from packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/utils/get-remote-table-name.util.ts rename to packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/utils/get-remote-table-local-name.util.ts index bcc7a2123..a7162c4c5 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/utils/get-remote-table-name.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/utils/get-remote-table-local-name.util.ts @@ -1,4 +1,4 @@ import { camelCase } from 'src/utils/camel-case'; -export const getRemoteTableName = (distantTableName: string) => +export const getRemoteTableLocalName = (distantTableName: string) => `${camelCase(distantTableName)}Remote`; diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/utils/validate-remote-server-input.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/utils/validate-remote-server-input.ts index 5c043f2a9..e9cc3e542 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/utils/validate-remote-server-input.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/utils/validate-remote-server-input.ts @@ -1,4 +1,4 @@ -const INPUT_REGEX = /^([A-Za-z0-9\-\_\.]+)$/; +const INPUT_REGEX = /^([A-Za-z0-9\-_.@]+)$/; export const validateObject = (input: object) => { for (const [key, value] of Object.entries(input)) { diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.entity.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.entity.ts index 6acee6df2..224eaf4e3 100644 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.entity.ts @@ -88,7 +88,7 @@ export enum WorkspaceMigrationTableActionType { ALTER = 'alter', DROP = 'drop', CREATE_FOREIGN_TABLE = 'create_foreign_table', - DROP_FOREIGN_TABLE = 'drop_foreign_table' + DROP_FOREIGN_TABLE = 'drop_foreign_table', } export type WorkspaceMigrationTableAction = { diff --git a/packages/twenty-ui/src/display/chip/components/EntityChip.tsx b/packages/twenty-ui/src/display/chip/components/EntityChip.tsx index 246782496..740875fe5 100644 --- a/packages/twenty-ui/src/display/chip/components/EntityChip.tsx +++ b/packages/twenty-ui/src/display/chip/components/EntityChip.tsx @@ -38,7 +38,6 @@ export const EntityChip = ({ maxWidth, }: EntityChipProps) => { const navigate = useNavigate(); - const theme = useTheme(); const handleLinkClick = (event: React.MouseEvent) => {