Refactor standard relations update at custom object renaming (#8638)

Refactoring update of standard relations when a custom object is
renamed, after observing occasional issues.

---------

Co-authored-by: Weiko <corentin@twenty.com>
This commit is contained in:
Marie 2024-11-21 15:54:12 +01:00 committed by GitHub
parent cf73e32969
commit 670c8a0b98
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 208 additions and 73 deletions

View File

@ -424,14 +424,14 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
existingObjectMetadata, existingObjectMetadata,
); );
if (!(newTargetTableName === existingTargetTableName)) { if (newTargetTableName !== existingTargetTableName) {
await this.objectMetadataMigrationService.createRenameTableMigration( await this.objectMetadataMigrationService.createRenameTableMigration(
existingObjectMetadata, existingObjectMetadata,
objectMetadataForUpdate, objectMetadataForUpdate,
objectMetadataForUpdate.workspaceId, objectMetadataForUpdate.workspaceId,
); );
await this.objectMetadataMigrationService.createRelationsUpdatesMigrations( await this.objectMetadataMigrationService.createStandardRelationsUpdatesMigrations(
existingObjectMetadata, existingObjectMetadata,
objectMetadataForUpdate, objectMetadataForUpdate,
objectMetadataForUpdate.workspaceId, objectMetadataForUpdate.workspaceId,

View File

@ -6,7 +6,10 @@ import { Repository } from 'typeorm';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
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 { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
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 { buildMigrationsForCustomObjectRelations } from 'src/engine/metadata-modules/object-metadata/utils/build-migrations-for-custom-object-relations.util'; import { buildMigrationsForCustomObjectRelations } from 'src/engine/metadata-modules/object-metadata/utils/build-migrations-for-custom-object-relations.util';
import { buildNameLabelAndDescriptionForForeignKeyFieldMetadata } from 'src/engine/metadata-modules/object-metadata/utils/build-name-label-and-description-for-foreign-key-field-metadata.util';
import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
import { RelationToDelete } from 'src/engine/metadata-modules/relation-metadata/types/relation-to-delete'; import { RelationToDelete } from 'src/engine/metadata-modules/relation-metadata/types/relation-to-delete';
import { fieldMetadataTypeToColumnType } from 'src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util'; import { fieldMetadataTypeToColumnType } from 'src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util';
@ -115,7 +118,7 @@ export class ObjectMetadataMigrationService {
); );
} }
public async createRelationsUpdatesMigrations( public async createStandardRelationsUpdatesMigrations(
existingObjectMetadata: ObjectMetadataEntity, existingObjectMetadata: ObjectMetadataEntity,
updatedObjectMetadata: ObjectMetadataEntity, updatedObjectMetadata: ObjectMetadataEntity,
workspaceId: string, workspaceId: string,
@ -124,87 +127,155 @@ export class ObjectMetadataMigrationService {
const newTableName = computeObjectTargetTable(updatedObjectMetadata); const newTableName = computeObjectTargetTable(updatedObjectMetadata);
if (existingTableName !== newTableName) { if (existingTableName !== newTableName) {
const searchCriteria = { const relatedObjectsIds = await this.relationMetadataRepository
isCustom: false, .find({
settings: { where: {
isForeignKey: true, workspaceId,
}, fromObjectMetadataId: existingObjectMetadata.id,
name: `${existingObjectMetadata.nameSingular}Id`, },
workspaceId: workspaceId, })
}; .then((relations) =>
relations.map((relation) => relation.toObjectMetadataId),
);
const fieldsWihStandardRelation = await this.fieldMetadataRepository.find( const foreignKeyFieldMetadataForStandardRelation =
{ await this.fieldMetadataRepository.find({
where: { where: {
isCustom: false, isCustom: false,
settings: { settings: {
isForeignKey: true, isForeignKey: true,
}, },
name: `${existingObjectMetadata.nameSingular}Id`, name: `${existingObjectMetadata.nameSingular}Id`,
workspaceId: workspaceId,
}, },
}, });
);
await this.fieldMetadataRepository.update(searchCriteria, {
name: `${updatedObjectMetadata.nameSingular}Id`,
});
await Promise.all( await Promise.all(
fieldsWihStandardRelation.map(async (fieldWihStandardRelation) => { foreignKeyFieldMetadataForStandardRelation.map(
const relatedObject = await this.objectMetadataRepository.findOneBy({ async (foreignKeyFieldMetadata) => {
id: fieldWihStandardRelation.objectMetadataId, if (
workspaceId: workspaceId, relatedObjectsIds.includes(
}); foreignKeyFieldMetadata.objectMetadataId,
)
) {
const relatedObject =
await this.objectMetadataRepository.findOneBy({
id: foreignKeyFieldMetadata.objectMetadataId,
workspaceId: workspaceId,
});
if (relatedObject) { if (relatedObject) {
await this.fieldMetadataRepository.update( // 1. Update to and from relation fieldMetadata)
{ const toFieldRelationFieldMetadataId =
name: existingObjectMetadata.nameSingular, await this.fieldMetadataRepository
label: existingObjectMetadata.labelSingular, .findOneByOrFail({
}, name: existingObjectMetadata.nameSingular,
{ label: existingObjectMetadata.labelSingular,
name: updatedObjectMetadata.nameSingular, objectMetadataId: relatedObject.id,
label: updatedObjectMetadata.labelSingular, workspaceId: workspaceId,
}, })
); .then((field) => field.id);
const relationTableName = computeObjectTargetTable(relatedObject); const { description: descriptionForToField } =
const columnName = `${existingObjectMetadata.nameSingular}Id`; buildDescriptionForRelationFieldMetadataOnToField({
const columnType = fieldMetadataTypeToColumnType( relationObjectMetadataNamePlural: relatedObject.namePlural,
fieldWihStandardRelation.type, targetObjectLabelSingular:
); updatedObjectMetadata.labelSingular,
});
await this.workspaceMigrationService.createCustomMigration( await this.fieldMetadataRepository.update(
generateMigrationName( toFieldRelationFieldMetadataId,
`rename-${existingObjectMetadata.nameSingular}-to-${updatedObjectMetadata.nameSingular}-in-${relatedObject.nameSingular}`, {
), name: updatedObjectMetadata.nameSingular,
workspaceId, label: updatedObjectMetadata.labelSingular,
[ description: descriptionForToField,
{ },
name: relationTableName, );
action: WorkspaceMigrationTableActionType.ALTER,
columns: [ const fromFieldRelationFieldMetadataId =
await this.relationMetadataRepository
.findOneByOrFail({
fromObjectMetadataId: existingObjectMetadata.id,
toObjectMetadataId: relatedObject.id,
toFieldMetadataId: toFieldRelationFieldMetadataId,
workspaceId,
})
.then((relation) => relation?.fromFieldMetadataId);
await this.fieldMetadataRepository.update(
fromFieldRelationFieldMetadataId,
{
description:
buildDescriptionForRelationFieldMetadataOnFromField({
relationObjectMetadataNamePlural:
relatedObject.namePlural,
targetObjectLabelSingular:
updatedObjectMetadata.labelSingular,
}).description,
},
);
// 2. Update foreign key fieldMetadata
const {
name: updatedNameForForeignKeyFieldMetadata,
label: updatedLabelForForeignKeyFieldMetadata,
description: updatedDescriptionForForeignKeyFieldMetadata,
} = buildNameLabelAndDescriptionForForeignKeyFieldMetadata({
targetObjectNameSingular: updatedObjectMetadata.nameSingular,
targetObjectLabelSingular:
updatedObjectMetadata.labelSingular,
relatedObjectLabelSingular: relatedObject.labelSingular,
});
await this.fieldMetadataRepository.update(
foreignKeyFieldMetadata.id,
{
name: updatedNameForForeignKeyFieldMetadata,
label: updatedLabelForForeignKeyFieldMetadata,
description: updatedDescriptionForForeignKeyFieldMetadata,
},
);
const relatedObjectTableName =
computeObjectTargetTable(relatedObject);
const columnName = `${existingObjectMetadata.nameSingular}Id`;
const columnType = fieldMetadataTypeToColumnType(
foreignKeyFieldMetadata.type,
);
await this.workspaceMigrationService.createCustomMigration(
generateMigrationName(
`rename-${existingObjectMetadata.nameSingular}-to-${updatedObjectMetadata.nameSingular}-in-${relatedObject.nameSingular}`,
),
workspaceId,
[
{ {
action: WorkspaceMigrationColumnActionType.ALTER, name: relatedObjectTableName,
currentColumnDefinition: { action: WorkspaceMigrationTableActionType.ALTER,
columnName, columns: [
columnType, {
isNullable: true, action: WorkspaceMigrationColumnActionType.ALTER,
defaultValue: null, currentColumnDefinition: {
}, columnName,
alteredColumnDefinition: { columnType,
columnName: `${updatedObjectMetadata.nameSingular}Id`, isNullable: true,
columnType, defaultValue: null,
isNullable: true, },
defaultValue: null, alteredColumnDefinition: {
}, columnName: `${updatedObjectMetadata.nameSingular}Id`,
columnType,
isNullable: true,
defaultValue: null,
},
},
],
}, },
], ],
}, );
], }
); }
} },
}), ),
); );
} }
} }

View File

@ -10,6 +10,9 @@ import {
FieldMetadataType, FieldMetadataType,
} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
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 { buildNameLabelAndDescriptionForForeignKeyFieldMetadata } from 'src/engine/metadata-modules/object-metadata/utils/build-name-label-and-description-for-foreign-key-field-metadata.util';
import { import {
RelationMetadataEntity, RelationMetadataEntity,
RelationMetadataType, RelationMetadataType,
@ -94,6 +97,13 @@ export class ObjectMetadataRelationService {
); );
} }
const { name, label, description } =
buildNameLabelAndDescriptionForForeignKeyFieldMetadata({
targetObjectNameSingular: createdObjectMetadata.nameSingular,
targetObjectLabelSingular: createdObjectMetadata.labelSingular,
relatedObjectLabelSingular: relatedObjectMetadata.labelSingular,
});
await this.fieldMetadataRepository.save({ await this.fieldMetadataRepository.save({
standardId: createForeignKeyDeterministicUuid({ standardId: createForeignKeyDeterministicUuid({
objectId: createdObjectMetadata.id, objectId: createdObjectMetadata.id,
@ -104,9 +114,9 @@ export class ObjectMetadataRelationService {
isCustom: false, isCustom: false,
isActive: true, isActive: true,
type: objectPrimaryKeyType, type: objectPrimaryKeyType,
name: `${createdObjectMetadata.nameSingular}Id`, name,
label: `${createdObjectMetadata.labelSingular} ID (foreign key)`, label,
description: `${relatedObjectMetadata.labelSingular} ${createdObjectMetadata.labelSingular} id foreign key`, description,
icon: undefined, icon: undefined,
isNullable: true, isNullable: true,
isSystem: true, isSystem: true,
@ -141,6 +151,13 @@ export class ObjectMetadataRelationService {
) { ) {
const relationObjectMetadataNamePlural = relatedObjectMetadata.namePlural; const relationObjectMetadataNamePlural = relatedObjectMetadata.namePlural;
const { description } = buildDescriptionForRelationFieldMetadataOnFromField(
{
relationObjectMetadataNamePlural,
targetObjectLabelSingular: createdObjectMetadata.labelSingular,
},
);
return { return {
standardId: standardId:
CUSTOM_OBJECT_STANDARD_FIELD_IDS[relationObjectMetadataNamePlural], CUSTOM_OBJECT_STANDARD_FIELD_IDS[relationObjectMetadataNamePlural],
@ -152,7 +169,7 @@ export class ObjectMetadataRelationService {
type: FieldMetadataType.RELATION, type: FieldMetadataType.RELATION,
name: relatedObjectMetadata.namePlural, name: relatedObjectMetadata.namePlural,
label: capitalize(relationObjectMetadataNamePlural), label: capitalize(relationObjectMetadataNamePlural),
description: `${capitalize(relationObjectMetadataNamePlural)} tied to the ${createdObjectMetadata.labelSingular}`, description,
icon: icon:
STANDARD_OBJECT_ICONS[relatedObjectMetadata.nameSingular] || STANDARD_OBJECT_ICONS[relatedObjectMetadata.nameSingular] ||
'IconBuildingSkyscraper', 'IconBuildingSkyscraper',
@ -174,6 +191,11 @@ export class ObjectMetadataRelationService {
); );
} }
const { description } = buildDescriptionForRelationFieldMetadataOnToField({
relationObjectMetadataNamePlural: relatedObjectMetadata.namePlural,
targetObjectLabelSingular: createdObjectMetadata.labelSingular,
});
return { return {
standardId: createRelationDeterministicUuid({ standardId: createRelationDeterministicUuid({
objectId: createdObjectMetadata.id, objectId: createdObjectMetadata.id,
@ -187,7 +209,7 @@ export class ObjectMetadataRelationService {
type: FieldMetadataType.RELATION, type: FieldMetadataType.RELATION,
name: createdObjectMetadata.nameSingular, name: createdObjectMetadata.nameSingular,
label: createdObjectMetadata.labelSingular, label: createdObjectMetadata.labelSingular,
description: `${capitalize(relatedObjectMetadata.nameSingular)} ${createdObjectMetadata.labelSingular}`, description,
icon: 'IconBuildingSkyscraper', icon: 'IconBuildingSkyscraper',
isNullable: true, isNullable: true,
}; };

View File

@ -0,0 +1,13 @@
import { capitalize } from 'src/utils/capitalize';
export const buildDescriptionForRelationFieldMetadataOnFromField = ({
relationObjectMetadataNamePlural,
targetObjectLabelSingular,
}: {
relationObjectMetadataNamePlural: string;
targetObjectLabelSingular: string;
}) => {
const description = `${capitalize(relationObjectMetadataNamePlural)} tied to the ${targetObjectLabelSingular}`;
return { description };
};

View File

@ -0,0 +1,13 @@
import { capitalize } from 'src/utils/capitalize';
export const buildDescriptionForRelationFieldMetadataOnToField = ({
relationObjectMetadataNamePlural,
targetObjectLabelSingular,
}: {
relationObjectMetadataNamePlural: string;
targetObjectLabelSingular: string;
}) => {
const description = `${capitalize(relationObjectMetadataNamePlural)} ${targetObjectLabelSingular}`;
return { description };
};

View File

@ -0,0 +1,15 @@
export const buildNameLabelAndDescriptionForForeignKeyFieldMetadata = ({
targetObjectNameSingular,
targetObjectLabelSingular,
relatedObjectLabelSingular,
}: {
targetObjectNameSingular: string;
targetObjectLabelSingular: string;
relatedObjectLabelSingular: string;
}) => {
const name = `${targetObjectNameSingular}Id`;
const label = `${targetObjectLabelSingular} ID (foreign key)`;
const description = `${relatedObjectLabelSingular} ${targetObjectLabelSingular} id foreign key`;
return { name, label, description };
};

View File

@ -332,6 +332,7 @@ export class RelationMetadataService extends TypeOrmQueryService<RelationMetadat
type: FieldMetadataType.UUID, type: FieldMetadataType.UUID,
objectMetadataId: relationMetadataInput.toObjectMetadataId, objectMetadataId: relationMetadataInput.toObjectMetadataId,
workspaceId: relationMetadataInput.workspaceId, workspaceId: relationMetadataInput.workspaceId,
settings: { isForeignKey: true },
}; };
} }