463dee3fe6
In this PR we are 1. cleaning typeORM service by removing connectToDataSource method 2. using workspaceDataSource instead of mainDataSource when possible, and replacing raw SQL with workspaceRepository methods to use
617 lines
19 KiB
TypeScript
617 lines
19 KiB
TypeScript
import { Logger } from '@nestjs/common';
|
|
import { InjectRepository } from '@nestjs/typeorm';
|
|
|
|
import isEmpty from 'lodash.isempty';
|
|
import { plural } from 'pluralize';
|
|
import { Repository } from 'typeorm';
|
|
|
|
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
|
import { CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
|
|
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
|
import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/field-metadata.service';
|
|
import { CreateObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/create-object.input';
|
|
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
|
|
import {
|
|
RemoteServerEntity,
|
|
RemoteServerType,
|
|
} from 'src/engine/metadata-modules/remote-server/remote-server.entity';
|
|
import { DistantTableService } from 'src/engine/metadata-modules/remote-server/remote-table/distant-table/distant-table.service';
|
|
import { sortDistantTables } from 'src/engine/metadata-modules/remote-server/remote-table/distant-table/utils/sort-distant-tables.util';
|
|
import { RemoteTableInput } from 'src/engine/metadata-modules/remote-server/remote-table/dtos/remote-table-input';
|
|
import {
|
|
DistantTableUpdate,
|
|
RemoteTableStatus,
|
|
} from 'src/engine/metadata-modules/remote-server/remote-table/dtos/remote-table.dto';
|
|
import { ForeignTableService } from 'src/engine/metadata-modules/remote-server/remote-table/foreign-table/foreign-table.service';
|
|
import { RemoteTableSchemaUpdateService } from 'src/engine/metadata-modules/remote-server/remote-table/remote-table-schema-update/remote-table-schema-update.service';
|
|
import { RemoteTableEntity } from 'src/engine/metadata-modules/remote-server/remote-table/remote-table.entity';
|
|
import {
|
|
RemoteTableException,
|
|
RemoteTableExceptionCode,
|
|
} from 'src/engine/metadata-modules/remote-server/remote-table/remote-table.exception';
|
|
import { fetchTableColumns } from 'src/engine/metadata-modules/remote-server/remote-table/utils/fetch-table-columns.util';
|
|
import { getRemoteTableLocalName } from 'src/engine/metadata-modules/remote-server/remote-table/utils/get-remote-table-local-name.util';
|
|
import {
|
|
mapUdtNameToFieldSettings,
|
|
mapUdtNameToFieldType,
|
|
} from 'src/engine/metadata-modules/remote-server/remote-table/utils/udt-name-mapper.util';
|
|
import { PostgresTableSchemaColumn } from 'src/engine/metadata-modules/remote-server/types/postgres-table-schema-column';
|
|
import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service';
|
|
import {
|
|
WorkspaceMigrationColumnAction,
|
|
WorkspaceMigrationColumnActionType,
|
|
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
|
|
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
|
|
import { camelCase } from 'src/utils/camel-case';
|
|
import { camelToTitleCase } from 'src/utils/camel-to-title-case';
|
|
|
|
export class RemoteTableService {
|
|
private readonly logger = new Logger(RemoteTableService.name);
|
|
|
|
constructor(
|
|
@InjectRepository(RemoteTableEntity, 'metadata')
|
|
private readonly remoteTableRepository: Repository<RemoteTableEntity>,
|
|
@InjectRepository(RemoteServerEntity, 'metadata')
|
|
private readonly remoteServerRepository: Repository<
|
|
RemoteServerEntity<RemoteServerType>
|
|
>,
|
|
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
|
|
private readonly dataSourceService: DataSourceService,
|
|
private readonly objectMetadataService: ObjectMetadataService,
|
|
private readonly fieldMetadataService: FieldMetadataService,
|
|
private readonly distantTableService: DistantTableService,
|
|
private readonly foreignTableService: ForeignTableService,
|
|
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
|
|
private readonly remoteTableSchemaUpdateService: RemoteTableSchemaUpdateService,
|
|
) {}
|
|
|
|
public async findDistantTablesWithStatus(
|
|
id: string,
|
|
workspaceId: string,
|
|
shouldFetchPendingSchemaUpdates?: boolean,
|
|
) {
|
|
const remoteServer = await this.remoteServerRepository.findOne({
|
|
where: {
|
|
id,
|
|
workspaceId,
|
|
},
|
|
});
|
|
|
|
if (!remoteServer) {
|
|
throw new RemoteTableException(
|
|
'Remote server does not exist',
|
|
RemoteTableExceptionCode.INVALID_REMOTE_TABLE_INPUT,
|
|
);
|
|
}
|
|
|
|
const currentRemoteTables = await this.findRemoteTablesByServerId({
|
|
remoteServerId: id,
|
|
workspaceId,
|
|
});
|
|
|
|
const currentRemoteTableDistantNames = currentRemoteTables.map(
|
|
(remoteTable) => remoteTable.distantTableName,
|
|
);
|
|
|
|
const distantTables = await this.distantTableService.fetchDistantTables(
|
|
remoteServer,
|
|
workspaceId,
|
|
);
|
|
|
|
const distantTablesWithStatus = Object.keys(distantTables).map(
|
|
(tableName) => ({
|
|
name: tableName,
|
|
schema: remoteServer.schema,
|
|
status: currentRemoteTableDistantNames.includes(tableName)
|
|
? RemoteTableStatus.SYNCED
|
|
: RemoteTableStatus.NOT_SYNCED,
|
|
}),
|
|
);
|
|
|
|
if (!shouldFetchPendingSchemaUpdates) {
|
|
return distantTablesWithStatus.sort(sortDistantTables);
|
|
}
|
|
|
|
const schemaPendingUpdates =
|
|
await this.remoteTableSchemaUpdateService.getSchemaUpdatesBetweenForeignAndDistantTables(
|
|
{
|
|
workspaceId,
|
|
remoteTables: currentRemoteTables,
|
|
distantTables,
|
|
},
|
|
);
|
|
|
|
const distantTablesWithPendingUpdates =
|
|
this.getDistantTablesWithPendingUpdates(
|
|
schemaPendingUpdates,
|
|
distantTablesWithStatus,
|
|
remoteServer.schema,
|
|
);
|
|
|
|
return distantTablesWithPendingUpdates.sort(sortDistantTables);
|
|
}
|
|
|
|
public async findRemoteTablesByServerId({
|
|
remoteServerId,
|
|
workspaceId,
|
|
}: {
|
|
remoteServerId: string;
|
|
workspaceId: string;
|
|
}) {
|
|
return this.remoteTableRepository.find({
|
|
where: {
|
|
remoteServerId,
|
|
workspaceId,
|
|
},
|
|
});
|
|
}
|
|
|
|
public async syncRemoteTable(input: RemoteTableInput, workspaceId: string) {
|
|
const remoteServer = await this.remoteServerRepository.findOne({
|
|
where: {
|
|
id: input.remoteServerId,
|
|
workspaceId,
|
|
},
|
|
});
|
|
|
|
if (!remoteServer) {
|
|
throw new RemoteTableException(
|
|
'Remote server does not exist',
|
|
RemoteTableExceptionCode.INVALID_REMOTE_TABLE_INPUT,
|
|
);
|
|
}
|
|
|
|
const currentRemoteTableWithSameDistantName =
|
|
await this.remoteTableRepository.findOne({
|
|
where: {
|
|
distantTableName: input.name,
|
|
remoteServerId: remoteServer.id,
|
|
workspaceId,
|
|
},
|
|
});
|
|
|
|
if (currentRemoteTableWithSameDistantName) {
|
|
throw new RemoteTableException(
|
|
'Remote server does not exist',
|
|
RemoteTableExceptionCode.REMOTE_TABLE_ALREADY_EXISTS,
|
|
);
|
|
}
|
|
|
|
const dataSourceMetatada =
|
|
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
|
|
workspaceId,
|
|
);
|
|
|
|
const mainDataSource =
|
|
await this.workspaceDataSourceService.connectToMainDataSource();
|
|
|
|
const { baseName: localTableBaseName, suffix: localTableSuffix } =
|
|
await getRemoteTableLocalName(
|
|
input.name,
|
|
dataSourceMetatada.schema,
|
|
mainDataSource,
|
|
);
|
|
|
|
const localTableName = localTableSuffix
|
|
? `${localTableBaseName}${localTableSuffix}`
|
|
: localTableBaseName;
|
|
|
|
const remoteTableEntity = this.remoteTableRepository.create({
|
|
distantTableName: input.name,
|
|
localTableName,
|
|
workspaceId,
|
|
remoteServerId: remoteServer.id,
|
|
});
|
|
|
|
const distantTableColumns =
|
|
await this.distantTableService.getDistantTableColumns(
|
|
remoteServer,
|
|
workspaceId,
|
|
input.name,
|
|
);
|
|
|
|
if (!distantTableColumns) {
|
|
throw new RemoteTableException(
|
|
'Remote server does not exist',
|
|
RemoteTableExceptionCode.REMOTE_TABLE_NOT_FOUND,
|
|
);
|
|
}
|
|
|
|
// We only support remote tables with an id column for now.
|
|
const distantTableIdColumn = distantTableColumns.find(
|
|
(column) => column.columnName === 'id',
|
|
);
|
|
|
|
if (!distantTableIdColumn) {
|
|
throw new RemoteTableException(
|
|
'Remote server does not exist',
|
|
RemoteTableExceptionCode.INVALID_REMOTE_TABLE_INPUT,
|
|
);
|
|
}
|
|
|
|
await this.foreignTableService.createForeignTable(
|
|
workspaceId,
|
|
localTableName,
|
|
remoteServer,
|
|
input.name,
|
|
distantTableColumns,
|
|
);
|
|
|
|
await this.createRemoteTableMetadata(
|
|
workspaceId,
|
|
localTableBaseName,
|
|
localTableSuffix,
|
|
distantTableColumns,
|
|
distantTableIdColumn,
|
|
dataSourceMetatada.id,
|
|
);
|
|
|
|
await this.remoteTableRepository.save(remoteTableEntity);
|
|
|
|
await this.workspaceMetadataVersionService.incrementMetadataVersion(
|
|
workspaceId,
|
|
);
|
|
|
|
return {
|
|
id: remoteTableEntity.id,
|
|
name: input.name,
|
|
schema: remoteServer.schema,
|
|
status: RemoteTableStatus.SYNCED,
|
|
};
|
|
}
|
|
|
|
public async unsyncRemoteTable(input: RemoteTableInput, workspaceId: string) {
|
|
const remoteServer = await this.remoteServerRepository.findOne({
|
|
where: {
|
|
id: input.remoteServerId,
|
|
workspaceId,
|
|
},
|
|
});
|
|
|
|
if (!remoteServer) {
|
|
throw new RemoteTableException(
|
|
'Remote server does not exist',
|
|
RemoteTableExceptionCode.INVALID_REMOTE_TABLE_INPUT,
|
|
);
|
|
}
|
|
|
|
const remoteTable = await this.remoteTableRepository.findOne({
|
|
where: {
|
|
distantTableName: input.name,
|
|
remoteServerId: remoteServer.id,
|
|
workspaceId,
|
|
},
|
|
});
|
|
|
|
if (!remoteTable) {
|
|
throw new RemoteTableException(
|
|
'Remote table does not exist',
|
|
RemoteTableExceptionCode.REMOTE_TABLE_NOT_FOUND,
|
|
);
|
|
}
|
|
|
|
await this.unsyncOne(workspaceId, remoteTable, remoteServer);
|
|
|
|
return {
|
|
name: input.name,
|
|
schema: remoteServer.schema,
|
|
status: RemoteTableStatus.NOT_SYNCED,
|
|
};
|
|
}
|
|
|
|
public async unsyncAll(
|
|
workspaceId: string,
|
|
remoteServer: RemoteServerEntity<RemoteServerType>,
|
|
) {
|
|
const remoteTables = await this.remoteTableRepository.find({
|
|
where: {
|
|
remoteServerId: remoteServer.id,
|
|
workspaceId,
|
|
},
|
|
});
|
|
|
|
for (const remoteTable of remoteTables) {
|
|
await this.unsyncOne(workspaceId, remoteTable, remoteServer);
|
|
}
|
|
}
|
|
|
|
public async syncRemoteTableSchemaChanges(
|
|
input: RemoteTableInput,
|
|
workspaceId: string,
|
|
) {
|
|
const remoteServer = await this.remoteServerRepository.findOne({
|
|
where: {
|
|
id: input.remoteServerId,
|
|
workspaceId,
|
|
},
|
|
});
|
|
|
|
if (!remoteServer) {
|
|
throw new RemoteTableException(
|
|
'Remote server does not exist',
|
|
RemoteTableExceptionCode.INVALID_REMOTE_TABLE_INPUT,
|
|
);
|
|
}
|
|
|
|
const remoteTable = await this.remoteTableRepository.findOne({
|
|
where: {
|
|
distantTableName: input.name,
|
|
remoteServerId: remoteServer.id,
|
|
workspaceId,
|
|
},
|
|
});
|
|
|
|
if (!remoteTable) {
|
|
throw new RemoteTableException(
|
|
'Remote table does not exist',
|
|
RemoteTableExceptionCode.REMOTE_TABLE_NOT_FOUND,
|
|
);
|
|
}
|
|
|
|
const distantTableColumns =
|
|
await this.distantTableService.getDistantTableColumns(
|
|
remoteServer,
|
|
workspaceId,
|
|
remoteTable.distantTableName,
|
|
);
|
|
|
|
if (!distantTableColumns) {
|
|
await this.unsyncOne(workspaceId, remoteTable, remoteServer);
|
|
|
|
return {
|
|
name: remoteTable.localTableName,
|
|
status: RemoteTableStatus.NOT_SYNCED,
|
|
schemaPendingUpdates: [],
|
|
};
|
|
}
|
|
|
|
const foreignTableColumns = await fetchTableColumns(
|
|
this.workspaceDataSourceService,
|
|
workspaceId,
|
|
remoteTable.localTableName,
|
|
);
|
|
|
|
const columnsUpdates =
|
|
this.remoteTableSchemaUpdateService.computeForeignTableColumnsUpdates(
|
|
foreignTableColumns,
|
|
distantTableColumns,
|
|
);
|
|
|
|
if (isEmpty(columnsUpdates)) {
|
|
this.logger.log(
|
|
`No update to perform on table "${remoteTable.localTableName}" for workspace ${workspaceId}`,
|
|
);
|
|
|
|
return {
|
|
name: remoteTable.localTableName,
|
|
status: RemoteTableStatus.SYNCED,
|
|
schemaPendingUpdates: [],
|
|
};
|
|
}
|
|
|
|
const updatedTable = await this.updateForeignTableAndFieldsMetadata(
|
|
remoteTable.localTableName,
|
|
workspaceId,
|
|
columnsUpdates,
|
|
);
|
|
|
|
return updatedTable;
|
|
}
|
|
|
|
private async unsyncOne(
|
|
workspaceId: string,
|
|
remoteTable: RemoteTableEntity,
|
|
remoteServer: RemoteServerEntity<RemoteServerType>,
|
|
) {
|
|
const currentForeignTableNames =
|
|
await this.foreignTableService.fetchForeignTableNamesWithinWorkspace(
|
|
workspaceId,
|
|
remoteServer.foreignDataWrapperId,
|
|
);
|
|
|
|
if (!currentForeignTableNames.includes(remoteTable.localTableName)) {
|
|
throw new RemoteTableException(
|
|
'Foreign table does not exist',
|
|
RemoteTableExceptionCode.NO_FOREIGN_TABLES_FOUND,
|
|
);
|
|
}
|
|
|
|
const objectMetadata =
|
|
await this.objectMetadataService.findOneWithinWorkspace(workspaceId, {
|
|
where: { nameSingular: remoteTable.localTableName },
|
|
});
|
|
|
|
if (objectMetadata) {
|
|
await this.objectMetadataService.deleteOneObject(
|
|
{ id: objectMetadata.id },
|
|
workspaceId,
|
|
);
|
|
}
|
|
|
|
await this.foreignTableService.deleteForeignTable(
|
|
remoteTable.localTableName,
|
|
workspaceId,
|
|
);
|
|
|
|
await this.remoteTableRepository.delete(remoteTable.id);
|
|
|
|
await this.workspaceMetadataVersionService.incrementMetadataVersion(
|
|
workspaceId,
|
|
);
|
|
}
|
|
|
|
private async createRemoteTableMetadata(
|
|
workspaceId: string,
|
|
localTableBaseName: string,
|
|
localTableSuffix: number | undefined,
|
|
distantTableColumns: PostgresTableSchemaColumn[],
|
|
distantTableIdColumn: PostgresTableSchemaColumn,
|
|
dataSourceMetadataId: string,
|
|
) {
|
|
const localTableNameSingular = localTableSuffix
|
|
? `${localTableBaseName}${localTableSuffix}`
|
|
: localTableBaseName;
|
|
|
|
const localTableNamePlural = localTableSuffix
|
|
? `${plural(localTableBaseName)}${localTableSuffix}`
|
|
: plural(localTableBaseName);
|
|
|
|
const objectMetadata = await this.objectMetadataService.createOne({
|
|
nameSingular: camelCase(localTableNameSingular),
|
|
namePlural: camelCase(localTableNamePlural),
|
|
labelSingular: camelToTitleCase(camelCase(localTableBaseName)),
|
|
labelPlural: camelToTitleCase(plural(camelCase(localTableBaseName))),
|
|
description: 'Remote table',
|
|
dataSourceId: dataSourceMetadataId,
|
|
workspaceId: workspaceId,
|
|
icon: 'IconPlug',
|
|
isRemote: true,
|
|
primaryKeyColumnType: distantTableIdColumn.udtName,
|
|
primaryKeyFieldMetadataSettings: mapUdtNameToFieldSettings(
|
|
distantTableIdColumn.udtName,
|
|
),
|
|
} satisfies CreateObjectInput);
|
|
|
|
for (const column of distantTableColumns) {
|
|
const columnName = camelCase(column.columnName);
|
|
|
|
// TODO: return error to the user when a column cannot be managed
|
|
try {
|
|
const field = await this.createFieldMetadataForForeignTableColumn(
|
|
workspaceId,
|
|
columnName,
|
|
column.udtName,
|
|
objectMetadata.id,
|
|
);
|
|
|
|
if (columnName === 'id') {
|
|
await this.objectMetadataService.updateOne(objectMetadata.id, {
|
|
labelIdentifierFieldMetadataId: field.id,
|
|
});
|
|
}
|
|
} catch (error) {
|
|
this.logger.error(
|
|
`Could not create field ${columnName} for remote table ${localTableNameSingular}: ${error}`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
private getDistantTablesWithPendingUpdates(
|
|
schemaPendingUpdates: { [tablename: string]: DistantTableUpdate[] },
|
|
distantTablesWithStatus: {
|
|
name: string;
|
|
schema: string;
|
|
status: RemoteTableStatus;
|
|
}[],
|
|
remoteServerSchema: string,
|
|
) {
|
|
const distantTablesWithUpdates = distantTablesWithStatus.map((table) => ({
|
|
...table,
|
|
schemaPendingUpdates: schemaPendingUpdates[table.name] || [],
|
|
}));
|
|
|
|
const deletedTables = Object.entries(schemaPendingUpdates)
|
|
.filter(([_tableName, updates]) =>
|
|
updates.includes(DistantTableUpdate.TABLE_DELETED),
|
|
)
|
|
.map(([tableName, updates]) => ({
|
|
name: tableName,
|
|
schema: remoteServerSchema,
|
|
status: RemoteTableStatus.SYNCED,
|
|
schemaPendingUpdates: updates,
|
|
}));
|
|
|
|
return [...distantTablesWithUpdates, ...deletedTables];
|
|
}
|
|
|
|
private async updateForeignTableAndFieldsMetadata(
|
|
foreignTableName: string,
|
|
workspaceId: string,
|
|
columnsUpdates: WorkspaceMigrationColumnAction[],
|
|
) {
|
|
const updatedForeignTable =
|
|
await this.foreignTableService.updateForeignTable(
|
|
foreignTableName,
|
|
workspaceId,
|
|
columnsUpdates,
|
|
);
|
|
|
|
const objectMetadata =
|
|
await this.objectMetadataService.findOneWithinWorkspace(workspaceId, {
|
|
where: { nameSingular: foreignTableName },
|
|
});
|
|
|
|
if (!objectMetadata) {
|
|
throw new RemoteTableException(
|
|
`Cannot find associated object for table ${foreignTableName}`,
|
|
RemoteTableExceptionCode.NO_OBJECT_METADATA_FOUND,
|
|
);
|
|
}
|
|
for (const columnUpdate of columnsUpdates) {
|
|
this.updateFieldMetadataFromColumnUpdate(
|
|
columnUpdate,
|
|
workspaceId,
|
|
objectMetadata.id,
|
|
);
|
|
}
|
|
|
|
return updatedForeignTable;
|
|
}
|
|
|
|
private async updateFieldMetadataFromColumnUpdate(
|
|
columnUpdate: WorkspaceMigrationColumnAction,
|
|
workspaceId: string,
|
|
objectMetadataId: string,
|
|
) {
|
|
if (columnUpdate.action === WorkspaceMigrationColumnActionType.CREATE) {
|
|
await this.createFieldMetadataForForeignTableColumn(
|
|
workspaceId,
|
|
columnUpdate.columnName,
|
|
columnUpdate.columnType,
|
|
objectMetadataId,
|
|
);
|
|
}
|
|
if (columnUpdate.action === WorkspaceMigrationColumnActionType.DROP) {
|
|
const columnName = columnUpdate.columnName;
|
|
|
|
const fieldMetadataToDelete =
|
|
await this.fieldMetadataService.findOneWithinWorkspace(workspaceId, {
|
|
where: {
|
|
objectMetadataId: objectMetadataId,
|
|
name: columnName,
|
|
},
|
|
});
|
|
|
|
if (!fieldMetadataToDelete) {
|
|
throw new RemoteTableException(
|
|
`Cannot find associated field metadata for column ${columnName}`,
|
|
RemoteTableExceptionCode.NO_FIELD_METADATA_FOUND,
|
|
);
|
|
}
|
|
|
|
await this.fieldMetadataService.deleteOne(fieldMetadataToDelete.id);
|
|
}
|
|
}
|
|
|
|
private async createFieldMetadataForForeignTableColumn(
|
|
workspaceId: string,
|
|
columnName: string,
|
|
columnType: string,
|
|
objectMetadataId: string,
|
|
): Promise<FieldMetadataEntity> {
|
|
return this.fieldMetadataService.createOne({
|
|
name: columnName,
|
|
label: camelToTitleCase(columnName),
|
|
description: 'Field of remote',
|
|
type: mapUdtNameToFieldType(columnType),
|
|
workspaceId: workspaceId,
|
|
objectMetadataId: objectMetadataId,
|
|
isRemoteCreation: true,
|
|
isNullable: true,
|
|
icon: 'IconPlug',
|
|
settings: mapUdtNameToFieldSettings(columnType),
|
|
} satisfies CreateFieldInput);
|
|
}
|
|
}
|