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:
@ -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: {
|
||||
|
||||
@ -0,0 +1,148 @@
|
||||
import { findManyFieldsMetadataQueryFactory } from 'test/integration/metadata/suites/field-metadata/utils/find-many-fields-metadata-query-factory.util';
|
||||
import { createMorphRelationBetweenObjects } from 'test/integration/metadata/suites/object-metadata/utils/create-morph-relation-between-objects.util';
|
||||
import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
|
||||
import { forceCreateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/force-create-one-object-metadata.util';
|
||||
import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
|
||||
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||
|
||||
import { RelationDTO } from 'src/engine/metadata-modules/field-metadata/dtos/relation.dto';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
|
||||
describe('Rename an object metadata with morph relation should succeed', () => {
|
||||
let opportunityId = '';
|
||||
let personId = '';
|
||||
let companyId = '';
|
||||
let morphRelationField: FieldMetadataEntity<FieldMetadataType.MORPH_RELATION> & {
|
||||
morphRelations: RelationDTO[];
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const {
|
||||
data: {
|
||||
createOneObject: { id: aId },
|
||||
},
|
||||
} = await forceCreateOneObjectMetadata({
|
||||
input: {
|
||||
nameSingular: 'opportunityForRename',
|
||||
namePlural: 'opportunitiesForRename',
|
||||
labelSingular: 'Opportunity For Rename',
|
||||
labelPlural: 'Opportunities For Rename',
|
||||
icon: 'IconOpportunity',
|
||||
},
|
||||
});
|
||||
|
||||
opportunityId = aId;
|
||||
const {
|
||||
data: {
|
||||
createOneObject: { id: bId },
|
||||
},
|
||||
} = await forceCreateOneObjectMetadata({
|
||||
input: {
|
||||
nameSingular: 'personForRename',
|
||||
namePlural: 'peopleForRename',
|
||||
labelSingular: 'Person For Rename',
|
||||
labelPlural: 'People For Rename',
|
||||
icon: 'IconPerson',
|
||||
},
|
||||
});
|
||||
|
||||
personId = bId;
|
||||
const {
|
||||
data: {
|
||||
createOneObject: { id: cId },
|
||||
},
|
||||
} = await forceCreateOneObjectMetadata({
|
||||
input: {
|
||||
nameSingular: 'companyForRename',
|
||||
namePlural: 'companiesForRename',
|
||||
labelSingular: 'Company For Rename',
|
||||
labelPlural: 'Companies For Rename',
|
||||
icon: 'IconCompany',
|
||||
},
|
||||
});
|
||||
|
||||
companyId = cId;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await deleteOneObjectMetadata({ input: { idToDelete: opportunityId } });
|
||||
await deleteOneObjectMetadata({ input: { idToDelete: personId } });
|
||||
await deleteOneObjectMetadata({ input: { idToDelete: companyId } });
|
||||
});
|
||||
|
||||
it('should rename custom object, and update the join column name of the morph relation that contains the object name', async () => {
|
||||
morphRelationField = await createMorphRelationBetweenObjects({
|
||||
name: 'owner',
|
||||
objectMetadataId: opportunityId,
|
||||
firstTargetObjectMetadataId: personId,
|
||||
secondTargetObjectMetadataId: companyId,
|
||||
type: FieldMetadataType.MORPH_RELATION,
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
});
|
||||
|
||||
const { data } = await updateOneObjectMetadata({
|
||||
gqlFields: `
|
||||
nameSingular
|
||||
labelSingular
|
||||
namePlural
|
||||
labelPlural
|
||||
`,
|
||||
input: {
|
||||
idToUpdate: personId,
|
||||
updatePayload: {
|
||||
nameSingular: 'personForRename2',
|
||||
namePlural: 'peopleForRename2',
|
||||
labelSingular: 'Person For Rename2',
|
||||
labelPlural: 'People For Rename2',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(data.updateOneObject.nameSingular).toBe('personForRename2');
|
||||
|
||||
const ownerFieldMetadataOnPersonId = morphRelationField.morphRelations.find(
|
||||
(morphRelation) => morphRelation.targetObjectMetadata.id === personId,
|
||||
)?.sourceFieldMetadata.id;
|
||||
|
||||
if (!ownerFieldMetadataOnPersonId) {
|
||||
throw new Error(
|
||||
'Morph Relation Error: Owner field metadata on person not found',
|
||||
);
|
||||
}
|
||||
|
||||
const fieldAfterRenaming = await findFieldMetadata({
|
||||
fieldMetadataId: ownerFieldMetadataOnPersonId,
|
||||
});
|
||||
|
||||
expect(fieldAfterRenaming.settings.joinColumnName).toBe(
|
||||
'ownerPersonForRename2Id',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
const findFieldMetadata = async ({
|
||||
fieldMetadataId,
|
||||
}: {
|
||||
fieldMetadataId: string;
|
||||
}) => {
|
||||
const operation = findManyFieldsMetadataQueryFactory({
|
||||
gqlFields: `
|
||||
id
|
||||
name
|
||||
object { id nameSingular }
|
||||
relation { type targetFieldMetadata { id } targetObjectMetadata { id } }
|
||||
settings
|
||||
`,
|
||||
input: {
|
||||
filter: { id: { eq: fieldMetadataId } },
|
||||
paging: { first: 1 },
|
||||
},
|
||||
});
|
||||
const fields = await makeMetadataAPIRequest(operation);
|
||||
const field = fields.body.data.fields.edges?.[0]?.node;
|
||||
|
||||
return field;
|
||||
};
|
||||
@ -74,6 +74,12 @@ export const createMorphRelationBetweenObjects = async ({
|
||||
targetObjectMetadata {
|
||||
id
|
||||
}
|
||||
sourceFieldMetadata {
|
||||
id
|
||||
}
|
||||
sourceObjectMetadata {
|
||||
id
|
||||
}
|
||||
}
|
||||
`,
|
||||
expectToFail: false,
|
||||
|
||||
Reference in New Issue
Block a user