Use migrations for remote tables (#4877)
Foreign tables should be created using migrations, as we do for standard tables. Since those are not really generated from the object metadata but from the remote table, those migrations won't live in the object metadata service. This PR: - creates new types of migration : create_foreign_table and drop_foreign_table - triggers those migrations rather than raw queries directly - moves the logic to fetch current foreign tables into the remote table service since this is not directly linked to postgres data wrapper - adds logic to unsync all tables before deleting --------- Co-authored-by: Thomas Trompette <thomast@twenty.com>
This commit is contained in:
@ -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<T extends RemoteServerType> {
|
||||
@ -28,6 +30,7 @@ export class RemoteServerService<T extends RemoteServerType> {
|
||||
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<T extends RemoteServerType> {
|
||||
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(
|
||||
|
||||
@ -17,5 +17,5 @@ export class RemoteTableInput {
|
||||
status: RemoteTableStatus;
|
||||
|
||||
@Field(() => String)
|
||||
schema: string;
|
||||
schema?: string;
|
||||
}
|
||||
|
||||
@ -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],
|
||||
})
|
||||
|
||||
@ -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<RemoteServerType>,
|
||||
) {
|
||||
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<RemoteServerType>,
|
||||
tableName: string,
|
||||
tableSchema: string,
|
||||
) {
|
||||
): Promise<RemoteTableColumn[]> {
|
||||
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<RemoteServerType>,
|
||||
) {
|
||||
): Promise<RemoteTable[]> {
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {}
|
||||
|
||||
@ -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<FeatureFlagEntity>,
|
||||
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<RemoteServerType>,
|
||||
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<RemoteServerType>,
|
||||
tableName: string,
|
||||
tableSchema: string,
|
||||
) {
|
||||
): Promise<RemoteTableColumn[]> {
|
||||
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<RemoteServerType>,
|
||||
): Promise<RemoteTable[]> {
|
||||
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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
// Type will evolve as we add more remote table types
|
||||
export type RemoteTableColumn = {
|
||||
columnName: string;
|
||||
dataType: string;
|
||||
udtName: string;
|
||||
};
|
||||
@ -0,0 +1,5 @@
|
||||
// Type will evolve as we add more remote table types
|
||||
export type RemoteTable = {
|
||||
tableName: string;
|
||||
tableSchema: string;
|
||||
};
|
||||
@ -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')
|
||||
|
||||
@ -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}})';
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user