morph relation : renaming an object (#13404)

# Why
If we have a Morph Relation, like :
Opportunity <-> Company & People

Let's say it's a MANY_TO_ONE on Opportunity side

Then we have two joinColumnNames looking like
- ownerPersonId
- ownerCompanyId

Let's say someone renames the obejct Person (assume we can even though
standard obejcts cannot be renames per say at the moment in the API)

We need to update the joinColumnName and create the associated
migrations
This commit is contained in:
Guillim
2025-07-25 16:40:51 +02:00
committed by GitHub
parent 9380a1386a
commit 8ea816b7ef
7 changed files with 343 additions and 0 deletions
@@ -17,4 +17,5 @@ export enum ObjectMetadataExceptionCode {
OBJECT_MUTATION_NOT_ALLOWED = 'OBJECT_MUTATION_NOT_ALLOWED',
OBJECT_ALREADY_EXISTS = 'OBJECT_ALREADY_EXISTS',
MISSING_CUSTOM_OBJECT_DEFAULT_LABEL_IDENTIFIER_FIELD = 'MISSING_CUSTOM_OBJECT_DEFAULT_LABEL_IDENTIFIER_FIELD',
INVALID_ORM_OUTPUT = 'INVALID_ORM_OUTPUT',
}
@@ -606,6 +606,21 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
queryRunner,
);
const morphRelationFieldMetadataToUpdate =
await this.objectMetadataFieldRelationService.updateMorphRelationsJoinColumnName(
{
existingObjectMetadata,
objectMetadataForUpdate,
queryRunner,
},
);
await this.objectMetadataMigrationService.updateMorphRelationMigrations({
workspaceId: objectMetadataForUpdate.workspaceId,
morphRelationFieldMetadataToUpdate: morphRelationFieldMetadataToUpdate,
queryRunner,
});
await this.objectMetadataMigrationService.recomputeEnumNames(
objectMetadataForUpdate,
objectMetadataForUpdate.workspaceId,
@@ -9,7 +9,12 @@ import { v4 as uuidV4 } from 'uuid';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { computeMorphRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-relation-field-join-column-name.util';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import {
ObjectMetadataException,
ObjectMetadataExceptionCode,
} from 'src/engine/metadata-modules/object-metadata/object-metadata.exception';
import { buildDescriptionForRelationFieldMetadataOnFromField } from 'src/engine/metadata-modules/object-metadata/utils/build-description-for-relation-field-on-from-field.util';
import { buildDescriptionForRelationFieldMetadataOnToField } from 'src/engine/metadata-modules/object-metadata/utils/build-description-for-relation-field-on-to-field.util';
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-metadata/relation-on-delete-action.type';
@@ -412,4 +417,108 @@ export class ObjectMetadataFieldRelationService {
description,
};
}
private validateFieldMetadataTypeIsMorphRelation = (
fieldMetadatas: FieldMetadataEntity[],
): fieldMetadatas is Array<
FieldMetadataEntity & FieldMetadataEntity<FieldMetadataType.MORPH_RELATION>
> => {
return fieldMetadatas.every(
(fieldMetadata) =>
fieldMetadata.type === FieldMetadataType.MORPH_RELATION,
);
};
public async findTargetMorphRelationFieldMetadatas(
objectMetadataId: string,
): Promise<FieldMetadataEntity<FieldMetadataType.MORPH_RELATION>[]> {
const fieldMetadatas = await this.fieldMetadataRepository.find({
where: {
relationTargetObjectMetadataId: objectMetadataId,
type: FieldMetadataType.MORPH_RELATION,
},
relations: {
relationTargetObjectMetadata: true,
object: true,
},
});
if (!this.validateFieldMetadataTypeIsMorphRelation(fieldMetadatas)) {
throw new ObjectMetadataException(
'Invalid field metadata type. Expected MORPH_RELATION only',
ObjectMetadataExceptionCode.INVALID_ORM_OUTPUT,
);
}
return fieldMetadatas;
}
public async updateMorphRelationsJoinColumnName({
existingObjectMetadata,
objectMetadataForUpdate,
queryRunner,
}: {
existingObjectMetadata: Pick<
ObjectMetadataItemWithFieldMaps,
'nameSingular' | 'isCustom' | 'id' | 'labelPlural' | 'icon' | 'fieldsById'
>;
objectMetadataForUpdate: Pick<
ObjectMetadataItemWithFieldMaps,
| 'nameSingular'
| 'isCustom'
| 'workspaceId'
| 'id'
| 'labelSingular'
| 'labelPlural'
| 'icon'
| 'fieldsById'
>;
queryRunner: QueryRunner;
}): Promise<
{
fieldMetadata: FieldMetadataEntity<FieldMetadataType.MORPH_RELATION>;
newJoinColumnName: string;
}[]
> {
const fieldMetadataRepository =
queryRunner.manager.getRepository(FieldMetadataEntity);
const morphRelationFieldMetadataTargets =
await this.findTargetMorphRelationFieldMetadatas(
existingObjectMetadata.id,
);
const morphRelationFieldMetadataToUpdate =
morphRelationFieldMetadataTargets.filter(
(morphRelationFieldMetadata) =>
morphRelationFieldMetadata.settings?.relationType ===
RelationType.MANY_TO_ONE,
);
const morphRelationFieldMetadataToUpdateWithNewJoinColumnName = [];
if (morphRelationFieldMetadataToUpdate.length > 0) {
for (const morphRelationFieldMetadata of morphRelationFieldMetadataToUpdate) {
const newJoinColumnName = computeMorphRelationFieldJoinColumnName({
name: morphRelationFieldMetadata.name,
targetObjectMetadataNameSingular:
objectMetadataForUpdate.nameSingular,
});
await fieldMetadataRepository.save({
...morphRelationFieldMetadata,
settings: {
...morphRelationFieldMetadata.settings,
joinColumnName: newJoinColumnName,
},
});
morphRelationFieldMetadataToUpdateWithNewJoinColumnName.push({
fieldMetadata: morphRelationFieldMetadata,
newJoinColumnName,
});
}
}
return morphRelationFieldMetadataToUpdateWithNewJoinColumnName;
}
}
@@ -8,6 +8,10 @@ import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfa
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 {
ObjectMetadataException,
ObjectMetadataExceptionCode,
} from 'src/engine/metadata-modules/object-metadata/object-metadata.exception';
import { buildMigrationsForCustomObjectRelations } from 'src/engine/metadata-modules/object-metadata/utils/build-migrations-for-custom-object-relations.util';
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
import { fieldMetadataTypeToColumnType } from 'src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util';
@@ -360,4 +364,61 @@ export class ObjectMetadataMigrationService {
);
}
}
public async updateMorphRelationMigrations({
workspaceId,
morphRelationFieldMetadataToUpdate,
queryRunner,
}: {
workspaceId: string;
morphRelationFieldMetadataToUpdate: {
fieldMetadata: FieldMetadataEntity<FieldMetadataType.MORPH_RELATION>;
newJoinColumnName: string;
}[];
queryRunner?: QueryRunner;
}) {
for (const morphRelationFieldMetadata of morphRelationFieldMetadataToUpdate) {
if (!morphRelationFieldMetadata.fieldMetadata.settings?.joinColumnName) {
throw new ObjectMetadataException(
`Settings for morph relation field should be defined ${morphRelationFieldMetadata.fieldMetadata.name}`,
ObjectMetadataExceptionCode.INVALID_ORM_OUTPUT,
);
}
await this.workspaceMigrationService.createCustomMigration(
generateMigrationName(
`rename-join-column-name-${morphRelationFieldMetadata.fieldMetadata.name}-to-${morphRelationFieldMetadata.newJoinColumnName}-in-${morphRelationFieldMetadata.fieldMetadata.object.nameSingular}`,
),
workspaceId,
[
{
name: computeObjectTargetTable(
morphRelationFieldMetadata.fieldMetadata.object,
),
action: WorkspaceMigrationTableActionType.ALTER,
columns: [
{
action: WorkspaceMigrationColumnActionType.ALTER,
currentColumnDefinition: {
columnName:
morphRelationFieldMetadata.fieldMetadata.settings
?.joinColumnName,
columnType: 'uuid',
isNullable: true,
defaultValue: null,
},
alteredColumnDefinition: {
columnName: `${morphRelationFieldMetadata.newJoinColumnName}`,
columnType: 'uuid',
isNullable: true,
defaultValue: null,
},
},
],
},
],
queryRunner,
);
}
}
}
@@ -1,6 +1,7 @@
import {
ConflictError,
ForbiddenError,
InternalServerError,
NotFoundError,
UserInputError,
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
@@ -25,6 +26,8 @@ export const objectMetadataGraphqlApiExceptionHandler = (error: Error) => {
throw new ForbiddenError(error);
case ObjectMetadataExceptionCode.OBJECT_ALREADY_EXISTS:
throw new ConflictError(error);
case ObjectMetadataExceptionCode.INVALID_ORM_OUTPUT:
throw new InternalServerError(error);
case ObjectMetadataExceptionCode.MISSING_CUSTOM_OBJECT_DEFAULT_LABEL_IDENTIFIER_FIELD:
throw error;
default: {