diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.service.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.service.ts index 185c1f41b..637ca3105 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.service.ts @@ -16,6 +16,8 @@ import { validateString, } from 'src/engine/metadata-modules/remote-server/utils/validate-remote-server-input'; import { ForeignDataWrapperQueryFactory } from 'src/engine/api/graphql/workspace-query-builder/factories/foreign-data-wrapper-query.factory'; +import { RemoteTableService } from 'src/engine/metadata-modules/remote-server/remote-table/remote-table.service'; +import { RemoteTableStatus } from 'src/engine/metadata-modules/remote-server/remote-table/dtos/remote-table.dto'; @Injectable() export class RemoteServerService { @@ -28,6 +30,7 @@ export class RemoteServerService { private readonly metadataDataSource: DataSource, private readonly environmentService: EnvironmentService, private readonly foreignDataWrapperQueryFactory: ForeignDataWrapperQueryFactory, + private readonly remoteTableService: RemoteTableService, ) {} async createOneRemoteServer( @@ -114,6 +117,26 @@ export class RemoteServerService { throw new NotFoundException('Object does not exist'); } + const remoteTablesToRemove = ( + await this.remoteTableService.findAvailableRemoteTablesByServerId( + id, + workspaceId, + ) + ).filter((remoteTable) => remoteTable.status === RemoteTableStatus.SYNCED); + + if (remoteTablesToRemove.length) { + for (const remoteTable of remoteTablesToRemove) { + await this.remoteTableService.updateRemoteTableSyncStatus( + { + remoteServerId: id, + name: remoteTable.name, + status: RemoteTableStatus.NOT_SYNCED, + }, + workspaceId, + ); + } + } + return this.metadataDataSource.transaction( async (entityManager: EntityManager) => { await entityManager.query( diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/dtos/remote-table-input.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/dtos/remote-table-input.ts index 13ee62074..32116d4d9 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/dtos/remote-table-input.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/dtos/remote-table-input.ts @@ -17,5 +17,5 @@ export class RemoteTableInput { status: RemoteTableStatus; @Field(() => String) - schema: string; + schema?: string; } diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-postgres-table/remote-postgres-table.module.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-postgres-table/remote-postgres-table.module.ts index bb74fd739..f12aba42a 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-postgres-table/remote-postgres-table.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-postgres-table/remote-postgres-table.module.ts @@ -1,10 +1,8 @@ import { Module } from '@nestjs/common'; import { RemotePostgresTableService } from 'src/engine/metadata-modules/remote-server/remote-table/remote-postgres-table/remote-postgres-table.service'; -import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; @Module({ - imports: [WorkspaceDataSourceModule], providers: [RemotePostgresTableService], exports: [RemotePostgresTableService], }) diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-postgres-table/remote-postgres-table.service.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-postgres-table/remote-postgres-table.service.ts index 5190c0a2b..1814090c7 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-postgres-table/remote-postgres-table.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-postgres-table/remote-postgres-table.service.ts @@ -6,56 +6,23 @@ import { RemoteServerEntity, RemoteServerType, } from 'src/engine/metadata-modules/remote-server/remote-server.entity'; -import { RemoteTableStatus } from 'src/engine/metadata-modules/remote-server/remote-table/dtos/remote-table.dto'; import { buildPostgresUrl, EXCLUDED_POSTGRES_SCHEMAS, } from 'src/engine/metadata-modules/remote-server/remote-table/remote-postgres-table/utils/remote-postgres-table.util'; import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; -import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service'; -import { getRemoteTableName } from 'src/engine/metadata-modules/remote-server/remote-table/utils/get-remote-table-name.util'; +import { RemoteTableColumn } from 'src/engine/metadata-modules/remote-server/remote-table/types/remote-table-column'; +import { RemoteTable } from 'src/engine/metadata-modules/remote-server/remote-table/types/remote-table'; @Injectable() export class RemotePostgresTableService { - constructor( - private readonly workspaceDataSourceService: WorkspaceDataSourceService, - private readonly environmentService: EnvironmentService, - ) {} - - public async findAvailableRemotePostgresTables( - workspaceId: string, - remoteServer: RemoteServerEntity, - ) { - const remotePostgresTables = - await this.fetchTablesFromRemotePostgresSchema(remoteServer); - - const workspaceDataSource = - await this.workspaceDataSourceService.connectToWorkspaceDataSource( - workspaceId, - ); - - const currentForeignTableNames = ( - await workspaceDataSource.query( - `SELECT foreign_table_name FROM information_schema.foreign_tables`, - ) - ).map((foreignTable) => foreignTable.foreign_table_name); - - return remotePostgresTables.map((remoteTable) => ({ - name: remoteTable.table_name, - schema: remoteTable.table_schema, - status: currentForeignTableNames.includes( - getRemoteTableName(remoteTable.table_name), - ) - ? RemoteTableStatus.SYNCED - : RemoteTableStatus.NOT_SYNCED, - })); - } + constructor(private readonly environmentService: EnvironmentService) {} public async fetchPostgresTableColumnsSchema( remoteServer: RemoteServerEntity, tableName: string, tableSchema: string, - ) { + ): Promise { const dataSource = new DataSource({ url: buildPostgresUrl( this.environmentService.get('LOGIN_TOKEN_SECRET'), @@ -73,12 +40,19 @@ export class RemotePostgresTableService { await dataSource.destroy(); - return columns; + return columns.map( + (column) => + ({ + columnName: column.column_name, + dataType: column.data_type, + udtName: column.udt_name, + }) as RemoteTableColumn, + ); } - private async fetchTablesFromRemotePostgresSchema( + public async fetchTablesFromRemotePostgresSchema( remoteServer: RemoteServerEntity, - ) { + ): Promise { const dataSource = new DataSource({ url: buildPostgresUrl( this.environmentService.get('LOGIN_TOKEN_SECRET'), @@ -104,6 +78,12 @@ export class RemotePostgresTableService { await dataSource.destroy(); - return remotePostgresTables; + return remotePostgresTables.map( + (table) => + ({ + tableName: table.table_name, + tableSchema: table.table_schema, + }) as RemoteTable, + ); } } diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table.module.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table.module.ts index 887d05f47..c2cc592f3 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table.module.ts @@ -10,19 +10,24 @@ import { RemotePostgresTableModule } from 'src/engine/metadata-modules/remote-se import { RemoteTableResolver } from 'src/engine/metadata-modules/remote-server/remote-table/remote-table.resolver'; import { RemoteTableService } from 'src/engine/metadata-modules/remote-server/remote-table/remote-table.service'; import { WorkspaceCacheVersionModule } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.module'; +import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module'; import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; +import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module'; @Module({ imports: [ TypeOrmModule.forFeature([RemoteServerEntity], 'metadata'), TypeOrmModule.forFeature([FeatureFlagEntity], 'core'), - WorkspaceDataSourceModule, DataSourceModule, ObjectMetadataModule, FieldMetadataModule, RemotePostgresTableModule, WorkspaceCacheVersionModule, + WorkspaceMigrationModule, + WorkspaceMigrationRunnerModule, + WorkspaceDataSourceModule, ], providers: [RemoteTableService, RemoteTableResolver], + exports: [RemoteTableService], }) export class RemoteTableModule {} 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 4ef3645f2..26b06edfd 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 @@ -1,14 +1,13 @@ import { NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { DataSource, Repository } from 'typeorm'; +import { Repository } from 'typeorm'; import { RemoteServerType, RemoteServerEntity, } from 'src/engine/metadata-modules/remote-server/remote-server.entity'; import { RemoteTableStatus } from 'src/engine/metadata-modules/remote-server/remote-table/dtos/remote-table.dto'; -import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service'; import { isPostgreSQLIntegrationEnabled, mapUdtNameToFieldType, @@ -19,13 +18,22 @@ import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metada import { CreateObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/create-object.input'; import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/field-metadata.service'; import { CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input'; -import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity'; import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; import { RemotePostgresTableService } from 'src/engine/metadata-modules/remote-server/remote-table/remote-postgres-table/remote-postgres-table.service'; 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 { 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'; +import { + WorkspaceMigrationColumnDefinition, + WorkspaceMigrationForeignTable, +} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; +import { RemoteTableColumn } from 'src/engine/metadata-modules/remote-server/remote-table/types/remote-table-column'; +import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service'; +import { RemoteTable } from 'src/engine/metadata-modules/remote-server/remote-table/types/remote-table'; export class RemoteTableService { constructor( @@ -35,12 +43,14 @@ export class RemoteTableService { >, @InjectRepository(FeatureFlagEntity, 'core') private readonly featureFlagRepository: Repository, - private readonly workspaceDataSourceService: WorkspaceDataSourceService, private readonly workspaceCacheVersionService: WorkspaceCacheVersionService, private readonly dataSourceService: DataSourceService, private readonly objectMetadataService: ObjectMetadataService, private readonly fieldMetadataService: FieldMetadataService, private readonly remotePostgresTableService: RemotePostgresTableService, + private readonly workspaceMigrationService: WorkspaceMigrationService, + private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService, + private readonly workspaceDataSourceService: WorkspaceDataSourceService, ) {} public async findAvailableRemoteTablesByServerId( @@ -58,20 +68,21 @@ export class RemoteTableService { throw new NotFoundException('Remote server does not exist'); } - switch (remoteServer.foreignDataWrapperType) { - case RemoteServerType.POSTGRES_FDW: - await isPostgreSQLIntegrationEnabled( - this.featureFlagRepository, - workspaceId, - ); + const currentForeignTableNames = + await this.fetchForeignTableNamesWithinWorkspace(workspaceId); - return this.remotePostgresTableService.findAvailableRemotePostgresTables( - workspaceId, - remoteServer, - ); - default: - throw new Error('Unsupported foreign data wrapper type'); - } + const tableInRemoteSchema = + await this.fetchTablesFromRemoteSchema(remoteServer); + + return tableInRemoteSchema.map((remoteTable) => ({ + name: remoteTable.tableName, + schema: remoteTable.tableSchema, + status: currentForeignTableNames.includes( + getRemoteTableName(remoteTable.tableName), + ) + ? RemoteTableStatus.SYNCED + : RemoteTableStatus.NOT_SYNCED, + })); } public async updateRemoteTableSyncStatus( @@ -89,37 +100,16 @@ export class RemoteTableService { throw new NotFoundException('Remote server does not exist'); } - const dataSourcesMetatada = - await this.dataSourceService.getDataSourcesMetadataFromWorkspaceId( - workspaceId, - ); - - if (!dataSourcesMetatada) { - throw new NotFoundException('Workspace data source does not exist'); - } - - const workspaceDataSource = - await this.workspaceDataSourceService.connectToWorkspaceDataSource( - workspaceId, - ); - switch (input.status) { case RemoteTableStatus.SYNCED: - await this.buildForeignTableAndMetadata( + await this.createForeignTableAndMetadata( input, remoteServer, workspaceId, - workspaceDataSource, - dataSourcesMetatada[0], ); break; case RemoteTableStatus.NOT_SYNCED: - await this.removeForeignTableAndMetadata( - input, - workspaceId, - workspaceDataSource, - dataSourcesMetatada[0].schema, - ); + await this.removeForeignTableAndMetadata(input, workspaceId); break; default: throw new Error('Unsupported remote table status'); @@ -130,67 +120,92 @@ export class RemoteTableService { return input; } - private async buildForeignTableAndMetadata( + private async createForeignTableAndMetadata( input: RemoteTableInput, remoteServer: RemoteServerEntity, workspaceId: string, - workspaceDataSource: DataSource, - dataSourceMetadata: DataSourceEntity, ) { - const localSchema = dataSourceMetadata.schema; + if (!input.schema) { + throw new Error('Schema is required for syncing remote table'); + } + + const currentForeignTableNames = + await this.fetchForeignTableNamesWithinWorkspace(workspaceId); + + if (currentForeignTableNames.includes(getRemoteTableName(input.name))) { + throw new Error('Remote table already exists'); + } - // TODO: Add strong typing for remote table columns. Will be done when we have another use case than Postgres const remoteTableColumns = await this.fetchTableColumnsSchema( remoteServer, input.name, input.schema, ); - const foreignTableColumns = remoteTableColumns - .map((column) => `"${column.column_name}" ${column.data_type}`) - .join(', '); - const remoteTableName = getRemoteTableName(input.name); const remoteTableLabel = camelToTitleCase(remoteTableName); // We only support remote tables with an id column for now. const remoteTableIdColumn = remoteTableColumns.filter( - (column) => column.column_name === 'id', + (column) => column.columnName === 'id', )?.[0]; if (!remoteTableIdColumn) { throw new Error('Remote table must have an id column'); } - await workspaceDataSource.query( - `CREATE FOREIGN TABLE ${localSchema}."${remoteTableName}" (${foreignTableColumns}) SERVER "${remoteServer.foreignDataWrapperId}" OPTIONS (schema_name '${input.schema}', table_name '${input.name}')`, + const dataSourceMetatada = + await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail( + workspaceId, + ); + + await this.workspaceMigrationService.createCustomMigration( + generateMigrationName(`create-foreign-table-${remoteTableName}`), + workspaceId, + [ + { + name: remoteTableName, + action: 'create_foreign_table', + foreignTable: { + columns: remoteTableColumns.map( + (column) => + ({ + columnName: column.columnName, + columnType: column.dataType, + }) satisfies WorkspaceMigrationColumnDefinition, + ), + referencedTableName: input.name, + referencedTableSchema: input.schema, + foreignDataWrapperId: remoteServer.foreignDataWrapperId, + } satisfies WorkspaceMigrationForeignTable, + }, + ], ); - await workspaceDataSource.query( - `COMMENT ON FOREIGN TABLE ${localSchema}."${remoteTableName}" IS e'@graphql({"primary_key_columns": ["id"], "totalCount": {"enabled": true}})'`, + await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations( + workspaceId, ); - // Should be done in a transaction. To be discussed const objectMetadata = await this.objectMetadataService.createOne({ nameSingular: remoteTableName, namePlural: `${remoteTableName}s`, labelSingular: remoteTableLabel, labelPlural: `${remoteTableLabel}s`, description: 'Remote table', - dataSourceId: dataSourceMetadata.id, + dataSourceId: dataSourceMetatada.id, workspaceId: workspaceId, icon: 'IconUser', isRemote: true, - remoteTablePrimaryKeyColumnType: remoteTableIdColumn.udt_name, + remoteTablePrimaryKeyColumnType: remoteTableIdColumn.udtName, } as CreateObjectInput); for (const column of remoteTableColumns) { const field = await this.fieldMetadataService.createOne({ - name: column.column_name, - label: camelToTitleCase(camelCase(column.column_name)), + name: column.columnName, + label: camelToTitleCase(camelCase(column.columnName)), description: 'Field of remote', // TODO: function should work for other types than Postgres - type: mapUdtNameToFieldType(column.udt_name), + type: mapUdtNameToFieldType(column.udtName), workspaceId: workspaceId, objectMetadataId: objectMetadata.id, isRemoteCreation: true, @@ -198,7 +213,7 @@ export class RemoteTableService { icon: 'IconUser', } as CreateFieldInput); - if (column.column_name === 'id') { + if (column.columnName === 'id') { await this.objectMetadataService.updateOne(objectMetadata.id, { labelIdentifierFieldMetadataId: field.id, }); @@ -209,11 +224,16 @@ export class RemoteTableService { private async removeForeignTableAndMetadata( input: RemoteTableInput, workspaceId: string, - workspaceDataSource: DataSource, - localSchema: string, ) { 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 }, @@ -226,8 +246,19 @@ export class RemoteTableService { ); } - await workspaceDataSource.query( - `DROP FOREIGN TABLE ${localSchema}."${input.name}Remote"`, + await this.workspaceMigrationService.createCustomMigration( + generateMigrationName(`drop-foreign-table-${input.name}`), + workspaceId, + [ + { + name: remoteTableName, + action: 'drop_foreign_table', + }, + ], + ); + + await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations( + workspaceId, ); } @@ -235,7 +266,7 @@ export class RemoteTableService { remoteServer: RemoteServerEntity, tableName: string, tableSchema: string, - ) { + ): Promise { switch (remoteServer.foreignDataWrapperType) { case RemoteServerType.POSTGRES_FDW: await isPostgreSQLIntegrationEnabled( @@ -252,4 +283,35 @@ export class RemoteTableService { throw new Error('Unsupported foreign data wrapper type'); } } + + 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 { + switch (remoteServer.foreignDataWrapperType) { + case RemoteServerType.POSTGRES_FDW: + await isPostgreSQLIntegrationEnabled( + this.featureFlagRepository, + remoteServer.workspaceId, + ); + + return this.remotePostgresTableService.fetchTablesFromRemotePostgresSchema( + remoteServer, + ); + default: + throw new Error('Unsupported foreign data wrapper type'); + } + } } diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/types/remote-table-column.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/types/remote-table-column.ts new file mode 100644 index 000000000..d3c8012be --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/types/remote-table-column.ts @@ -0,0 +1,6 @@ +// Type will evolve as we add more remote table types +export type RemoteTableColumn = { + columnName: string; + dataType: string; + udtName: string; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/types/remote-table.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/types/remote-table.ts new file mode 100644 index 000000000..443f1245d --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/types/remote-table.ts @@ -0,0 +1,5 @@ +// Type will evolve as we add more remote table types +export type RemoteTable = { + tableName: string; + tableSchema: string; +}; 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 c0bd136f6..667d0c697 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 @@ -62,6 +62,13 @@ export type WorkspaceMigrationCreateComment = { comment: string; }; +export type WorkspaceMigrationForeignTable = { + columns: WorkspaceMigrationColumnDefinition[]; + referencedTableName: string; + referencedTableSchema: string; + foreignDataWrapperId: string; +}; + export type WorkspaceMigrationColumnAction = { action: WorkspaceMigrationColumnActionType; } & ( @@ -75,8 +82,14 @@ export type WorkspaceMigrationColumnAction = { export type WorkspaceMigrationTableAction = { name: string; - action: 'create' | 'alter' | 'drop'; + action: + | 'create' + | 'alter' + | 'drop' + | 'create_foreign_table' + | 'drop_foreign_table'; columns?: WorkspaceMigrationColumnAction[]; + foreignTable?: WorkspaceMigrationForeignTable; }; @Entity('workspaceMigration') diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service.ts index 5c645a260..867927049 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service.ts @@ -18,6 +18,7 @@ import { WorkspaceMigrationColumnCreateRelation, WorkspaceMigrationColumnAlter, WorkspaceMigrationColumnDropRelation, + WorkspaceMigrationForeignTable, } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; import { WorkspaceCacheVersionService } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.service'; import { WorkspaceMigrationEnumService } from 'src/engine/workspace-manager/workspace-migration-runner/services/workspace-migration-enum.service'; @@ -131,6 +132,19 @@ export class WorkspaceMigrationRunnerService { case 'drop': await queryRunner.dropTable(`${schemaName}.${tableMigration.name}`); break; + case 'create_foreign_table': + await this.createForeignTable( + queryRunner, + schemaName, + tableMigration.name, + tableMigration?.foreignTable, + ); + break; + case 'drop_foreign_table': + await queryRunner.query( + `DROP FOREIGN TABLE ${schemaName}."${tableMigration.name}"`, + ); + break; default: throw new Error( `Migration table action ${tableMigration.action} not supported`, @@ -431,4 +445,27 @@ export class WorkspaceMigrationRunnerService { COMMENT ON TABLE "${schemaName}"."${tableName}" IS e'${comment}'; `); } + + private async createForeignTable( + queryRunner: QueryRunner, + schemaName: string, + name: string, + foreignTable: WorkspaceMigrationForeignTable | undefined, + ) { + if (!foreignTable) { + return; + } + + const foreignTableColumns = foreignTable.columns + .map((column) => `"${column.columnName}" ${column.columnType}`) + .join(', '); + + await queryRunner.query( + `CREATE FOREIGN TABLE ${schemaName}."${name}" (${foreignTableColumns}) SERVER "${foreignTable.foreignDataWrapperId}" OPTIONS (schema_name '${foreignTable.referencedTableSchema}', table_name '${foreignTable.referencedTableName}')`, + ); + + await queryRunner.query(` + COMMENT ON FOREIGN TABLE "${schemaName}"."${name}" IS '@graphql({"primary_key_columns": ["id"], "totalCount": {"enabled": true}})'; + `); + } }