Handle relations separately for remotes (#5538)

Remote object id columns are not removed anymore when a remote object is
unsynced.
This is because we do not use relations anymore. We only created the id
field. So the current behavior that was implemented for custom objects,
to retrieve the fields to deleted, does not work.

Since remote object relations are really different, I extracted the
logic from `objectMetadataService`. It now handles only the relations
for custom objects creation and deletion (this part should be extracted
as well).

I create a new remote table relation service that will:
- fetch objects metadata linked to remotes (favorites,
activityTargets...)
- look for columns based on remote object name
- delete the fields and columns
This commit is contained in:
Thomas Trompette
2024-05-23 14:59:34 +02:00
committed by GitHub
parent 8019ba8782
commit 0d6fe7b2b4
10 changed files with 762 additions and 624 deletions

View File

@ -20,6 +20,7 @@ import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { WorkspaceCacheVersionModule } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.module'; import { WorkspaceCacheVersionModule } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.module';
import { BeforeUpdateOneObject } from 'src/engine/metadata-modules/object-metadata/hooks/before-update-one-object.hook'; import { BeforeUpdateOneObject } from 'src/engine/metadata-modules/object-metadata/hooks/before-update-one-object.hook';
import { RemoteTableRelationsModule } from 'src/engine/metadata-modules/remote-server/remote-table/remote-table-relations/remote-table-relations.module';
import { ObjectMetadataService } from './object-metadata.service'; import { ObjectMetadataService } from './object-metadata.service';
import { ObjectMetadataEntity } from './object-metadata.entity'; import { ObjectMetadataEntity } from './object-metadata.entity';
@ -43,6 +44,7 @@ import { ObjectMetadataDTO } from './dtos/object-metadata.dto';
WorkspaceMigrationRunnerModule, WorkspaceMigrationRunnerModule,
WorkspaceCacheVersionModule, WorkspaceCacheVersionModule,
FeatureFlagModule, FeatureFlagModule,
RemoteTableRelationsModule,
], ],
services: [ObjectMetadataService], services: [ObjectMetadataService],
resolvers: [ resolvers: [

View File

@ -49,14 +49,13 @@ import {
createForeignKeyDeterministicUuid, createForeignKeyDeterministicUuid,
createRelationDeterministicUuid, createRelationDeterministicUuid,
} from 'src/engine/workspace-manager/workspace-sync-metadata/utils/create-deterministic-uuid.util'; } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/create-deterministic-uuid.util';
import { createWorkspaceMigrationsForCustomObjectRelations } from 'src/engine/metadata-modules/object-metadata/utils/create-migrations-for-custom-object-relations.util'; import { buildMigrationsForCustomObjectRelations } from 'src/engine/metadata-modules/object-metadata/utils/build-migrations-for-custom-object-relations.util';
import { createWorkspaceMigrationsForRemoteObjectRelations } from 'src/engine/metadata-modules/object-metadata/utils/create-workspace-migrations-for-remote-object-relations.util';
import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
import { validateObjectMetadataInputOrThrow } from 'src/engine/metadata-modules/object-metadata/utils/validate-object-metadata-input.util'; import { validateObjectMetadataInputOrThrow } from 'src/engine/metadata-modules/object-metadata/utils/validate-object-metadata-input.util';
import { mapUdtNameToFieldType } from 'src/engine/metadata-modules/remote-server/remote-table/utils/udt-name-mapper.util'; import { mapUdtNameToFieldType } from 'src/engine/metadata-modules/remote-server/remote-table/utils/udt-name-mapper.util';
import { WorkspaceCacheVersionService } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.service'; import { WorkspaceCacheVersionService } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.service';
import { UpdateOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input'; import { UpdateOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input';
import { createMigrationToAlterCommentOnForeignKeyDeletion } from 'src/engine/metadata-modules/object-metadata/utils/create-migration-for-foreign-key-comment-alteration.util'; import { RemoteTableRelationsService } from 'src/engine/metadata-modules/remote-server/remote-table/remote-table-relations/remote-table-relations.service';
import { ObjectMetadataEntity } from './object-metadata.entity'; import { ObjectMetadataEntity } from './object-metadata.entity';
@ -74,6 +73,8 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
@InjectRepository(RelationMetadataEntity, 'metadata') @InjectRepository(RelationMetadataEntity, 'metadata')
private readonly relationMetadataRepository: Repository<RelationMetadataEntity>, private readonly relationMetadataRepository: Repository<RelationMetadataEntity>,
private readonly remoteTableRelationsService: RemoteTableRelationsService,
private readonly dataSourceService: DataSourceService, private readonly dataSourceService: DataSourceService,
private readonly typeORMService: TypeORMService, private readonly typeORMService: TypeORMService,
private readonly workspaceMigrationService: WorkspaceMigrationService, private readonly workspaceMigrationService: WorkspaceMigrationService,
@ -123,113 +124,18 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
throw new NotFoundException('Object does not exist'); throw new NotFoundException('Object does not exist');
} }
const relationsToDelete: RelationToDelete[] = []; // DELETE RELATIONS
if (objectMetadata.isRemote) {
// TODO: Most of this logic should be moved to relation-metadata.service.ts await this.remoteTableRelationsService.deleteForeignKeysMetadataAndCreateMigrations(
for (const relation of [ objectMetadata.workspaceId,
...objectMetadata.fromRelations, objectMetadata,
...objectMetadata.toRelations,
]) {
relationsToDelete.push({
id: relation.id,
fromFieldMetadataId: relation.fromFieldMetadata.id,
toFieldMetadataId: relation.toFieldMetadata.id,
fromFieldMetadataName: relation.fromFieldMetadata.name,
toFieldMetadataName: relation.toFieldMetadata.name,
fromObjectMetadataId: relation.fromObjectMetadata.id,
toObjectMetadataId: relation.toObjectMetadata.id,
fromObjectName: relation.fromObjectMetadata.nameSingular,
toObjectName: relation.toObjectMetadata.nameSingular,
toFieldMetadataIsCustom: relation.toFieldMetadata.isCustom,
toObjectMetadataIsCustom: relation.toObjectMetadata.isCustom,
direction:
relation.fromObjectMetadata.nameSingular ===
objectMetadata.nameSingular
? 'from'
: 'to',
});
}
if (relationsToDelete.length > 0) {
await this.relationMetadataRepository.delete(
relationsToDelete.map((relation) => relation.id),
); );
} } else {
await this.deleteAllRelationsAndDropTable(objectMetadata, workspaceId);
for (const relationToDelete of relationsToDelete) {
const foreignKeyFieldsToDelete = await this.fieldMetadataRepository.find({
where: {
name: `${relationToDelete.toFieldMetadataName}Id`,
objectMetadataId: relationToDelete.toObjectMetadataId,
workspaceId,
},
});
const foreignKeyFieldsToDeleteIds = foreignKeyFieldsToDelete.map(
(field) => field.id,
);
await this.fieldMetadataRepository.delete([
...foreignKeyFieldsToDeleteIds,
relationToDelete.fromFieldMetadataId,
relationToDelete.toFieldMetadataId,
]);
if (relationToDelete.direction === 'from') {
await this.workspaceMigrationService.createCustomMigration(
generateMigrationName(
`delete-${relationToDelete.fromObjectName}-${relationToDelete.toObjectName}`,
),
workspaceId,
[
{
name: computeTableName(
relationToDelete.toObjectName,
relationToDelete.toObjectMetadataIsCustom,
),
action: WorkspaceMigrationTableActionType.ALTER,
columns: [
{
action: WorkspaceMigrationColumnActionType.DROP,
columnName: computeColumnName(
relationToDelete.toFieldMetadataName,
{ isForeignKey: true },
),
} satisfies WorkspaceMigrationColumnDrop,
],
},
],
);
// for remote objects, we need to update the comment of the foreign key column
if (objectMetadata.isRemote) {
await createMigrationToAlterCommentOnForeignKeyDeletion(
this.dataSourceService,
this.typeORMService,
this.workspaceMigrationService,
workspaceId,
relationToDelete,
);
}
}
} }
await this.objectMetadataRepository.delete(objectMetadata.id); await this.objectMetadataRepository.delete(objectMetadata.id);
if (!objectMetadata.isRemote) {
// DROP TABLE
await this.workspaceMigrationService.createCustomMigration(
generateMigrationName(`delete-${objectMetadata.nameSingular}`),
workspaceId,
[
{
name: computeObjectTargetTable(objectMetadata),
action: WorkspaceMigrationTableActionType.DROP,
},
],
);
}
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations( await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
workspaceId, workspaceId,
); );
@ -371,10 +277,19 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
const workspaceDataSource = const workspaceDataSource =
await this.typeORMService.connectToDataSource(dataSourceMetadata); await this.typeORMService.connectToDataSource(dataSourceMetadata);
await this.createObjectRelationsMetadataAndMigrations( if (isCustom) {
objectMetadataInput, await this.createObjectRelationsMetadataAndMigrations(
createdObjectMetadata, objectMetadataInput,
); createdObjectMetadata,
);
} else {
await this.remoteTableRelationsService.createForeignKeysMetadataAndMigrations(
objectMetadataInput.workspaceId,
createdObjectMetadata,
objectMetadataInput.primaryKeyFieldMetadataSettings,
objectMetadataInput.primaryKeyColumnType,
);
}
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations( await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
createdObjectMetadata.workspaceId, createdObjectMetadata.workspaceId,
@ -507,8 +422,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
objectMetadataInput: CreateObjectInput, objectMetadataInput: CreateObjectInput,
createdObjectMetadata: ObjectMetadataEntity, createdObjectMetadata: ObjectMetadataEntity,
) { ) {
const isRemoteObject = objectMetadataInput.isRemote ?? false;
const { timelineActivityObjectMetadata } = const { timelineActivityObjectMetadata } =
await this.createTimelineActivityRelation( await this.createTimelineActivityRelation(
objectMetadataInput.workspaceId, objectMetadataInput.workspaceId,
@ -517,7 +430,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
objectMetadataInput.primaryKeyColumnType ?? 'uuid', objectMetadataInput.primaryKeyColumnType ?? 'uuid',
), ),
objectMetadataInput.primaryKeyFieldMetadataSettings, objectMetadataInput.primaryKeyFieldMetadataSettings,
isRemoteObject,
); );
const { activityTargetObjectMetadata } = const { activityTargetObjectMetadata } =
@ -528,7 +440,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
objectMetadataInput.primaryKeyColumnType ?? 'uuid', objectMetadataInput.primaryKeyColumnType ?? 'uuid',
), ),
objectMetadataInput.primaryKeyFieldMetadataSettings, objectMetadataInput.primaryKeyFieldMetadataSettings,
isRemoteObject,
); );
const { favoriteObjectMetadata } = await this.createFavoriteRelation( const { favoriteObjectMetadata } = await this.createFavoriteRelation(
@ -536,7 +447,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
createdObjectMetadata, createdObjectMetadata,
mapUdtNameToFieldType(objectMetadataInput.primaryKeyColumnType ?? 'uuid'), mapUdtNameToFieldType(objectMetadataInput.primaryKeyColumnType ?? 'uuid'),
objectMetadataInput.primaryKeyFieldMetadataSettings, objectMetadataInput.primaryKeyFieldMetadataSettings,
isRemoteObject,
); );
const { attachmentObjectMetadata } = await this.createAttachmentRelation( const { attachmentObjectMetadata } = await this.createAttachmentRelation(
@ -544,28 +454,18 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
createdObjectMetadata, createdObjectMetadata,
mapUdtNameToFieldType(objectMetadataInput.primaryKeyColumnType ?? 'uuid'), mapUdtNameToFieldType(objectMetadataInput.primaryKeyColumnType ?? 'uuid'),
objectMetadataInput.primaryKeyFieldMetadataSettings, objectMetadataInput.primaryKeyFieldMetadataSettings,
isRemoteObject,
); );
return this.workspaceMigrationService.createCustomMigration( return this.workspaceMigrationService.createCustomMigration(
generateMigrationName(`create-${createdObjectMetadata.nameSingular}`), generateMigrationName(`create-${createdObjectMetadata.nameSingular}`),
createdObjectMetadata.workspaceId, createdObjectMetadata.workspaceId,
isRemoteObject buildMigrationsForCustomObjectRelations(
? await createWorkspaceMigrationsForRemoteObjectRelations( createdObjectMetadata,
createdObjectMetadata, activityTargetObjectMetadata,
activityTargetObjectMetadata, attachmentObjectMetadata,
attachmentObjectMetadata, timelineActivityObjectMetadata,
timelineActivityObjectMetadata, favoriteObjectMetadata,
favoriteObjectMetadata, ),
objectMetadataInput.primaryKeyColumnType ?? 'uuid',
)
: createWorkspaceMigrationsForCustomObjectRelations(
createdObjectMetadata,
activityTargetObjectMetadata,
attachmentObjectMetadata,
timelineActivityObjectMetadata,
favoriteObjectMetadata,
),
); );
} }
@ -576,7 +476,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
objectPrimaryKeyFieldSettings: objectPrimaryKeyFieldSettings:
| FieldMetadataSettings<FieldMetadataType | 'default'> | FieldMetadataSettings<FieldMetadataType | 'default'>
| undefined, | undefined,
isRemoteObject: boolean,
) { ) {
const activityTargetObjectMetadata = const activityTargetObjectMetadata =
await this.objectMetadataRepository.findOneByOrFail({ await this.objectMetadataRepository.findOneByOrFail({
@ -607,70 +506,68 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
}, },
); );
if (!isRemoteObject) { const activityTargetRelationFieldMetadata =
const activityTargetRelationFieldMetadata = await this.fieldMetadataRepository.save([
await this.fieldMetadataRepository.save([ // FROM
// FROM
{
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.activityTargets,
objectMetadataId: createdObjectMetadata.id,
workspaceId: workspaceId,
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'activityTargets',
label: 'Activities',
description: `Activities tied to the ${createdObjectMetadata.labelSingular}`,
icon: 'IconCheckbox',
isNullable: true,
},
// TO
{
standardId: createRelationDeterministicUuid({
objectId: createdObjectMetadata.id,
standardId: ACTIVITY_TARGET_STANDARD_FIELD_IDS.custom,
}),
objectMetadataId: activityTargetObjectMetadata.id,
workspaceId: workspaceId,
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: createdObjectMetadata.nameSingular,
label: createdObjectMetadata.labelSingular,
description: `ActivityTarget ${createdObjectMetadata.labelSingular}`,
icon: 'IconBuildingSkyscraper',
isNullable: true,
},
]);
const activityTargetRelationFieldMetadataMap =
activityTargetRelationFieldMetadata.reduce(
(acc, fieldMetadata: FieldMetadataEntity) => {
if (fieldMetadata.type === FieldMetadataType.RELATION) {
acc[fieldMetadata.objectMetadataId] = fieldMetadata;
}
return acc;
},
{},
);
await this.relationMetadataRepository.save([
{ {
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.activityTargets,
objectMetadataId: createdObjectMetadata.id,
workspaceId: workspaceId, workspaceId: workspaceId,
relationType: RelationMetadataType.ONE_TO_MANY, isCustom: false,
fromObjectMetadataId: createdObjectMetadata.id, isActive: true,
toObjectMetadataId: activityTargetObjectMetadata.id, type: FieldMetadataType.RELATION,
fromFieldMetadataId: name: 'activityTargets',
activityTargetRelationFieldMetadataMap[createdObjectMetadata.id].id, label: 'Activities',
toFieldMetadataId: description: `Activities tied to the ${createdObjectMetadata.labelSingular}`,
activityTargetRelationFieldMetadataMap[ icon: 'IconCheckbox',
activityTargetObjectMetadata.id isNullable: true,
].id, },
onDeleteAction: RelationOnDeleteAction.CASCADE, // TO
{
standardId: createRelationDeterministicUuid({
objectId: createdObjectMetadata.id,
standardId: ACTIVITY_TARGET_STANDARD_FIELD_IDS.custom,
}),
objectMetadataId: activityTargetObjectMetadata.id,
workspaceId: workspaceId,
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: createdObjectMetadata.nameSingular,
label: createdObjectMetadata.labelSingular,
description: `ActivityTarget ${createdObjectMetadata.labelSingular}`,
icon: 'IconBuildingSkyscraper',
isNullable: true,
}, },
]); ]);
}
const activityTargetRelationFieldMetadataMap =
activityTargetRelationFieldMetadata.reduce(
(acc, fieldMetadata: FieldMetadataEntity) => {
if (fieldMetadata.type === FieldMetadataType.RELATION) {
acc[fieldMetadata.objectMetadataId] = fieldMetadata;
}
return acc;
},
{},
);
await this.relationMetadataRepository.save([
{
workspaceId: workspaceId,
relationType: RelationMetadataType.ONE_TO_MANY,
fromObjectMetadataId: createdObjectMetadata.id,
toObjectMetadataId: activityTargetObjectMetadata.id,
fromFieldMetadataId:
activityTargetRelationFieldMetadataMap[createdObjectMetadata.id].id,
toFieldMetadataId:
activityTargetRelationFieldMetadataMap[
activityTargetObjectMetadata.id
].id,
onDeleteAction: RelationOnDeleteAction.CASCADE,
},
]);
return { activityTargetObjectMetadata }; return { activityTargetObjectMetadata };
} }
@ -682,7 +579,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
objectPrimaryKeyFieldSettings: objectPrimaryKeyFieldSettings:
| FieldMetadataSettings<FieldMetadataType | 'default'> | FieldMetadataSettings<FieldMetadataType | 'default'>
| undefined, | undefined,
isRemoteObject: boolean,
) { ) {
const attachmentObjectMetadata = const attachmentObjectMetadata =
await this.objectMetadataRepository.findOneByOrFail({ await this.objectMetadataRepository.findOneByOrFail({
@ -713,68 +609,66 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
}, },
); );
if (!isRemoteObject) { const attachmentRelationFieldMetadata =
const attachmentRelationFieldMetadata = await this.fieldMetadataRepository.save([
await this.fieldMetadataRepository.save([ // FROM
// FROM
{
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.attachments,
objectMetadataId: createdObjectMetadata.id,
workspaceId: workspaceId,
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'attachments',
label: 'Attachments',
description: `Attachments tied to the ${createdObjectMetadata.labelSingular}`,
icon: 'IconFileImport',
isNullable: true,
},
// TO
{
standardId: createRelationDeterministicUuid({
objectId: createdObjectMetadata.id,
standardId: ATTACHMENT_STANDARD_FIELD_IDS.custom,
}),
objectMetadataId: attachmentObjectMetadata.id,
workspaceId: workspaceId,
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: createdObjectMetadata.nameSingular,
label: createdObjectMetadata.labelSingular,
description: `Attachment ${createdObjectMetadata.labelSingular}`,
icon: 'IconBuildingSkyscraper',
isNullable: true,
},
]);
const attachmentRelationFieldMetadataMap =
attachmentRelationFieldMetadata.reduce(
(acc, fieldMetadata: FieldMetadataEntity) => {
if (fieldMetadata.type === FieldMetadataType.RELATION) {
acc[fieldMetadata.objectMetadataId] = fieldMetadata;
}
return acc;
},
{},
);
await this.relationMetadataRepository.save([
{ {
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.attachments,
objectMetadataId: createdObjectMetadata.id,
workspaceId: workspaceId, workspaceId: workspaceId,
relationType: RelationMetadataType.ONE_TO_MANY, isCustom: false,
fromObjectMetadataId: createdObjectMetadata.id, isActive: true,
toObjectMetadataId: attachmentObjectMetadata.id, type: FieldMetadataType.RELATION,
fromFieldMetadataId: name: 'attachments',
attachmentRelationFieldMetadataMap[createdObjectMetadata.id].id, label: 'Attachments',
toFieldMetadataId: description: `Attachments tied to the ${createdObjectMetadata.labelSingular}`,
attachmentRelationFieldMetadataMap[attachmentObjectMetadata.id].id, icon: 'IconFileImport',
onDeleteAction: RelationOnDeleteAction.CASCADE, isNullable: true,
},
// TO
{
standardId: createRelationDeterministicUuid({
objectId: createdObjectMetadata.id,
standardId: ATTACHMENT_STANDARD_FIELD_IDS.custom,
}),
objectMetadataId: attachmentObjectMetadata.id,
workspaceId: workspaceId,
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: createdObjectMetadata.nameSingular,
label: createdObjectMetadata.labelSingular,
description: `Attachment ${createdObjectMetadata.labelSingular}`,
icon: 'IconBuildingSkyscraper',
isNullable: true,
}, },
]); ]);
}
const attachmentRelationFieldMetadataMap =
attachmentRelationFieldMetadata.reduce(
(acc, fieldMetadata: FieldMetadataEntity) => {
if (fieldMetadata.type === FieldMetadataType.RELATION) {
acc[fieldMetadata.objectMetadataId] = fieldMetadata;
}
return acc;
},
{},
);
await this.relationMetadataRepository.save([
{
workspaceId: workspaceId,
relationType: RelationMetadataType.ONE_TO_MANY,
fromObjectMetadataId: createdObjectMetadata.id,
toObjectMetadataId: attachmentObjectMetadata.id,
fromFieldMetadataId:
attachmentRelationFieldMetadataMap[createdObjectMetadata.id].id,
toFieldMetadataId:
attachmentRelationFieldMetadataMap[attachmentObjectMetadata.id].id,
onDeleteAction: RelationOnDeleteAction.CASCADE,
},
]);
return { attachmentObjectMetadata }; return { attachmentObjectMetadata };
} }
@ -786,7 +680,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
objectPrimaryKeyFieldSettings: objectPrimaryKeyFieldSettings:
| FieldMetadataSettings<FieldMetadataType | 'default'> | FieldMetadataSettings<FieldMetadataType | 'default'>
| undefined, | undefined,
isRemoteObject: boolean,
) { ) {
const timelineActivityObjectMetadata = const timelineActivityObjectMetadata =
await this.objectMetadataRepository.findOneByOrFail({ await this.objectMetadataRepository.findOneByOrFail({
@ -817,71 +710,68 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
}, },
); );
if (!isRemoteObject) { const timelineActivityRelationFieldMetadata =
const timelineActivityRelationFieldMetadata = await this.fieldMetadataRepository.save([
await this.fieldMetadataRepository.save([ // FROM
// FROM
{
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.timelineActivities,
objectMetadataId: createdObjectMetadata.id,
workspaceId: workspaceId,
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'timelineActivities',
label: 'Timeline Activities',
description: `Timeline Activities tied to the ${createdObjectMetadata.labelSingular}`,
icon: 'IconTimeline',
isNullable: true,
},
// TO
{
standardId: createRelationDeterministicUuid({
objectId: createdObjectMetadata.id,
standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.custom,
}),
objectMetadataId: timelineActivityObjectMetadata.id,
workspaceId: workspaceId,
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: createdObjectMetadata.nameSingular,
label: createdObjectMetadata.labelSingular,
description: `Timeline Activity ${createdObjectMetadata.labelSingular}`,
icon: 'IconBuildingSkyscraper',
isNullable: true,
},
]);
const timelineActivityRelationFieldMetadataMap =
timelineActivityRelationFieldMetadata.reduce(
(acc, fieldMetadata: FieldMetadataEntity) => {
if (fieldMetadata.type === FieldMetadataType.RELATION) {
acc[fieldMetadata.objectMetadataId] = fieldMetadata;
}
return acc;
},
{},
);
await this.relationMetadataRepository.save([
{ {
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.timelineActivities,
objectMetadataId: createdObjectMetadata.id,
workspaceId: workspaceId, workspaceId: workspaceId,
relationType: RelationMetadataType.ONE_TO_MANY, isCustom: false,
fromObjectMetadataId: createdObjectMetadata.id, isActive: true,
toObjectMetadataId: timelineActivityObjectMetadata.id, type: FieldMetadataType.RELATION,
fromFieldMetadataId: name: 'timelineActivities',
timelineActivityRelationFieldMetadataMap[createdObjectMetadata.id] label: 'Timeline Activities',
.id, description: `Timeline Activities tied to the ${createdObjectMetadata.labelSingular}`,
toFieldMetadataId: icon: 'IconTimeline',
timelineActivityRelationFieldMetadataMap[ isNullable: true,
timelineActivityObjectMetadata.id },
].id, // TO
onDeleteAction: RelationOnDeleteAction.CASCADE, {
standardId: createRelationDeterministicUuid({
objectId: createdObjectMetadata.id,
standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.custom,
}),
objectMetadataId: timelineActivityObjectMetadata.id,
workspaceId: workspaceId,
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: createdObjectMetadata.nameSingular,
label: createdObjectMetadata.labelSingular,
description: `Timeline Activity ${createdObjectMetadata.labelSingular}`,
icon: 'IconBuildingSkyscraper',
isNullable: true,
}, },
]); ]);
}
const timelineActivityRelationFieldMetadataMap =
timelineActivityRelationFieldMetadata.reduce(
(acc, fieldMetadata: FieldMetadataEntity) => {
if (fieldMetadata.type === FieldMetadataType.RELATION) {
acc[fieldMetadata.objectMetadataId] = fieldMetadata;
}
return acc;
},
{},
);
await this.relationMetadataRepository.save([
{
workspaceId: workspaceId,
relationType: RelationMetadataType.ONE_TO_MANY,
fromObjectMetadataId: createdObjectMetadata.id,
toObjectMetadataId: timelineActivityObjectMetadata.id,
fromFieldMetadataId:
timelineActivityRelationFieldMetadataMap[createdObjectMetadata.id].id,
toFieldMetadataId:
timelineActivityRelationFieldMetadataMap[
timelineActivityObjectMetadata.id
].id,
onDeleteAction: RelationOnDeleteAction.CASCADE,
},
]);
return { timelineActivityObjectMetadata }; return { timelineActivityObjectMetadata };
} }
@ -893,7 +783,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
objectPrimaryKeyFieldSettings: objectPrimaryKeyFieldSettings:
| FieldMetadataSettings<FieldMetadataType | 'default'> | FieldMetadataSettings<FieldMetadataType | 'default'>
| undefined, | undefined,
isRemoteObject: boolean,
) { ) {
const favoriteObjectMetadata = const favoriteObjectMetadata =
await this.objectMetadataRepository.findOneByOrFail({ await this.objectMetadataRepository.findOneByOrFail({
@ -924,70 +813,165 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
}, },
); );
if (!isRemoteObject) { const favoriteRelationFieldMetadata =
const favoriteRelationFieldMetadata = await this.fieldMetadataRepository.save([
await this.fieldMetadataRepository.save([ // FROM
// FROM
{
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.favorites,
objectMetadataId: createdObjectMetadata.id,
workspaceId: workspaceId,
isCustom: false,
isActive: true,
isSystem: true,
type: FieldMetadataType.RELATION,
name: 'favorites',
label: 'Favorites',
description: `Favorites tied to the ${createdObjectMetadata.labelSingular}`,
icon: 'IconHeart',
isNullable: true,
},
// TO
{
standardId: createRelationDeterministicUuid({
objectId: createdObjectMetadata.id,
standardId: FAVORITE_STANDARD_FIELD_IDS.custom,
}),
objectMetadataId: favoriteObjectMetadata.id,
workspaceId: workspaceId,
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: createdObjectMetadata.nameSingular,
label: createdObjectMetadata.labelSingular,
description: `Favorite ${createdObjectMetadata.labelSingular}`,
icon: 'IconBuildingSkyscraper',
isNullable: true,
},
]);
const favoriteRelationFieldMetadataMap =
favoriteRelationFieldMetadata.reduce(
(acc, fieldMetadata: FieldMetadataEntity) => {
if (fieldMetadata.type === FieldMetadataType.RELATION) {
acc[fieldMetadata.objectMetadataId] = fieldMetadata;
}
return acc;
},
{},
);
await this.relationMetadataRepository.save([
{ {
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.favorites,
objectMetadataId: createdObjectMetadata.id,
workspaceId: workspaceId, workspaceId: workspaceId,
relationType: RelationMetadataType.ONE_TO_MANY, isCustom: false,
fromObjectMetadataId: createdObjectMetadata.id, isActive: true,
toObjectMetadataId: favoriteObjectMetadata.id, isSystem: true,
fromFieldMetadataId: type: FieldMetadataType.RELATION,
favoriteRelationFieldMetadataMap[createdObjectMetadata.id].id, name: 'favorites',
toFieldMetadataId: label: 'Favorites',
favoriteRelationFieldMetadataMap[favoriteObjectMetadata.id].id, description: `Favorites tied to the ${createdObjectMetadata.labelSingular}`,
onDeleteAction: RelationOnDeleteAction.CASCADE, icon: 'IconHeart',
isNullable: true,
},
// TO
{
standardId: createRelationDeterministicUuid({
objectId: createdObjectMetadata.id,
standardId: FAVORITE_STANDARD_FIELD_IDS.custom,
}),
objectMetadataId: favoriteObjectMetadata.id,
workspaceId: workspaceId,
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: createdObjectMetadata.nameSingular,
label: createdObjectMetadata.labelSingular,
description: `Favorite ${createdObjectMetadata.labelSingular}`,
icon: 'IconBuildingSkyscraper',
isNullable: true,
}, },
]); ]);
}
const favoriteRelationFieldMetadataMap =
favoriteRelationFieldMetadata.reduce(
(acc, fieldMetadata: FieldMetadataEntity) => {
if (fieldMetadata.type === FieldMetadataType.RELATION) {
acc[fieldMetadata.objectMetadataId] = fieldMetadata;
}
return acc;
},
{},
);
await this.relationMetadataRepository.save([
{
workspaceId: workspaceId,
relationType: RelationMetadataType.ONE_TO_MANY,
fromObjectMetadataId: createdObjectMetadata.id,
toObjectMetadataId: favoriteObjectMetadata.id,
fromFieldMetadataId:
favoriteRelationFieldMetadataMap[createdObjectMetadata.id].id,
toFieldMetadataId:
favoriteRelationFieldMetadataMap[favoriteObjectMetadata.id].id,
onDeleteAction: RelationOnDeleteAction.CASCADE,
},
]);
return { favoriteObjectMetadata }; return { favoriteObjectMetadata };
} }
private async deleteAllRelationsAndDropTable(
objectMetadata: ObjectMetadataEntity,
workspaceId: string,
) {
const relationsToDelete: RelationToDelete[] = [];
// TODO: Most of this logic should be moved to relation-metadata.service.ts
for (const relation of [
...objectMetadata.fromRelations,
...objectMetadata.toRelations,
]) {
relationsToDelete.push({
id: relation.id,
fromFieldMetadataId: relation.fromFieldMetadata.id,
toFieldMetadataId: relation.toFieldMetadata.id,
fromFieldMetadataName: relation.fromFieldMetadata.name,
toFieldMetadataName: relation.toFieldMetadata.name,
fromObjectMetadataId: relation.fromObjectMetadata.id,
toObjectMetadataId: relation.toObjectMetadata.id,
fromObjectName: relation.fromObjectMetadata.nameSingular,
toObjectName: relation.toObjectMetadata.nameSingular,
toFieldMetadataIsCustom: relation.toFieldMetadata.isCustom,
toObjectMetadataIsCustom: relation.toObjectMetadata.isCustom,
direction:
relation.fromObjectMetadata.nameSingular ===
objectMetadata.nameSingular
? 'from'
: 'to',
});
}
if (relationsToDelete.length > 0) {
await this.relationMetadataRepository.delete(
relationsToDelete.map((relation) => relation.id),
);
}
for (const relationToDelete of relationsToDelete) {
const foreignKeyFieldsToDelete = await this.fieldMetadataRepository.find({
where: {
name: `${relationToDelete.toFieldMetadataName}Id`,
objectMetadataId: relationToDelete.toObjectMetadataId,
workspaceId,
},
});
const foreignKeyFieldsToDeleteIds = foreignKeyFieldsToDelete.map(
(field) => field.id,
);
await this.fieldMetadataRepository.delete([
...foreignKeyFieldsToDeleteIds,
relationToDelete.fromFieldMetadataId,
relationToDelete.toFieldMetadataId,
]);
if (relationToDelete.direction === 'from') {
await this.workspaceMigrationService.createCustomMigration(
generateMigrationName(
`delete-${relationToDelete.fromObjectName}-${relationToDelete.toObjectName}`,
),
workspaceId,
[
{
name: computeTableName(
relationToDelete.toObjectName,
relationToDelete.toObjectMetadataIsCustom,
),
action: WorkspaceMigrationTableActionType.ALTER,
columns: [
{
action: WorkspaceMigrationColumnActionType.DROP,
columnName: computeColumnName(
relationToDelete.toFieldMetadataName,
{ isForeignKey: true },
),
} satisfies WorkspaceMigrationColumnDrop,
],
},
],
);
}
}
// DROP TABLE
await this.workspaceMigrationService.createCustomMigration(
generateMigrationName(`delete-${objectMetadata.nameSingular}`),
workspaceId,
[
{
name: computeObjectTargetTable(objectMetadata),
action: WorkspaceMigrationTableActionType.DROP,
},
],
);
}
} }

View File

@ -1,76 +0,0 @@
import { buildAlteredCommentOnForeignKeyDeletion } from 'src/engine/metadata-modules/object-metadata/utils/create-migration-for-foreign-key-comment-alteration.util';
describe('buildAlteredCommentOnForeignKeyDeletion', () => {
const localObjectMetadataName = 'favorite';
const remoteObjectMetadataName = 'blog';
const schema = 'schema';
const workspaceDataSource = {
query: jest.fn(),
};
it('should return null if no comment ', async () => {
workspaceDataSource.query.mockResolvedValueOnce([]);
const result = await buildAlteredCommentOnForeignKeyDeletion(
localObjectMetadataName,
remoteObjectMetadataName,
schema,
workspaceDataSource as any,
);
expect(result).toBeNull();
});
it('should return null if the existing comment does not contain foreign keys', async () => {
workspaceDataSource.query.mockResolvedValueOnce([
{ col_description: '@graphql({"totalCount":{"enabled":true}})' },
]);
const result = await buildAlteredCommentOnForeignKeyDeletion(
localObjectMetadataName,
remoteObjectMetadataName,
schema,
workspaceDataSource as any,
);
expect(result).toBeNull();
});
it('should return altered comment without foreign key', async () => {
const existingComment = {
col_description: `@graphql({"totalCount":{"enabled":true},"foreign_keys":[{"local_name":"favoriteCollection","local_columns":["${remoteObjectMetadataName}Id"],"foreign_name":"${remoteObjectMetadataName}","foreign_schema":"schema","foreign_table":"${remoteObjectMetadataName}","foreign_columns":["id"]}]})`,
};
workspaceDataSource.query.mockResolvedValueOnce([existingComment]);
const result = await buildAlteredCommentOnForeignKeyDeletion(
localObjectMetadataName,
remoteObjectMetadataName,
schema,
workspaceDataSource as any,
);
expect(result).toBe(
'@graphql({"totalCount":{"enabled":true},"foreign_keys":[]})',
);
});
it('should return altered comment without the input foreign key', async () => {
const existingComment = {
col_description: `@graphql({"totalCount":{"enabled":true},"foreign_keys":[{"local_name":"favoriteCollection","local_columns":["${remoteObjectMetadataName}Id"],"foreign_name":"${remoteObjectMetadataName}","foreign_schema":"schema","foreign_table":"${remoteObjectMetadataName}","foreign_columns":["id"]}, {"local_name":"favoriteCollection","local_columns":["testId"],"foreign_name":"test","foreign_schema":"schema","foreign_table":"test","foreign_columns":["id"]}]})`,
};
workspaceDataSource.query.mockResolvedValueOnce([existingComment]);
const result = await buildAlteredCommentOnForeignKeyDeletion(
localObjectMetadataName,
remoteObjectMetadataName,
schema,
workspaceDataSource as any,
);
expect(result).toBe(
'@graphql({"totalCount":{"enabled":true},"foreign_keys":[{"local_name":"favoriteCollection","local_columns":["testId"],"foreign_name":"test","foreign_schema":"schema","foreign_table":"test","foreign_columns":["id"]}]})',
);
});
});

View File

@ -9,7 +9,7 @@ import {
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
export const createWorkspaceMigrationsForCustomObjectRelations = ( export const buildMigrationsForCustomObjectRelations = (
createdObjectMetadata: ObjectMetadataEntity, createdObjectMetadata: ObjectMetadataEntity,
activityTargetObjectMetadata: ObjectMetadataEntity, activityTargetObjectMetadata: ObjectMetadataEntity,
attachmentObjectMetadata: ObjectMetadataEntity, attachmentObjectMetadata: ObjectMetadataEntity,

View File

@ -1,97 +0,0 @@
import { DataSource } from 'typeorm';
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { RelationToDelete } from 'src/engine/metadata-modules/relation-metadata/types/relation-to-delete';
import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util';
import {
WorkspaceMigrationTableActionType,
WorkspaceMigrationColumnActionType,
WorkspaceMigrationCreateComment,
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service';
import { computeTableName } from 'src/engine/utils/compute-table-name.util';
export const buildAlteredCommentOnForeignKeyDeletion = async (
localObjectMetadataName: string,
remoteObjectMetadataName: string,
schema: string,
workspaceDataSource: DataSource | undefined,
): Promise<string | null> => {
const existingComment = await workspaceDataSource?.query(
`SELECT col_description('${schema}."${localObjectMetadataName}"'::regclass, 0)`,
);
if (!existingComment[0]?.col_description) {
return null;
}
const commentWithoutGraphQL = existingComment[0].col_description
.replace('@graphql(', '')
.replace(')', '');
const parsedComment = JSON.parse(commentWithoutGraphQL);
const currentForeignKeys = parsedComment.foreign_keys;
if (!currentForeignKeys) {
return null;
}
const updatedForeignKeys = currentForeignKeys.filter(
(foreignKey: any) =>
foreignKey.foreign_name !== remoteObjectMetadataName &&
foreignKey.foreign_table !== remoteObjectMetadataName,
);
parsedComment.foreign_keys = updatedForeignKeys;
return `@graphql(${JSON.stringify(parsedComment)})`;
};
export const createMigrationToAlterCommentOnForeignKeyDeletion = async (
dataSourceService: DataSourceService,
typeORMService: TypeORMService,
workspaceMigrationService: WorkspaceMigrationService,
workspaceId: string,
relationToDelete: RelationToDelete,
) => {
const dataSourceMetadata =
await dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
workspaceId,
);
const workspaceDataSource =
await typeORMService.connectToDataSource(dataSourceMetadata);
const alteredComment = await buildAlteredCommentOnForeignKeyDeletion(
relationToDelete.toObjectName,
relationToDelete.fromObjectName,
dataSourceMetadata.schema,
workspaceDataSource,
);
if (alteredComment) {
await workspaceMigrationService.createCustomMigration(
generateMigrationName(
`alter-comment-${relationToDelete.fromObjectName}-${relationToDelete.toObjectName}`,
),
workspaceId,
[
{
name: computeTableName(
relationToDelete.toObjectName,
relationToDelete.toObjectMetadataIsCustom,
),
action: WorkspaceMigrationTableActionType.ALTER,
columns: [
{
action: WorkspaceMigrationColumnActionType.CREATE_COMMENT,
comment: alteredComment,
} satisfies WorkspaceMigrationCreateComment,
],
},
],
);
}
};

View File

@ -1,77 +0,0 @@
import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import {
WorkspaceMigrationTableAction,
WorkspaceMigrationColumnActionType,
WorkspaceMigrationColumnCreate,
WorkspaceMigrationTableActionType,
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
export const createWorkspaceMigrationsForRemoteObjectRelations = async (
createdObjectMetadata: ObjectMetadataEntity,
activityTargetObjectMetadata: ObjectMetadataEntity,
attachmentObjectMetadata: ObjectMetadataEntity,
timelineActivityObjectMetadata: ObjectMetadataEntity,
favoriteObjectMetadata: ObjectMetadataEntity,
primaryKeyColumnType: string,
): Promise<WorkspaceMigrationTableAction[]> => {
return [
{
name: computeObjectTargetTable(activityTargetObjectMetadata),
action: WorkspaceMigrationTableActionType.ALTER,
columns: [
{
action: WorkspaceMigrationColumnActionType.CREATE,
columnName: computeColumnName(createdObjectMetadata.nameSingular, {
isForeignKey: true,
}),
columnType: primaryKeyColumnType,
isNullable: true,
} satisfies WorkspaceMigrationColumnCreate,
],
},
{
name: computeObjectTargetTable(attachmentObjectMetadata),
action: WorkspaceMigrationTableActionType.ALTER,
columns: [
{
action: WorkspaceMigrationColumnActionType.CREATE,
columnName: computeColumnName(createdObjectMetadata.nameSingular, {
isForeignKey: true,
}),
columnType: primaryKeyColumnType,
isNullable: true,
} satisfies WorkspaceMigrationColumnCreate,
],
},
{
name: computeObjectTargetTable(timelineActivityObjectMetadata),
action: WorkspaceMigrationTableActionType.ALTER,
columns: [
{
action: WorkspaceMigrationColumnActionType.CREATE,
columnName: computeColumnName(createdObjectMetadata.nameSingular, {
isForeignKey: true,
}),
columnType: primaryKeyColumnType,
isNullable: true,
} satisfies WorkspaceMigrationColumnCreate,
],
},
{
name: computeObjectTargetTable(favoriteObjectMetadata),
action: WorkspaceMigrationTableActionType.ALTER,
columns: [
{
action: WorkspaceMigrationColumnActionType.CREATE,
columnName: computeColumnName(createdObjectMetadata.nameSingular, {
isForeignKey: true,
}),
columnType: primaryKeyColumnType,
isNullable: true,
} satisfies WorkspaceMigrationColumnCreate,
],
},
];
};

View File

@ -0,0 +1,20 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { RemoteTableRelationsService } from 'src/engine/metadata-modules/remote-server/remote-table/remote-table-relations/remote-table-relations.service';
import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module';
@Module({
imports: [
TypeOrmModule.forFeature(
[ObjectMetadataEntity, FieldMetadataEntity],
'metadata',
),
WorkspaceMigrationModule,
],
providers: [RemoteTableRelationsService],
exports: [RemoteTableRelationsService],
})
export class RemoteTableRelationsModule {}

View File

@ -0,0 +1,327 @@
import { InjectRepository } from '@nestjs/typeorm';
import { Injectable } from '@nestjs/common';
import { In, Repository } from 'typeorm';
import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util';
import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service';
import {
FieldMetadataEntity,
FieldMetadataType,
} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { createForeignKeyDeterministicUuid } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/create-deterministic-uuid.util';
import {
ACTIVITY_TARGET_STANDARD_FIELD_IDS,
ATTACHMENT_STANDARD_FIELD_IDS,
FAVORITE_STANDARD_FIELD_IDS,
TIMELINE_ACTIVITY_STANDARD_FIELD_IDS,
} from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
import { buildMigrationsToCreateRemoteTableRelations } from 'src/engine/metadata-modules/remote-server/remote-table/remote-table-relations/utils/build-migrations-to-create-remote-table-relations.util';
import { buildMigrationsToRemoveRemoteTableRelations } from 'src/engine/metadata-modules/remote-server/remote-table/remote-table-relations/utils/build-migrations-to-remove-remote-table-relations.util';
import { mapUdtNameToFieldType } from 'src/engine/metadata-modules/remote-server/remote-table/utils/udt-name-mapper.util';
import { createRelationForeignKeyFieldMetadataName } from 'src/engine/metadata-modules/relation-metadata/utils/create-relation-foreign-key-field-metadata-name.util';
@Injectable()
export class RemoteTableRelationsService {
constructor(
@InjectRepository(ObjectMetadataEntity, 'metadata')
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
@InjectRepository(FieldMetadataEntity, 'metadata')
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
private readonly workspaceMigrationService: WorkspaceMigrationService,
) {}
public async createForeignKeysMetadataAndMigrations(
workspaceId: string,
remoteObjectMetadata: ObjectMetadataEntity,
objectPrimaryKeyFieldSettings:
| FieldMetadataSettings<FieldMetadataType | 'default'>
| undefined,
objectPrimaryKeyColumnType?: string,
) {
const objectPrimaryKeyFieldType = mapUdtNameToFieldType(
objectPrimaryKeyColumnType ?? 'uuid',
);
const favoriteObjectMetadata = await this.createFavoriteRelation(
workspaceId,
remoteObjectMetadata,
objectPrimaryKeyFieldType,
objectPrimaryKeyFieldSettings,
);
const activityTargetObjectMetadata =
await this.createActivityTargetRelation(
workspaceId,
remoteObjectMetadata,
objectPrimaryKeyFieldType,
objectPrimaryKeyFieldSettings,
);
const attachmentObjectMetadata = await this.createAttachmentRelation(
workspaceId,
remoteObjectMetadata,
objectPrimaryKeyFieldType,
objectPrimaryKeyFieldSettings,
);
const timelineActivityObjectMetadata =
await this.createTimelineActivityRelation(
workspaceId,
remoteObjectMetadata,
objectPrimaryKeyFieldType,
objectPrimaryKeyFieldSettings,
);
// create migration to add foreign key columns
await this.workspaceMigrationService.createCustomMigration(
generateMigrationName(
`add-foreign-keys-${remoteObjectMetadata.nameSingular}`,
),
workspaceId,
buildMigrationsToCreateRemoteTableRelations(
remoteObjectMetadata.nameSingular,
[
favoriteObjectMetadata,
activityTargetObjectMetadata,
attachmentObjectMetadata,
timelineActivityObjectMetadata,
],
objectPrimaryKeyColumnType ?? 'uuid',
),
);
}
public async deleteForeignKeysMetadataAndCreateMigrations(
workspaceId: string,
remoteObjectMetadata: ObjectMetadataEntity,
) {
// find favorite, activityTarget, attachment, timelineActivity objects
const favoriteObjectMetadata =
await this.objectMetadataRepository.findOneByOrFail({
nameSingular: 'favorite',
workspaceId: workspaceId,
});
const activityTargetObjectMetadata =
await this.objectMetadataRepository.findOneByOrFail({
nameSingular: 'activityTarget',
workspaceId: workspaceId,
});
const attachmentObjectMetadata =
await this.objectMetadataRepository.findOneByOrFail({
nameSingular: 'attachment',
workspaceId: workspaceId,
});
const timelineActivityObjectMetadata =
await this.objectMetadataRepository.findOneByOrFail({
nameSingular: 'timelineActivity',
workspaceId: workspaceId,
});
// compute the target column name
const targetColumnName = createRelationForeignKeyFieldMetadataName(
remoteObjectMetadata.nameSingular,
);
// find the foreign key fields to delete
const foreignKeyFieldsToDelete = await this.fieldMetadataRepository.find({
where: {
name: targetColumnName,
objectMetadataId: In([
favoriteObjectMetadata.id,
activityTargetObjectMetadata.id,
attachmentObjectMetadata.id,
timelineActivityObjectMetadata.id,
]),
workspaceId,
},
});
const foreignKeyFieldsToDeleteIds = foreignKeyFieldsToDelete.map(
(field) => field.id,
);
await this.fieldMetadataRepository.delete(foreignKeyFieldsToDeleteIds);
// create migration to drop foreign key columns
await this.workspaceMigrationService.createCustomMigration(
generateMigrationName(
`delete-foreign-keys-${remoteObjectMetadata.nameSingular}`,
),
workspaceId,
buildMigrationsToRemoveRemoteTableRelations(targetColumnName, [
favoriteObjectMetadata,
activityTargetObjectMetadata,
attachmentObjectMetadata,
timelineActivityObjectMetadata,
]),
);
}
private async createActivityTargetRelation(
workspaceId: string,
createdObjectMetadata: ObjectMetadataEntity,
objectPrimaryKeyType: FieldMetadataType,
objectPrimaryKeyFieldSettings:
| FieldMetadataSettings<FieldMetadataType | 'default'>
| undefined,
) {
const activityTargetObjectMetadata =
await this.objectMetadataRepository.findOneByOrFail({
nameSingular: 'activityTarget',
workspaceId: workspaceId,
});
await this.fieldMetadataRepository.save(
// Foreign key
{
standardId: createForeignKeyDeterministicUuid({
objectId: createdObjectMetadata.id,
standardId: ACTIVITY_TARGET_STANDARD_FIELD_IDS.custom,
}),
objectMetadataId: activityTargetObjectMetadata.id,
workspaceId: workspaceId,
isCustom: false,
isActive: true,
type: objectPrimaryKeyType,
name: `${createdObjectMetadata.nameSingular}Id`,
label: `${createdObjectMetadata.labelSingular} ID (foreign key)`,
description: `ActivityTarget ${createdObjectMetadata.labelSingular} id foreign key`,
icon: undefined,
isNullable: true,
isSystem: true,
defaultValue: undefined,
settings: { ...objectPrimaryKeyFieldSettings, isForeignKey: true },
},
);
return activityTargetObjectMetadata;
}
private async createAttachmentRelation(
workspaceId: string,
createdObjectMetadata: ObjectMetadataEntity,
objectPrimaryKeyType: FieldMetadataType,
objectPrimaryKeyFieldSettings:
| FieldMetadataSettings<FieldMetadataType | 'default'>
| undefined,
) {
const attachmentObjectMetadata =
await this.objectMetadataRepository.findOneByOrFail({
nameSingular: 'attachment',
workspaceId: workspaceId,
});
await this.fieldMetadataRepository.save(
// Foreign key
{
standardId: createForeignKeyDeterministicUuid({
objectId: createdObjectMetadata.id,
standardId: ATTACHMENT_STANDARD_FIELD_IDS.custom,
}),
objectMetadataId: attachmentObjectMetadata.id,
workspaceId: workspaceId,
isCustom: false,
isActive: true,
type: objectPrimaryKeyType,
name: `${createdObjectMetadata.nameSingular}Id`,
label: `${createdObjectMetadata.labelSingular} ID (foreign key)`,
description: `Attachment ${createdObjectMetadata.labelSingular} id foreign key`,
icon: undefined,
isNullable: true,
isSystem: true,
defaultValue: undefined,
settings: { ...objectPrimaryKeyFieldSettings, isForeignKey: true },
},
);
return attachmentObjectMetadata;
}
private async createTimelineActivityRelation(
workspaceId: string,
createdObjectMetadata: ObjectMetadataEntity,
objectPrimaryKeyType: FieldMetadataType,
objectPrimaryKeyFieldSettings:
| FieldMetadataSettings<FieldMetadataType | 'default'>
| undefined,
) {
const timelineActivityObjectMetadata =
await this.objectMetadataRepository.findOneByOrFail({
nameSingular: 'timelineActivity',
workspaceId: workspaceId,
});
await this.fieldMetadataRepository.save(
// Foreign key
{
standardId: createForeignKeyDeterministicUuid({
objectId: createdObjectMetadata.id,
standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.custom,
}),
objectMetadataId: timelineActivityObjectMetadata.id,
workspaceId: workspaceId,
isCustom: false,
isActive: true,
type: objectPrimaryKeyType,
name: `${createdObjectMetadata.nameSingular}Id`,
label: `${createdObjectMetadata.labelSingular} ID (foreign key)`,
description: `Timeline Activity ${createdObjectMetadata.labelSingular} id foreign key`,
icon: undefined,
isNullable: true,
isSystem: true,
defaultValue: undefined,
settings: { ...objectPrimaryKeyFieldSettings, isForeignKey: true },
},
);
return timelineActivityObjectMetadata;
}
private async createFavoriteRelation(
workspaceId: string,
createdObjectMetadata: ObjectMetadataEntity,
objectPrimaryKeyType: FieldMetadataType,
objectPrimaryKeyFieldSettings:
| FieldMetadataSettings<FieldMetadataType | 'default'>
| undefined,
) {
const favoriteObjectMetadata =
await this.objectMetadataRepository.findOneByOrFail({
nameSingular: 'favorite',
workspaceId: workspaceId,
});
await this.fieldMetadataRepository.save(
// Foreign key
{
standardId: createForeignKeyDeterministicUuid({
objectId: createdObjectMetadata.id,
standardId: FAVORITE_STANDARD_FIELD_IDS.custom,
}),
objectMetadataId: favoriteObjectMetadata.id,
workspaceId: workspaceId,
isCustom: false,
isActive: true,
type: objectPrimaryKeyType,
name: `${createdObjectMetadata.nameSingular}Id`,
label: `${createdObjectMetadata.labelSingular} ID (foreign key)`,
description: `Favorite ${createdObjectMetadata.labelSingular} id foreign key`,
icon: undefined,
isNullable: true,
isSystem: true,
defaultValue: undefined,
settings: { ...objectPrimaryKeyFieldSettings, isForeignKey: true },
},
);
return favoriteObjectMetadata;
}
}

View File

@ -0,0 +1,29 @@
import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import {
WorkspaceMigrationTableAction,
WorkspaceMigrationColumnActionType,
WorkspaceMigrationColumnCreate,
WorkspaceMigrationTableActionType,
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
export const buildMigrationsToCreateRemoteTableRelations = (
createdObjectNameSingular: string,
targetObjectMetadataList: ObjectMetadataEntity[],
primaryKeyColumnType: string,
): WorkspaceMigrationTableAction[] =>
targetObjectMetadataList.map((targetObjectMetadata) => ({
name: computeObjectTargetTable(targetObjectMetadata),
action: WorkspaceMigrationTableActionType.ALTER,
columns: [
{
action: WorkspaceMigrationColumnActionType.CREATE,
columnName: computeColumnName(createdObjectNameSingular, {
isForeignKey: true,
}),
columnType: primaryKeyColumnType,
isNullable: true,
} satisfies WorkspaceMigrationColumnCreate,
],
}));

View File

@ -0,0 +1,26 @@
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import {
WorkspaceMigrationTableAction,
WorkspaceMigrationTableActionType,
WorkspaceMigrationColumnActionType,
WorkspaceMigrationColumnDrop,
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
import { computeTableName } from 'src/engine/utils/compute-table-name.util';
export const buildMigrationsToRemoveRemoteTableRelations = (
targetColumnName: string,
targetObjectMetadataList: ObjectMetadataEntity[],
): WorkspaceMigrationTableAction[] =>
targetObjectMetadataList.map((objectMetadata) => ({
name: computeTableName(
objectMetadata.nameSingular,
objectMetadata.isCustom,
),
action: WorkspaceMigrationTableActionType.ALTER,
columns: [
{
action: WorkspaceMigrationColumnActionType.DROP,
columnName: targetColumnName,
} satisfies WorkspaceMigrationColumnDrop,
],
}));