Update Name of relation fieldMetadata (#15749)

UpdateOne of a Relation that involves a CustomObject, because the
nameSingular needs to be updated in the fieldMetadata
- nameSingular and namePlural must be provided since they are necessary
for morph name computation
  - label sync should be false


Interesting files to look at:

-
packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-default-relation-flat-field-metadatas-for-custom-object.util.ts

- UPDATE =>
packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/rename-related-morph-field-on-object-names-update.util.ts
( also update relation indexes ) needs v2 refactor to handle field
relation name update
- CREATE =>
packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-default-relation-flat-field-metadatas-for-custom-object.util.ts
( handle morph instead of previous classic relation )
- DELETE => DONE

Edit:
closes https://github.com/twentyhq/core-team-issues/issues/1897

---------

Co-authored-by: prastoin <paul@twenty.com>
This commit is contained in:
Guillim
2025-11-23 21:45:30 +01:00
committed by GitHub
parent 6203b7b3e6
commit 4848bc03f3
24 changed files with 1282 additions and 405 deletions
@@ -0,0 +1,26 @@
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
export const FLAT_FIELD_METADATA_MORPH_RELATION_EDITABLE_PROPERTIES_ON_SIBLING_MORPH_RELATION_UPDATE_CONSTANT =
{
custom: [
'defaultValue',
'description',
'icon',
'isActive',
'isLabelSyncedWithName',
'isUnique',
'label',
'options',
],
standard: [
'defaultValue',
'description',
'icon',
'isActive',
'label',
'options',
],
} as const satisfies Record<
'standard' | 'custom',
(keyof FlatFieldMetadata)[]
>;
@@ -0,0 +1,4 @@
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
export const FLAT_FIELD_METADATA_RELATION_EDITABLE_PROPERTIES_ON_SIBLING_MORPH_OR_RELATION_UPDATE_CONSTANT =
['isActive'] as const satisfies (keyof FlatFieldMetadata)[];
@@ -6,4 +6,6 @@ export const FLAT_FIELD_METADATA_RELATION_PROPERTIES_TO_COMPARE = [
'isActive',
'standardOverrides',
'icon',
'name',
'settings',
] as const satisfies (typeof ALL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY.fieldMetadata.propertiesToCompare)[number][];
@@ -33,11 +33,14 @@ export const computeFlatFieldMetadataRelatedFlatFieldMetadata = ({
FieldMetadataType.MORPH_RELATION,
)
) {
return findFlatFieldMetadatasRelatedToMorphRelationOrThrow({
flatFieldMetadata,
flatFieldMetadataMaps,
flatObjectMetadata,
});
const { morphRelationFlatFieldMetadatas, relationFlatFieldMetadatas } =
findFlatFieldMetadatasRelatedToMorphRelationOrThrow({
flatFieldMetadata,
flatFieldMetadataMaps,
flatObjectMetadata,
});
return [...morphRelationFlatFieldMetadatas, ...relationFlatFieldMetadatas];
}
return [];
@@ -0,0 +1,159 @@
import { FieldMetadataType, type FromTo } from 'twenty-shared/types';
import { type UpdateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/update-field.input';
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { FLAT_FIELD_METADATA_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-editable-properties.constant';
import { FLAT_FIELD_METADATA_MORPH_RELATION_EDITABLE_PROPERTIES_ON_SIBLING_MORPH_RELATION_UPDATE_CONSTANT } from 'src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-morph-relation-editable-properties-on-sibling-morph-relation-update.constant';
import { FLAT_FIELD_METADATA_RELATION_EDITABLE_PROPERTIES_ON_SIBLING_MORPH_OR_RELATION_UPDATE_CONSTANT } from 'src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-relation-editable-properties-on-sibling-morph-or-relation-update.constant';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { findFlatFieldMetadatasRelatedToMorphRelationOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-flat-field-metadatas-related-to-morph-relation-or-throw.util';
import { findRelationFlatFieldMetadataTargetFlatFieldMetadataOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-relation-flat-field-metadatas-target-flat-field-metadata-or-throw.util';
import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util';
import { sanitizeRawUpdateFieldInput } from 'src/engine/metadata-modules/flat-field-metadata/utils/sanitize-raw-update-field-input';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { isStandardMetadata } from 'src/engine/metadata-modules/utils/is-standard-metadata.util';
import { mergeUpdateInExistingRecord } from 'src/utils/merge-update-in-existing-record.util';
type ComputeFlatFieldToUpdateAndRelatedFlatFieldToUpdateReturnType = {
flatFieldMetadataFromTo: FromTo<FlatFieldMetadata, 'flatFieldMetadata'>;
relatedFlatFieldMetadatasFromTo: FromTo<
FlatFieldMetadata,
'flatFieldMetadata'
>[];
};
type ComputeFlatFieldToUpdateAndRelatedFlatFieldToUpdateArgs = {
rawUpdateFieldInput: UpdateFieldInput;
fromFlatFieldMetadata: FlatFieldMetadata;
flatObjectMetadata: FlatObjectMetadata;
} & Pick<AllFlatEntityMaps, 'flatFieldMetadataMaps'>;
// Note: Standard override is way too complex we should land a smoother implemenentation once we standardize
// them across every flat entities
export const computeFlatFieldToUpdateAndRelatedFlatFieldToUpdate = ({
fromFlatFieldMetadata,
rawUpdateFieldInput,
flatFieldMetadataMaps,
flatObjectMetadata,
}: ComputeFlatFieldToUpdateAndRelatedFlatFieldToUpdateArgs): ComputeFlatFieldToUpdateAndRelatedFlatFieldToUpdateReturnType => {
const { standardOverrides, updatedEditableFieldProperties } =
sanitizeRawUpdateFieldInput({
existingFlatFieldMetadata: fromFlatFieldMetadata,
rawUpdateFieldInput,
});
const isStandardField = isStandardMetadata(fromFlatFieldMetadata);
const toFlatFieldMetadata = {
...mergeUpdateInExistingRecord({
existing: fromFlatFieldMetadata,
properties:
FLAT_FIELD_METADATA_EDITABLE_PROPERTIES[
isStandardField ? 'standard' : 'custom'
],
update: updatedEditableFieldProperties,
}),
standardOverrides,
};
if (
isFlatFieldMetadataOfType(fromFlatFieldMetadata, FieldMetadataType.RELATION)
) {
const relatedFlatFieldMetadataFrom =
findRelationFlatFieldMetadataTargetFlatFieldMetadataOrThrow({
flatFieldMetadata: fromFlatFieldMetadata,
flatFieldMetadataMaps,
});
const relatedFlatFieldMetadataTo = mergeUpdateInExistingRecord({
existing: relatedFlatFieldMetadataFrom as FlatFieldMetadata,
properties:
FLAT_FIELD_METADATA_RELATION_EDITABLE_PROPERTIES_ON_SIBLING_MORPH_OR_RELATION_UPDATE_CONSTANT,
update: updatedEditableFieldProperties,
});
return {
flatFieldMetadataFromTo: {
fromFlatFieldMetadata,
toFlatFieldMetadata,
},
relatedFlatFieldMetadatasFromTo: [
{
fromFlatFieldMetadata: relatedFlatFieldMetadataFrom,
toFlatFieldMetadata: relatedFlatFieldMetadataTo,
},
],
};
}
if (
isFlatFieldMetadataOfType(
fromFlatFieldMetadata,
FieldMetadataType.MORPH_RELATION,
)
) {
const { morphRelationFlatFieldMetadatas, relationFlatFieldMetadatas } =
findFlatFieldMetadatasRelatedToMorphRelationOrThrow({
flatFieldMetadata: fromFlatFieldMetadata,
flatFieldMetadataMaps,
flatObjectMetadata,
});
const relatedMorphRelationFlatFieldMetdataTo =
morphRelationFlatFieldMetadatas.map<
FromTo<FlatFieldMetadata, 'flatFieldMetadata'>
>((relatedFlatFieldMetadataFrom) => {
const relatedMorphPropertiesToUpdateTo =
FLAT_FIELD_METADATA_MORPH_RELATION_EDITABLE_PROPERTIES_ON_SIBLING_MORPH_RELATION_UPDATE_CONSTANT[
isStandardField ? 'standard' : 'custom'
];
const relatedFlatFieldMetadataTo = {
...mergeUpdateInExistingRecord({
existing: relatedFlatFieldMetadataFrom as FlatFieldMetadata,
properties: relatedMorphPropertiesToUpdateTo,
update: updatedEditableFieldProperties,
}),
standardOverrides,
};
return {
fromFlatFieldMetadata: relatedFlatFieldMetadataFrom,
toFlatFieldMetadata: relatedFlatFieldMetadataTo,
};
});
const relatedRelationFlatFieldMetadataTo = relationFlatFieldMetadatas.map<
FromTo<FlatFieldMetadata, 'flatFieldMetadata'>
>((relatedFlatFieldMetadataFrom) => {
const relatedFlatFieldMetadataTo = mergeUpdateInExistingRecord({
existing: relatedFlatFieldMetadataFrom as FlatFieldMetadata,
properties:
FLAT_FIELD_METADATA_RELATION_EDITABLE_PROPERTIES_ON_SIBLING_MORPH_OR_RELATION_UPDATE_CONSTANT,
update: updatedEditableFieldProperties,
});
return {
fromFlatFieldMetadata: relatedFlatFieldMetadataFrom,
toFlatFieldMetadata: relatedFlatFieldMetadataTo,
};
});
return {
flatFieldMetadataFromTo: {
fromFlatFieldMetadata,
toFlatFieldMetadata,
},
relatedFlatFieldMetadatasFromTo: [
...relatedMorphRelationFlatFieldMetdataTo,
...relatedRelationFlatFieldMetadataTo,
],
};
}
return {
flatFieldMetadataFromTo: {
fromFlatFieldMetadata,
toFlatFieldMetadata,
},
relatedFlatFieldMetadatasFromTo: [],
};
};
@@ -0,0 +1,26 @@
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
export const findFieldRelatedIndexes = ({
flatFieldMetadata,
flatObjectMetadata,
flatIndexMaps,
}: {
flatFieldMetadata: FlatFieldMetadata;
flatObjectMetadata: FlatObjectMetadata;
flatIndexMaps: FlatEntityMaps<FlatIndexMetadata>;
}): FlatIndexMetadata[] => {
const objectIndexes = findManyFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityMaps: flatIndexMaps,
flatEntityIds: flatObjectMetadata.indexMetadataIds,
});
return objectIndexes.filter((index) =>
index.flatIndexFieldMetadatas.some(
(indexField) => indexField.fieldMetadataId === flatFieldMetadata.id,
),
);
};
@@ -1,6 +1,5 @@
import { type FieldMetadataType } from 'twenty-shared/types';
import { type MorphOrRelationFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/types/morph-or-relation-field-metadata-type.type';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { findAllOthersMorphRelationFlatFieldMetadatasOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-all-others-morph-relation-flat-field-metadatas-or-throw.util';
@@ -12,32 +11,35 @@ export type FindFlatFieldMetadatasRelatedToMorphRelationOrThrowArgs = {
flatFieldMetadata: FlatFieldMetadata<FieldMetadataType.MORPH_RELATION>;
flatObjectMetadata: FlatObjectMetadata;
};
type FindFlatFieldMetadatasRelatedToMorphRelationOrThrowReturnType = {
morphRelationFlatFieldMetadatas: FlatFieldMetadata<FieldMetadataType.MORPH_RELATION>[];
relationFlatFieldMetadatas: FlatFieldMetadata<FieldMetadataType.RELATION>[];
};
export const findFlatFieldMetadatasRelatedToMorphRelationOrThrow = ({
flatFieldMetadataMaps,
flatFieldMetadata: morphRelationFlatFieldMetadata,
flatObjectMetadata,
}: FindFlatFieldMetadatasRelatedToMorphRelationOrThrowArgs): FlatFieldMetadata<MorphOrRelationFieldMetadataType>[] => {
const allMorphFlatFieldMetadatas =
}: FindFlatFieldMetadatasRelatedToMorphRelationOrThrowArgs): FindFlatFieldMetadatasRelatedToMorphRelationOrThrowReturnType => {
const morphRelationFlatFieldMetadatas =
findAllOthersMorphRelationFlatFieldMetadatasOrThrow({
flatFieldMetadata: morphRelationFlatFieldMetadata,
flatFieldMetadataMaps,
flatObjectMetadata,
});
return [
const relationFlatFieldMetadatas = [
morphRelationFlatFieldMetadata,
...allMorphFlatFieldMetadatas,
].flatMap((flatFieldMetadata) => {
const relationTargetFlatFieldMetadata =
findRelationFlatFieldMetadataTargetFlatFieldMetadataOrThrow({
flatFieldMetadata,
flatFieldMetadataMaps,
});
...morphRelationFlatFieldMetadatas,
].map((flatFieldMetadata) =>
findRelationFlatFieldMetadataTargetFlatFieldMetadataOrThrow({
flatFieldMetadata,
flatFieldMetadataMaps,
}),
) as FlatFieldMetadata<FieldMetadataType.RELATION>[];
if (flatFieldMetadata.id === morphRelationFlatFieldMetadata.id) {
return [relationTargetFlatFieldMetadata];
}
return [flatFieldMetadata, relationTargetFlatFieldMetadata];
});
return {
morphRelationFlatFieldMetadatas,
relationFlatFieldMetadatas,
};
};
@@ -3,7 +3,6 @@ import {
extractAndSanitizeObjectStringFields,
isDefined,
} from 'twenty-shared/utils';
import { v4 } from 'uuid';
import { type UpdateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/update-field.input';
import {
@@ -12,18 +11,14 @@ import {
} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { FLAT_FIELD_METADATA_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-editable-properties.constant';
import { type FieldInputTranspilationResult } from 'src/engine/metadata-modules/flat-field-metadata/types/field-input-transpilation-result.type';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { computeFlatFieldMetadataRelatedFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/compute-flat-field-metadata-related-flat-field-metadata.util';
import { computeFlatFieldToUpdateAndRelatedFlatFieldToUpdate } from 'src/engine/metadata-modules/flat-field-metadata/utils/compute-flat-field-to-update-and-related-flat-field-to-update.util';
import {
FLAT_FIELD_METADATA_UPDATE_EMPTY_SIDE_EFFECTS,
type FlatFieldMetadataUpdateSideEffects,
handleFlatFieldMetadataUpdateSideEffect,
} from 'src/engine/metadata-modules/flat-field-metadata/utils/handle-flat-field-metadata-update-side-effect.util';
import { sanitizeRawUpdateFieldInput } from 'src/engine/metadata-modules/flat-field-metadata/utils/sanitize-raw-update-field-input';
import { isStandardMetadata } from 'src/engine/metadata-modules/utils/is-standard-metadata.util';
import { mergeUpdateInExistingRecord } from 'src/utils/merge-update-in-existing-record.util';
type FromUpdateFieldInputToFlatFieldMetadataArgs = {
updateFieldInput: UpdateFieldInput;
@@ -73,13 +68,6 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({
};
}
const isStandardField = isStandardMetadata(existingFlatFieldMetadataToUpdate);
const { standardOverrides, updatedEditableFieldProperties } =
sanitizeRawUpdateFieldInput({
existingFlatFieldMetadata: existingFlatFieldMetadataToUpdate,
rawUpdateFieldInput,
});
const flatObjectMetadata = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: existingFlatFieldMetadataToUpdate.objectMetadataId,
flatEntityMaps: existingFlatObjectMetadataMaps,
@@ -92,124 +80,100 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({
);
}
const relatedFlatFieldMetadatasToUpdate =
computeFlatFieldMetadataRelatedFlatFieldMetadata({
flatFieldMetadata: existingFlatFieldMetadataToUpdate,
const { flatFieldMetadataFromTo, relatedFlatFieldMetadatasFromTo } =
computeFlatFieldToUpdateAndRelatedFlatFieldToUpdate({
flatFieldMetadataMaps,
flatObjectMetadata,
fromFlatFieldMetadata: existingFlatFieldMetadataToUpdate,
rawUpdateFieldInput,
});
const flatFieldMetadatasToUpdate = [
existingFlatFieldMetadataToUpdate,
...relatedFlatFieldMetadatasToUpdate,
];
const initialAccumulator: FlatFieldMetadataAndIndexToUpdate = {
...structuredClone(FLAT_FIELD_METADATA_UPDATE_EMPTY_SIDE_EFFECTS),
flatFieldMetadatasToUpdate: [],
};
updatedEditableFieldProperties.options = !isDefined(
updatedEditableFieldProperties.options,
)
? updatedEditableFieldProperties.options
: updatedEditableFieldProperties.options.map((option) => ({
id: v4(),
...option,
}));
const optimisticiallyUpdatedFlatFieldMetadatas = [
flatFieldMetadataFromTo,
...relatedFlatFieldMetadatasFromTo,
].reduce<FlatFieldMetadataAndIndexToUpdate>(
(accumulator, { fromFlatFieldMetadata, toFlatFieldMetadata }) => {
const {
flatViewGroupsToCreate,
flatViewGroupsToDelete,
flatViewGroupsToUpdate,
flatIndexMetadatasToUpdate,
flatViewFiltersToDelete,
flatViewFiltersToUpdate,
flatIndexMetadatasToCreate,
flatIndexMetadatasToDelete,
flatViewsToDelete,
flatViewFieldsToDelete,
flatViewsToUpdate,
} = handleFlatFieldMetadataUpdateSideEffect({
flatViewFilterMaps,
flatViewGroupMaps,
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
fromFlatFieldMetadata,
flatFieldMetadataMaps,
flatIndexMaps,
toFlatFieldMetadata,
flatViewMaps,
flatViewFieldMaps,
});
const optimisticiallyUpdatedFlatFieldMetadatas =
flatFieldMetadatasToUpdate.reduce<FlatFieldMetadataAndIndexToUpdate>(
(accumulator, fromFlatFieldMetadata) => {
const toFlatFieldMetadata = {
...mergeUpdateInExistingRecord({
existing: fromFlatFieldMetadata,
properties:
FLAT_FIELD_METADATA_EDITABLE_PROPERTIES[
isStandardField ? 'standard' : 'custom'
],
update: updatedEditableFieldProperties,
}),
standardOverrides,
};
const {
flatViewGroupsToCreate,
flatViewGroupsToDelete,
flatViewGroupsToUpdate,
flatIndexMetadatasToUpdate,
flatViewFiltersToDelete,
flatViewFiltersToUpdate,
flatIndexMetadatasToCreate,
flatIndexMetadatasToDelete,
flatViewsToDelete,
flatViewFieldsToDelete,
flatViewsToUpdate,
} = handleFlatFieldMetadataUpdateSideEffect({
flatViewFilterMaps,
flatViewGroupMaps,
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
fromFlatFieldMetadata,
flatFieldMetadataMaps,
flatIndexMaps,
return {
flatFieldMetadatasToUpdate: [
...accumulator.flatFieldMetadatasToUpdate,
toFlatFieldMetadata,
flatViewMaps,
flatViewFieldMaps,
});
return {
flatFieldMetadatasToUpdate: [
...accumulator.flatFieldMetadatasToUpdate,
toFlatFieldMetadata,
],
flatIndexMetadatasToUpdate: [
...accumulator.flatIndexMetadatasToUpdate,
...flatIndexMetadatasToUpdate,
],
flatViewFiltersToDelete: [
...accumulator.flatViewFiltersToDelete,
...flatViewFiltersToDelete,
],
flatViewFiltersToUpdate: [
...accumulator.flatViewFiltersToUpdate,
...flatViewFiltersToUpdate,
],
flatViewGroupsToCreate: [
...accumulator.flatViewGroupsToCreate,
...flatViewGroupsToCreate,
],
flatViewGroupsToDelete: [
...accumulator.flatViewGroupsToDelete,
...flatViewGroupsToDelete,
],
flatViewGroupsToUpdate: [
...accumulator.flatViewGroupsToUpdate,
...flatViewGroupsToUpdate,
],
flatIndexMetadatasToDelete: [
...accumulator.flatIndexMetadatasToDelete,
...flatIndexMetadatasToDelete,
],
flatIndexMetadatasToCreate: [
...accumulator.flatIndexMetadatasToCreate,
...flatIndexMetadatasToCreate,
],
flatViewsToDelete: [
...accumulator.flatViewsToDelete,
...flatViewsToDelete,
],
flatViewFieldsToDelete: [
...accumulator.flatViewFieldsToDelete,
...flatViewFieldsToDelete,
],
flatViewsToUpdate: [
...accumulator.flatViewsToUpdate,
...flatViewsToUpdate,
],
};
},
initialAccumulator,
);
],
flatIndexMetadatasToUpdate: [
...accumulator.flatIndexMetadatasToUpdate,
...flatIndexMetadatasToUpdate,
],
flatViewFiltersToDelete: [
...accumulator.flatViewFiltersToDelete,
...flatViewFiltersToDelete,
],
flatViewFiltersToUpdate: [
...accumulator.flatViewFiltersToUpdate,
...flatViewFiltersToUpdate,
],
flatViewGroupsToCreate: [
...accumulator.flatViewGroupsToCreate,
...flatViewGroupsToCreate,
],
flatViewGroupsToDelete: [
...accumulator.flatViewGroupsToDelete,
...flatViewGroupsToDelete,
],
flatViewGroupsToUpdate: [
...accumulator.flatViewGroupsToUpdate,
...flatViewGroupsToUpdate,
],
flatIndexMetadatasToDelete: [
...accumulator.flatIndexMetadatasToDelete,
...flatIndexMetadatasToDelete,
],
flatIndexMetadatasToCreate: [
...accumulator.flatIndexMetadatasToCreate,
...flatIndexMetadatasToCreate,
],
flatViewsToDelete: [
...accumulator.flatViewsToDelete,
...flatViewsToDelete,
],
flatViewFieldsToDelete: [
...accumulator.flatViewFieldsToDelete,
...flatViewFieldsToDelete,
],
flatViewsToUpdate: [
...accumulator.flatViewsToUpdate,
...flatViewsToUpdate,
],
};
},
initialAccumulator,
);
return {
status: 'success',
@@ -1,11 +1,11 @@
import { type FromTo } from 'twenty-shared/types';
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { findFieldRelatedIndexes } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-field-related-index.util';
import { generateIndexForFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/generate-index-for-flat-field-metadata.util';
import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util';
import { recomputeIndexOnFlatFieldMetadataNameUpdate } from 'src/engine/metadata-modules/flat-field-metadata/utils/recompute-index-on-flat-field-metadata-name-update.util';
import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
@@ -52,7 +52,7 @@ export const handleIndexChangesDuringFieldUpdate = ({
flatEntityId: fromFlatFieldMetadata.objectMetadataId,
});
const relatedIndexes = findRelatedIndexes({
const relatedIndexes = findFieldRelatedIndexes({
flatFieldMetadata: fromFlatFieldMetadata,
flatObjectMetadata,
flatIndexMaps,
@@ -81,27 +81,6 @@ const hasIndexRelevantChanges = ({
fromFlatFieldMetadata.name !== toFlatFieldMetadata.name ||
fromFlatFieldMetadata.isUnique !== toFlatFieldMetadata.isUnique;
const findRelatedIndexes = ({
flatFieldMetadata,
flatObjectMetadata,
flatIndexMaps,
}: {
flatFieldMetadata: FlatFieldMetadata;
flatObjectMetadata: FlatObjectMetadata;
flatIndexMaps: FlatEntityMaps<FlatIndexMetadata>;
}): FlatIndexMetadata[] => {
const objectIndexes = findManyFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityMaps: flatIndexMaps,
flatEntityIds: flatObjectMetadata.indexMetadataIds,
});
return objectIndexes.filter((index) =>
index.flatIndexFieldMetadatas.some(
(indexField) => indexField.fieldMetadataId === flatFieldMetadata.id,
),
);
};
const handleNoExistingIndexes = ({
toFlatFieldMetadata,
flatObjectMetadata,
@@ -139,7 +118,10 @@ const handleExistingIndexes = ({
FlatFieldMetadata,
'flatFieldMetadata'
>): FieldMetadataUpdateIndexSideEffect => {
if (toFlatFieldMetadata.isUnique === false) {
if (
toFlatFieldMetadata.isUnique === false &&
!isMorphOrRelationFlatFieldMetadata(fromFlatFieldMetadata)
) {
const expectedUniqueIndex = generateIndexForFlatFieldMetadata({
flatFieldMetadata: {
...fromFlatFieldMetadata,
@@ -160,7 +142,6 @@ const handleExistingIndexes = ({
: [],
};
}
const updatedIndexes = recomputeIndexOnFlatFieldMetadataNameUpdate({
flatFieldMetadataMaps,
flatObjectMetadata,
@@ -42,13 +42,11 @@ export const recomputeIndexOnFlatFieldMetadataNameUpdate = ({
},
);
return relatedFlatIndexMetadata.map<FlatIndexMetadata>((flatIndex) => {
const newIndex = generateFlatIndexMetadataWithNameOrThrow({
return relatedFlatIndexMetadata.map((flatIndex) =>
generateFlatIndexMetadataWithNameOrThrow({
flatIndex,
flatObjectMetadata,
objectFlatFieldMetadatas: optimisticObjectFlatFieldMetadatas,
});
return newIndex;
});
}),
);
};
@@ -2,6 +2,7 @@ import {
extractAndSanitizeObjectStringFields,
isDefined,
} from 'twenty-shared/utils';
import { v4 } from 'uuid';
import { FIELD_METADATA_STANDARD_OVERRIDES_PROPERTIES } from 'src/engine/metadata-modules/field-metadata/constants/field-metadata-standard-overrides-properties.constant';
import { type UpdateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/update-field.input';
@@ -33,6 +34,15 @@ export const sanitizeRawUpdateFieldInput = ({
],
);
updatedEditableFieldProperties.options = !isDefined(
updatedEditableFieldProperties.options,
)
? updatedEditableFieldProperties.options
: updatedEditableFieldProperties.options.map((option) => ({
id: v4(),
...option,
}));
if (!isStandardField) {
return {
updatedEditableFieldProperties,
@@ -88,6 +88,7 @@ export const fromUpdateObjectInputToFlatObjectMetadataAndRelatedFlatEntities =
fromFlatObjectMetadata: existingFlatObjectMetadata,
toFlatObjectMetadata,
flatFieldMetadataMaps,
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
flatIndexMaps,
flatViewFieldMaps,
flatViewMaps,
@@ -24,6 +24,7 @@ type HandleFlatObjectMetadataUpdateSideEffectArgs = FromTo<
Pick<
AllFlatEntityMaps,
| 'flatFieldMetadataMaps'
| 'flatObjectMetadataMaps'
| 'flatViewFieldMaps'
| 'flatIndexMaps'
| 'flatViewMaps'
@@ -32,20 +33,26 @@ type HandleFlatObjectMetadataUpdateSideEffectArgs = FromTo<
export const handleFlatObjectMetadataUpdateSideEffect = ({
flatIndexMaps,
flatFieldMetadataMaps,
flatObjectMetadataMaps,
flatViewFieldMaps,
flatViewMaps,
fromFlatObjectMetadata,
toFlatObjectMetadata,
}: HandleFlatObjectMetadataUpdateSideEffectArgs): FlatObjectMetadataUpdateSideEffects => {
const otherObjectFlatFieldMetadatasToUpdate =
const { morphRelatedFlatIndexesToUpdate, morphFlatFieldMetadatasToUpdate } =
fromFlatObjectMetadata.nameSingular !== toFlatObjectMetadata.nameSingular ||
fromFlatObjectMetadata.namePlural !== toFlatObjectMetadata.namePlural
? renameRelatedMorphFieldOnObjectNamesUpdate({
flatFieldMetadataMaps,
fromFlatObjectMetadata,
toFlatObjectMetadata,
flatObjectMetadataMaps,
flatIndexMaps,
})
: [];
: {
morphRelatedFlatIndexesToUpdate: [],
morphFlatFieldMetadatasToUpdate: [],
};
const flatIndexMetadatasToUpdate =
fromFlatObjectMetadata.nameSingular !== toFlatObjectMetadata.nameSingular
@@ -75,9 +82,12 @@ export const handleFlatObjectMetadataUpdateSideEffect = ({
};
return {
flatIndexMetadatasToUpdate,
flatIndexMetadatasToUpdate: [
...morphRelatedFlatIndexesToUpdate,
...flatIndexMetadatasToUpdate,
],
flatViewFieldsToCreate,
flatViewFieldsToUpdate,
otherObjectFlatFieldMetadatasToUpdate,
otherObjectFlatFieldMetadatasToUpdate: morphFlatFieldMetadatasToUpdate,
};
};
@@ -1,5 +1,3 @@
import { isDefined } from 'twenty-shared/utils';
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
@@ -16,11 +14,11 @@ export const recomputeIndexAfterFlatObjectMetadataSingularNameUpdate = ({
updatedSingularName,
flatFieldMetadataMaps,
}: RecomputeIndexAfterFlatObjectMetadataSingularNameUpdateArgs): FlatIndexMetadata[] => {
const allRelatedFlatIndexMetadata = Object.values(flatIndexMaps.byId).filter(
(flatIndexMetadata): flatIndexMetadata is FlatIndexMetadata =>
isDefined(flatIndexMetadata) &&
flatIndexMetadata.objectMetadataId === existingFlatObjectMetadata.id,
);
const allRelatedFlatIndexMetadata =
findManyFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityIds: existingFlatObjectMetadata.indexMetadataIds,
flatEntityMaps: flatIndexMaps,
});
if (allRelatedFlatIndexMetadata.length === 0) {
return [];
@@ -7,69 +7,141 @@ import { computeMorphRelationFieldName } from 'twenty-shared/utils';
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { findFieldRelatedIndexes } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-field-related-index.util';
import { recomputeIndexOnFlatFieldMetadataNameUpdate } from 'src/engine/metadata-modules/flat-field-metadata/utils/recompute-index-on-flat-field-metadata-name-update.util';
import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { getFlatObjectMetadataTargetMorphRelationFlatFieldMetadatasOrThrow } from 'src/engine/metadata-modules/flat-object-metadata/utils/get-flat-object-metadata-many-to-one-target-morph-relation-flat-field-metadatas-or-throw.util';
import { getMorphNameFromMorphFieldMetadataName } from 'src/engine/metadata-modules/flat-object-metadata/utils/get-morph-name-from-morph-field-metadata-name.util';
type UpdateMorphFlatFieldNameArgs = FromTo<
FlatObjectMetadata,
'relationTargetFlatObjectMetadata'
> & {
fromMorphFlatFieldMetadata: FlatFieldMetadata<FieldMetadataType.MORPH_RELATION>;
};
const updateMorphFlatFieldName = ({
fromMorphFlatFieldMetadata,
fromRelationTargetFlatObjectMetadata,
toRelationTargetFlatObjectMetadata,
}: UpdateMorphFlatFieldNameArgs): FlatFieldMetadata<FieldMetadataType.MORPH_RELATION> => {
const isManyToOneRelationType =
fromMorphFlatFieldMetadata.settings.relationType ===
RelationType.MANY_TO_ONE;
const initialMorphRelationFieldName = getMorphNameFromMorphFieldMetadataName({
morphRelationFlatFieldMetadata: fromMorphFlatFieldMetadata,
nameSingular: fromRelationTargetFlatObjectMetadata.nameSingular,
namePlural: fromRelationTargetFlatObjectMetadata.namePlural,
});
const newMorphFieldName = computeMorphRelationFieldName({
fieldName: initialMorphRelationFieldName,
relationType: fromMorphFlatFieldMetadata.settings.relationType,
targetObjectMetadataNameSingular:
toRelationTargetFlatObjectMetadata.nameSingular,
targetObjectMetadataNamePlural:
toRelationTargetFlatObjectMetadata.namePlural,
});
const newJoinColumnName = isManyToOneRelationType
? computeMorphOrRelationFieldJoinColumnName({
name: newMorphFieldName,
})
: undefined;
return {
...fromMorphFlatFieldMetadata,
name: newMorphFieldName,
settings: {
...fromMorphFlatFieldMetadata.settings,
joinColumnName: newJoinColumnName,
},
};
};
type RenameRelatedMorphFieldOnObjectNamesUpdateArgs = FromTo<
FlatObjectMetadata,
'flatObjectMetadata'
> &
Pick<AllFlatEntityMaps, 'flatFieldMetadataMaps'>;
// TODO We should recompute each index here too
Pick<
AllFlatEntityMaps,
'flatFieldMetadataMaps' | 'flatObjectMetadataMaps' | 'flatIndexMaps'
>;
type RenameRelatedMorphFieldOnObjectNamesUpdateReturnType = {
morphFlatFieldMetadatasToUpdate: FlatFieldMetadata<FieldMetadataType.MORPH_RELATION>[];
morphRelatedFlatIndexesToUpdate: FlatIndexMetadata[];
};
export const renameRelatedMorphFieldOnObjectNamesUpdate = ({
fromFlatObjectMetadata,
toFlatObjectMetadata,
flatFieldMetadataMaps,
}: RenameRelatedMorphFieldOnObjectNamesUpdateArgs): FlatFieldMetadata<FieldMetadataType.MORPH_RELATION>[] => {
flatObjectMetadataMaps,
flatIndexMaps,
}: RenameRelatedMorphFieldOnObjectNamesUpdateArgs): RenameRelatedMorphFieldOnObjectNamesUpdateReturnType => {
const objectFlatFieldMetadatas =
findManyFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityMaps: flatFieldMetadataMaps,
flatEntityIds: fromFlatObjectMetadata.fieldMetadataIds,
});
const manyToOneMorphRelationFlatFieldMetadatas =
const allMorphRelationFlatFieldMetadatas =
getFlatObjectMetadataTargetMorphRelationFlatFieldMetadatasOrThrow({
flatFieldMetadataMaps,
objectFlatFieldMetadatas,
});
const updatedFlatFieldMetadatas =
manyToOneMorphRelationFlatFieldMetadatas.map(
(morphRelationFlatFieldMetadata) => {
const isManyToOneRelationType =
morphRelationFlatFieldMetadata.settings.relationType ===
RelationType.MANY_TO_ONE;
const initialMorphRelationFieldName =
getMorphNameFromMorphFieldMetadataName({
morphRelationFlatFieldMetadata,
nameSingular: fromFlatObjectMetadata.nameSingular,
namePlural: fromFlatObjectMetadata.namePlural,
});
const initialAccumulator: RenameRelatedMorphFieldOnObjectNamesUpdateReturnType =
{
morphRelatedFlatIndexesToUpdate: [],
morphFlatFieldMetadatasToUpdate: [],
};
const newMorphFieldName = computeMorphRelationFieldName({
fieldName: initialMorphRelationFieldName,
relationType: morphRelationFlatFieldMetadata.settings.relationType,
targetObjectMetadataNameSingular: toFlatObjectMetadata.nameSingular,
targetObjectMetadataNamePlural: toFlatObjectMetadata.namePlural,
return allMorphRelationFlatFieldMetadatas.reduce(
(acc, fromMorphFlatFieldMetadata) => {
const morphFlatFieldMetadataTo = updateMorphFlatFieldName({
fromMorphFlatFieldMetadata,
fromRelationTargetFlatObjectMetadata: fromFlatObjectMetadata,
toRelationTargetFlatObjectMetadata: toFlatObjectMetadata,
});
const morphFieldParentFlatObject =
findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: fromMorphFlatFieldMetadata.objectMetadataId,
flatEntityMaps: flatObjectMetadataMaps,
});
const newJoinColumnName = isManyToOneRelationType
? computeMorphOrRelationFieldJoinColumnName({
name: newMorphFieldName,
})
: undefined;
const relatedIndexes = findFieldRelatedIndexes({
flatFieldMetadata: fromMorphFlatFieldMetadata,
flatObjectMetadata: morphFieldParentFlatObject,
flatIndexMaps,
});
return {
...morphRelationFlatFieldMetadata,
name: newMorphFieldName,
settings: {
...morphRelationFlatFieldMetadata.settings,
joinColumnName: newJoinColumnName,
},
};
},
);
const flatIndexesToUpdate = recomputeIndexOnFlatFieldMetadataNameUpdate({
flatFieldMetadataMaps,
flatObjectMetadata: morphFieldParentFlatObject,
fromFlatFieldMetadata: fromMorphFlatFieldMetadata,
toFlatFieldMetadata: {
name: morphFlatFieldMetadataTo.name,
isUnique: morphFlatFieldMetadataTo.isUnique,
},
relatedFlatIndexMetadata: relatedIndexes,
});
return updatedFlatFieldMetadatas;
return {
...acc,
morphRelatedFlatIndexesToUpdate: [
...acc.morphRelatedFlatIndexesToUpdate,
...flatIndexesToUpdate,
],
morphFlatFieldMetadatasToUpdate: [
...acc.morphFlatFieldMetadatasToUpdate,
morphFlatFieldMetadataTo,
],
};
},
initialAccumulator,
);
};
@@ -108,6 +108,7 @@ export class FlatFieldMetadataValidatorService {
});
}
// Should be moved in relation field validator
if (isMorphOrRelationFlatFieldMetadata(flatFieldMetadataToValidate)) {
const relationNonEditableUpdatedProperties = updates.flatMap(
({ property }) =>
@@ -126,6 +127,7 @@ export class FlatFieldMetadataValidatorService {
});
}
}
///
if (updates.some((update) => update.property === 'name')) {
validationResult.errors.push(
@@ -211,19 +211,26 @@ export class UpdateFieldActionHandlerService extends WorkspaceMigrationRunnerAct
optimisticFlatFieldMetadata.options = update.to ?? [];
}
if (
isMorphOrRelationFlatFieldMetadata(optimisticFlatFieldMetadata) &&
update.property === 'settings'
isPropertyUpdate(update, 'settings') &&
isDefined(update.from?.joinColumnName) &&
isDefined(update.to?.joinColumnName) &&
update.from.joinColumnName !== update.to.joinColumnName &&
isMorphOrRelationFlatFieldMetadata(optimisticFlatFieldMetadata)
) {
await this.handleMorphOrRelationSettingsUpdate({
flatFieldMetadata: optimisticFlatFieldMetadata,
await this.workspaceSchemaManagerService.columnManager.renameColumn({
queryRunner,
schemaName,
tableName,
update: update as PropertyUpdate<
FlatFieldMetadata<MorphOrRelationFieldMetadataType>,
'settings'
>,
oldColumnName: update.from.joinColumnName,
newColumnName: update.to.joinColumnName,
});
optimisticFlatFieldMetadata = {
...optimisticFlatFieldMetadata,
settings: {
...optimisticFlatFieldMetadata.settings,
joinColumnName: update.to.joinColumnName,
},
};
}
}
}
@@ -263,20 +270,6 @@ export class UpdateFieldActionHandlerService extends WorkspaceMigrationRunnerAct
newColumnName: toCompositeColumnName,
});
}
} else {
if (isMorphOrRelationFlatFieldMetadata(flatFieldMetadata)) {
throw new WorkspaceMigrationRunnerException(
'Relation field metadata name update is not supported yet',
WorkspaceMigrationRunnerExceptionCode.NOT_SUPPORTED,
);
}
await this.workspaceSchemaManagerService.columnManager.renameColumn({
queryRunner,
schemaName,
tableName,
oldColumnName: update.from,
newColumnName: update.to,
});
}
const enumOperations = collectEnumOperationsForField({
@@ -440,8 +433,8 @@ export class UpdateFieldActionHandlerService extends WorkspaceMigrationRunnerAct
}
await this.workspaceSchemaManagerService.columnManager.renameColumn({
newColumnName: fromJoinColumnName,
oldColumnName: toJoinColumnName,
oldColumnName: fromJoinColumnName,
newColumnName: toJoinColumnName,
queryRunner,
schemaName,
tableName,
@@ -0,0 +1,75 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`updateOne FieldMetadataService morph relation fields v2 It should update all morph related flat field metadata and their related field allowing its deletion 1`] = `
{
"description": [
"Description for all",
],
"isActive": [
true,
],
"label": [
"field label",
],
"name": [
"fieldNameOpportunityForMorphRelationSecond",
"fieldNamePersonForMorphRelationSecond",
],
}
`;
exports[`updateOne FieldMetadataService morph relation fields v2 It should update all morph related flat field metadata and their related field allowing its deletion 2`] = `
{
"description": [
null,
],
"isActive": [
true,
],
"label": [
"tata",
"toto",
],
"name": [
"tata",
"toto",
],
}
`;
exports[`updateOne FieldMetadataService morph relation fields v2 It should update all morph related flat field metadata and their related field allowing its deletion 3`] = `
{
"description": [
"new description",
],
"isActive": [
false,
],
"label": [
"new label",
],
"name": [
"fieldNameOpportunityForMorphRelationSecond",
"fieldNamePersonForMorphRelationSecond",
],
}
`;
exports[`updateOne FieldMetadataService morph relation fields v2 It should update all morph related flat field metadata and their related field allowing its deletion 4`] = `
{
"description": [
null,
],
"isActive": [
false,
],
"label": [
"tata",
"toto",
],
"name": [
"tata",
"toto",
],
}
`;
@@ -1,13 +1,50 @@
import { type DeepPartial } from 'ai';
import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util';
import { deleteOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/delete-one-field-metadata.util';
import { findManyFieldsMetadata } from 'test/integration/metadata/suites/field-metadata/utils/find-many-fields-metadata.util';
import { updateOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/update-one-field-metadata.util';
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
import { jestExpectToBeDefined } from 'test/utils/jest-expect-to-be-defined.util.test';
import { FieldMetadataType } from 'twenty-shared/types';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { type FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto';
import { type RelationDTO } from 'src/engine/metadata-modules/field-metadata/dtos/relation.dto';
type AggregatedFieldMetadataDto = {
description: (string | null)[];
name: string[];
label: string[];
isActive: boolean[];
};
const aggregateFieldMetadata = (
fieldMetadataDtos: { node: FieldMetadataDTO }[],
) => {
const initialAcc: AggregatedFieldMetadataDto = {
description: [],
name: [],
label: [],
isActive: [],
};
return fieldMetadataDtos.reduce((acc, { node: fieldMetadataDto }) => {
return {
...acc,
description: [
...new Set([...acc.description, fieldMetadataDto.description]),
].sort(),
isActive: [
...new Set([...acc.isActive, fieldMetadataDto.isActive]),
].sort(),
label: [...new Set([...acc.label, fieldMetadataDto.label])].sort(),
name: [...new Set([...acc.name, fieldMetadataDto.name])].sort(),
};
}, initialAcc);
};
describe('updateOne FieldMetadataService morph relation fields v2', () => {
let createdObjectMetadataPersonId: string;
let createdObjectMetadataOpportunityId: string;
@@ -97,6 +134,7 @@ describe('updateOne FieldMetadataService morph relation fields v2', () => {
label: 'field label',
name: 'fieldName',
objectMetadataId: createdObjectMetadataCompanyId,
description: 'Description for all',
type: FieldMetadataType.MORPH_RELATION,
morphRelationsCreationPayload: [
{
@@ -119,7 +157,125 @@ describe('updateOne FieldMetadataService morph relation fields v2', () => {
createdFieldMetadataId = rawCreateOneField.id;
});
it('It should update all morph related flat field metadata allowing its deletion', async () => {
it('It should update all morph related flat field metadata and their related field allowing its deletion', async () => {
// SETUP
const { fields: findResult } = await findManyFieldsMetadata({
input: {
filter: { id: { eq: createdFieldMetadataId } },
paging: { first: 1 },
},
expectToFail: false,
gqlFields: `
id
type
name
label
isLabelSyncedWithName
settings
object {
id
nameSingular
}
morphRelations {
type
targetFieldMetadata {
id
type
}
sourceFieldMetadata {
id
type
}
}
`,
});
expect(findResult.length).toBe(1);
const createdMorphFromResult = findResult[0].node as FieldMetadataDTO & {
morphRelations: RelationDTO[];
};
jestExpectToBeDefined(createdMorphFromResult);
createdMorphFromResult.morphRelations.map((relationDto) =>
expect(relationDto).toMatchObject<DeepPartial<RelationDTO>>({
sourceFieldMetadata: {
type: FieldMetadataType.MORPH_RELATION,
},
targetFieldMetadata: {
type: FieldMetadataType.RELATION,
},
}),
);
const allMorphFieldIds = createdMorphFromResult.morphRelations.map(
({ sourceFieldMetadata }) => sourceFieldMetadata.id,
);
const allRelationFieldIds = createdMorphFromResult.morphRelations.map(
({ targetFieldMetadata }) => targetFieldMetadata.id,
);
/// ASSERT
{
const morphRelationFieldsBeforeUpdate = (await findManyFieldsMetadata({
input: {
filter: { id: { in: allMorphFieldIds } },
paging: { first: allMorphFieldIds.length },
},
gqlFields: `
id
name
description
label
isActive
`,
expectToFail: false,
})) as { fields: { node: FieldMetadataDTO }[] };
expect(morphRelationFieldsBeforeUpdate.fields.length).toBe(
allMorphFieldIds.length,
);
const aggregatedMorphFieldMetadataDtos = aggregateFieldMetadata(
morphRelationFieldsBeforeUpdate.fields,
);
expect(aggregatedMorphFieldMetadataDtos).toMatchSnapshot();
expect(
aggregatedMorphFieldMetadataDtos,
).toMatchObject<AggregatedFieldMetadataDto>({
description: ['Description for all'],
isActive: [true],
label: ['field label'],
name: [expect.any(String), expect.any(String)],
});
const relationFieldsBeforeUpdate = (await findManyFieldsMetadata({
input: {
filter: { id: { in: allRelationFieldIds } },
paging: { first: allRelationFieldIds.length },
},
gqlFields: `
id
name
description
label
isActive
`,
expectToFail: false,
})) as { fields: { node: FieldMetadataDTO }[] };
expect(relationFieldsBeforeUpdate.fields.length).toBe(
allRelationFieldIds.length,
);
const aggregatedRelationFieldMetadataDtos = aggregateFieldMetadata(
relationFieldsBeforeUpdate.fields,
);
expect(aggregatedRelationFieldMetadataDtos).toMatchSnapshot();
}
// UPDATE
const input = {
idToUpdate: createdFieldMetadataId,
updatePayload: {
@@ -128,21 +284,82 @@ describe('updateOne FieldMetadataService morph relation fields v2', () => {
description: 'new description',
},
};
const {
data: { updateOneField },
} = await updateOneFieldMetadata({
await updateOneFieldMetadata({
expectToFail: false,
input,
gqlFields: `
id
isActive
description
name
description
label
isActive
`,
});
expect(updateOneField).toMatchObject(input.updatePayload);
//ASSERT
{
const morphRelationFieldsAfterUpdate = (await findManyFieldsMetadata({
input: {
filter: { id: { in: allMorphFieldIds } },
paging: { first: allMorphFieldIds.length },
},
gqlFields: `
id
name
description
label
isActive
`,
expectToFail: false,
})) as { fields: { node: FieldMetadataDTO }[] };
expect(morphRelationFieldsAfterUpdate.fields.length).toBe(
allMorphFieldIds.length,
);
const aggregatedMorphFieldMetadataDtos = aggregateFieldMetadata(
morphRelationFieldsAfterUpdate.fields,
);
expect(aggregatedMorphFieldMetadataDtos).toMatchSnapshot();
expect(
aggregatedMorphFieldMetadataDtos,
).toMatchObject<AggregatedFieldMetadataDto>({
description: ['new description'],
isActive: [false],
label: ['new label'],
name: [expect.any(String), expect.any(String)],
});
const allRelationFieldIds = createdMorphFromResult.morphRelations.map(
({ targetFieldMetadata }) => targetFieldMetadata.id,
);
const relationFieldsBeforeUpdate = (await findManyFieldsMetadata({
input: {
filter: { id: { in: allRelationFieldIds } },
paging: { first: allRelationFieldIds.length },
},
gqlFields: `
id
name
description
label
isActive
`,
expectToFail: false,
})) as { fields: { node: FieldMetadataDTO }[] };
expect(relationFieldsBeforeUpdate.fields.length).toBe(
allRelationFieldIds.length,
);
const aggregatedRelationFieldMetadataDtos = aggregateFieldMetadata(
relationFieldsBeforeUpdate.fields,
);
expect(aggregatedRelationFieldMetadataDtos).toMatchSnapshot();
}
await deleteOneFieldMetadata({
input: {
@@ -1,79 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Field metadata relation update should fail relation when name is changed 1`] = `
[
{
"extensions": {
"code": "METADATA_VALIDATION_FAILED",
"errors": {
"cronTrigger": [],
"databaseEventTrigger": [],
"fieldMetadata": [
{
"errors": [
{
"code": "FIELD_MUTATION_NOT_ALLOWED",
"message": "Forbidden updated properties for relation field metadata: name",
"userFriendlyMessage": "Forbidden updated properties for relation field metadata",
},
],
"flatEntityMinimalInformation": {
"id": Any<String>,
"name": "newName",
"objectMetadataId": Any<String>,
},
"status": "fail",
"type": "update_field",
},
{
"errors": [
{
"code": "FIELD_MUTATION_NOT_ALLOWED",
"message": "Forbidden updated properties for relation field metadata: name",
"userFriendlyMessage": "Forbidden updated properties for relation field metadata",
},
],
"flatEntityMinimalInformation": {
"id": Any<String>,
"name": "newName",
"objectMetadataId": Any<String>,
},
"status": "fail",
"type": "update_field",
},
],
"index": [],
"objectMetadata": [],
"routeTrigger": [],
"serverlessFunction": [],
"view": [],
"viewField": [],
"viewFilter": [],
"viewGroup": [],
},
"message": "Validation failed for 0 object(s) and 0 field(s)",
"summary": {
"invalidCronTrigger": 0,
"invalidDatabaseEventTrigger": 0,
"invalidFieldMetadata": 0,
"invalidIndex": 0,
"invalidObjectMetadata": 0,
"invalidRouteTrigger": 0,
"invalidServerlessFunction": 0,
"invalidView": 0,
"invalidViewField": 0,
"invalidViewFilter": 0,
"invalidViewGroup": 0,
"totalErrors": 0,
},
"userFriendlyMessage": "Validation failed for 0 object(s) and 0 field(s)",
},
"message": "Multiple validation errors occurred while updating field",
"name": "GraphQLError",
},
]
`;
exports[`Field metadata relation update should fail relation when name is not in camel case 1`] = `
[
{
@@ -85,39 +11,6 @@ exports[`Field metadata relation update should fail relation when name is not in
"fieldMetadata": [
{
"errors": [
{
"code": "FIELD_MUTATION_NOT_ALLOWED",
"message": "Forbidden updated properties for relation field metadata: name",
"userFriendlyMessage": "Forbidden updated properties for relation field metadata",
},
{
"code": "INVALID_FIELD_INPUT",
"message": "Name should be in camelCase",
"userFriendlyMessage": "Name should be in camelCase",
"value": "New Name",
},
{
"code": "INVALID_FIELD_INPUT",
"message": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters",
"userFriendlyMessage": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters",
"value": "New Name",
},
],
"flatEntityMinimalInformation": {
"id": Any<String>,
"name": "New Name",
"objectMetadataId": Any<String>,
},
"status": "fail",
"type": "update_field",
},
{
"errors": [
{
"code": "FIELD_MUTATION_NOT_ALLOWED",
"message": "Forbidden updated properties for relation field metadata: name",
"userFriendlyMessage": "Forbidden updated properties for relation field metadata",
},
{
"code": "INVALID_FIELD_INPUT",
"message": "Name should be in camelCase",
@@ -4,9 +4,9 @@ import { createOneObjectMetadata } from 'test/integration/metadata/suites/object
import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
import { getMockCreateObjectInput } from 'test/integration/metadata/suites/object-metadata/utils/generate-mock-create-object-metadata-input';
import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any';
import { type EachTestingContext } from 'twenty-shared/testing';
import { FieldMetadataType } from 'twenty-shared/types';
import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
@@ -27,10 +27,6 @@ describe('Field metadata relation update should fail', () => {
title: 'when name is not in camel case',
context: { name: 'New Name' },
},
{
title: 'when name is changed',
context: { name: 'newName' },
},
];
beforeAll(async () => {
@@ -1,4 +1,5 @@
import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util';
import { findManyFieldsMetadata } from 'test/integration/metadata/suites/field-metadata/utils/find-many-fields-metadata.util';
import { updateOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/update-one-field-metadata.util';
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
import { createRelationBetweenObjects } from 'test/integration/metadata/suites/object-metadata/utils/create-relation-between-objects.util';
@@ -104,6 +105,43 @@ describe('Field metadata relation update should succeed', () => {
expect(data).toBeDefined();
expect(data.updateOneField.isActive).toBe(false);
});
it('should successfully update the name of a relation field', async () => {
const { fields } = await findManyFieldsMetadata({
input: {
filter: {
id: { eq: globalTestContext.employerFieldMetadataId },
},
paging: { first: 1 },
},
gqlFields: `
id
name
`,
});
const field = fields[0]?.node;
expect(field?.name).toBe('employer');
const { data, errors } = await updateOneFieldMetadata({
expectToFail: false,
input: {
idToUpdate: globalTestContext.employerFieldMetadataId,
updatePayload: {
name: 'leadEmployer',
},
},
gqlFields: `
id
name
`,
});
expect(errors).toBeUndefined();
expect(data).toBeDefined();
expect(data.updateOneField.name).toBe('leadEmployer');
});
});
describe('Field metadata self-relation update should succeed', () => {
@@ -0,0 +1,73 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Rename an object metadata with morph relation should succeed should update indexes on ONE_TO_MANY morph field related target object name update 1`] = `
{
"indexFieldMetadataList": [
{
"createdAt": Any<String>,
"fieldMetadataId": Any<String>,
"id": Any<String>,
"order": 0,
"updatedAt": Any<String>,
},
],
"indexType": "BTREE",
"isCustom": true,
"isUnique": false,
"name": "IDX_d46ee05346f12228875612c7a2a",
}
`;
exports[`Rename an object metadata with morph relation should succeed should update indexes on ONE_TO_MANY morph field related target object name update 2`] = `
{
"indexFieldMetadataList": [
{
"createdAt": Any<String>,
"fieldMetadataId": Any<String>,
"id": Any<String>,
"order": 0,
"updatedAt": Any<String>,
},
],
"indexType": "BTREE",
"isCustom": true,
"isUnique": false,
"name": "IDX_270c22872e92d429bb4f07a15dd",
}
`;
exports[`Rename an object metadata with morph relation should succeed should update indexes on ONE_TO_MANY morph field related target object name update 3`] = `
{
"indexFieldMetadataList": [
{
"createdAt": Any<String>,
"fieldMetadataId": Any<String>,
"id": Any<String>,
"order": 0,
"updatedAt": Any<String>,
},
],
"indexType": "BTREE",
"isCustom": true,
"isUnique": false,
"name": "IDX_aabda09e5917c5a7cbe5b6c84fc",
}
`;
exports[`Rename an object metadata with morph relation should succeed should update indexes on ONE_TO_MANY morph field related target object name update 4`] = `
{
"indexFieldMetadataList": [
{
"createdAt": Any<String>,
"fieldMetadataId": Any<String>,
"id": Any<String>,
"order": 0,
"updatedAt": Any<String>,
},
],
"indexType": "BTREE",
"isCustom": true,
"isUnique": false,
"name": "IDX_270c22872e92d429bb4f07a15dd",
}
`;
@@ -1,13 +1,95 @@
import { findManyFieldsMetadataQueryFactory } from 'test/integration/metadata/suites/field-metadata/utils/find-many-fields-metadata-query-factory.util';
import { findManyFieldsMetadata } from 'test/integration/metadata/suites/field-metadata/utils/find-many-fields-metadata.util';
import { createMorphRelationBetweenObjects } from 'test/integration/metadata/suites/object-metadata/utils/create-morph-relation-between-objects.util';
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
import { findManyObjectMetadataWithIndexes } from 'test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata-with-indexes.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 { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any';
import { jestExpectToBeDefined } from 'test/utils/jest-expect-to-be-defined.util.test';
import {
eachTestingContextFilter,
type EachTestingContext,
} from 'twenty-shared/testing';
import { FieldMetadataType } from 'twenty-shared/types';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { type IndexFieldMetadataDTO } from 'src/engine/metadata-modules/index-metadata/dtos/index-field-metadata.dto';
import { type IndexMetadataDTO } from 'src/engine/metadata-modules/index-metadata/dtos/index-metadata.dto';
const findFieldMetadata = async ({
fieldMetadataId,
}: {
fieldMetadataId: string;
}) => {
const { fields } = await findManyFieldsMetadata({
gqlFields: `
id
name
object { id nameSingular }
relation { type targetFieldMetadata { id } targetObjectMetadata { id } }
settings
`,
input: {
filter: { id: { eq: fieldMetadataId } },
paging: { first: 1 },
},
expectToFail: false,
});
return fields[0]?.node;
};
const allTestsUseCases: EachTestingContext<{
nameSingular: string;
namePlural: string;
labelSingular: string;
labelPlural: string;
isLabelSyncedWithName: boolean;
newJoinColumnName: string | undefined;
relationType: RelationType;
}>[] = [
{
title:
'should rename custom object, and update both the field name and join column name of the morph relation that contains the object name',
context: {
nameSingular: 'personForRenameSecond2',
namePlural: 'peopleForRenameSecond2',
labelSingular: 'Person For Rename2',
labelPlural: 'People For Rename2',
isLabelSyncedWithName: false,
newJoinColumnName: 'ownerPersonForRenameSecond2Id',
relationType: RelationType.MANY_TO_ONE,
},
},
{
title:
'should rename custom object, and update both the field name and join column name of the morph relation that contains the object name if label is sync with name',
context: {
nameSingular: 'personForRenameSecond3',
namePlural: 'peopleForRenameSecond3',
labelSingular: 'person For Rename Second3',
labelPlural: 'people For Rename Second3',
isLabelSyncedWithName: true,
newJoinColumnName: 'ownerPersonForRenameSecond3Id',
relationType: RelationType.MANY_TO_ONE,
},
},
{
title:
'should rename custom object, and update both the field name and join column name of the morph relation that contains the object name with ONE_TO_MANY relation type',
context: {
nameSingular: 'personForRenameSecond4',
namePlural: 'peopleForRenameSecond4',
labelSingular: 'Person For Rename Second4',
labelPlural: 'People For Rename Second4',
isLabelSyncedWithName: true,
newJoinColumnName: undefined,
relationType: RelationType.ONE_TO_MANY,
},
},
];
describe('Rename an object metadata with morph relation should succeed', () => {
let createdObjectMetadataPersonId: string;
let createdObjectMetadataOpportunityId: string;
@@ -83,16 +165,26 @@ describe('Rename an object metadata with morph relation should succeed', () => {
}
});
it.failing(
'should rename custom object, and update both the field name and join column name of the morph relation that contains the object name',
async () => {
it.each(eachTestingContextFilter(allTestsUseCases))(
'$title',
async ({ context }) => {
const {
nameSingular,
namePlural,
labelSingular,
labelPlural,
isLabelSyncedWithName,
newJoinColumnName,
relationType,
} = context;
const morphRelationField = await createMorphRelationBetweenObjects({
name: 'owner',
objectMetadataId: createdObjectMetadataOpportunityId,
firstTargetObjectMetadataId: createdObjectMetadataPersonId,
secondTargetObjectMetadataId: createdObjectMetadataCompanyId,
type: FieldMetadataType.MORPH_RELATION,
relationType: RelationType.MANY_TO_ONE,
relationType,
});
expect(morphRelationField.morphRelations.length).toBe(2);
@@ -100,23 +192,24 @@ describe('Rename an object metadata with morph relation should succeed', () => {
const { data } = await updateOneObjectMetadata({
expectToFail: false,
gqlFields: `
nameSingular
labelSingular
namePlural
labelPlural
`,
nameSingular
labelSingular
namePlural
labelPlural
`,
input: {
idToUpdate: createdObjectMetadataPersonId,
updatePayload: {
nameSingular: 'personForRenameSecond2',
namePlural: 'peopleForRenameSecond2',
labelSingular: 'Person For Rename2',
labelPlural: 'People For Rename2',
nameSingular,
namePlural,
labelSingular,
labelPlural,
isLabelSyncedWithName,
},
},
});
expect(data.updateOneObject.nameSingular).toBe('personForRenameSecond2');
expect(data.updateOneObject.nameSingular).toBe(nameSingular);
const ownerFieldMetadataOnPersonId =
morphRelationField.morphRelations.find(
@@ -136,32 +229,273 @@ describe('Rename an object metadata with morph relation should succeed', () => {
});
expect(fieldAfterRenaming.settings.joinColumnName).toBe(
'ownerPersonForRenameSecond2Id',
newJoinColumnName,
);
},
);
});
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 },
},
it('should update indexes on ONE_TO_MANY morph field related target object name update', async () => {
const morphRelationField = await createMorphRelationBetweenObjects({
name: 'owner',
objectMetadataId: createdObjectMetadataOpportunityId,
firstTargetObjectMetadataId: createdObjectMetadataPersonId,
secondTargetObjectMetadataId: createdObjectMetadataCompanyId,
type: FieldMetadataType.MORPH_RELATION,
relationType: RelationType.ONE_TO_MANY,
});
expect(morphRelationField.morphRelations.length).toBe(2);
const objects = await findManyObjectMetadataWithIndexes({
expectToFail: false,
});
let relationIndexByFieldId: Record<
string,
IndexMetadataDTO & {
indexFieldMetadataList: IndexFieldMetadataDTO[];
}
> = {};
const morphParentObject = objects.find(
(object) => object.id === createdObjectMetadataOpportunityId,
);
jestExpectToBeDefined(morphParentObject);
for (const {
targetObjectMetadata,
targetFieldMetadata,
sourceFieldMetadata,
} of morphRelationField.morphRelations) {
const relatedObject = objects.find(
(object) => object.id === targetObjectMetadata.id,
);
jestExpectToBeDefined(relatedObject);
const objectRelatedIndexes = relatedObject.indexMetadataList.filter(
(index) =>
index.indexFieldMetadataList.some(
(indexField) =>
indexField.fieldMetadataId === targetFieldMetadata.id,
),
);
expect(objectRelatedIndexes.length).toBe(1);
const [relationIndex] = objectRelatedIndexes;
jestExpectToBeDefined(relationIndex);
expect(relationIndex).toMatchSnapshot(
extractRecordIdsAndDatesAsExpectAny({ ...relationIndex }),
);
relationIndexByFieldId[targetFieldMetadata.id] = relationIndex;
const parentRelationIndex = morphParentObject.indexMetadataList.filter(
(index) =>
index.indexFieldMetadataList.some(
(indexField) =>
indexField.fieldMetadataId == sourceFieldMetadata.id,
),
);
expect(parentRelationIndex.length).toBe(0);
}
await updateOneObjectMetadata({
expectToFail: false,
input: {
idToUpdate: createdObjectMetadataPersonId,
updatePayload: {
nameSingular: 'personForRenameSecondUpdated',
namePlural: 'peopleForRenameSecondUpdated',
labelSingular: 'Person For Rename Updated',
labelPlural: 'People For Rename Updated',
},
},
});
const updatedObjects = await findManyObjectMetadataWithIndexes({
expectToFail: false,
});
const morphParentObjectAfterUpdate = objects.find(
(object) => object.id === createdObjectMetadataOpportunityId,
);
jestExpectToBeDefined(morphParentObjectAfterUpdate);
for (const {
targetObjectMetadata,
targetFieldMetadata,
sourceFieldMetadata,
} of morphRelationField.morphRelations) {
const relatedObject = updatedObjects.find(
(object) => object.id === targetObjectMetadata.id,
);
jestExpectToBeDefined(relatedObject);
const objectRelatedIndexes = relatedObject.indexMetadataList.filter(
(index) =>
index.indexFieldMetadataList.some(
(indexField) =>
indexField.fieldMetadataId === targetFieldMetadata.id,
),
);
expect(objectRelatedIndexes.length).toBe(1);
const [relationIndex] = objectRelatedIndexes;
jestExpectToBeDefined(relationIndex);
expect(relationIndex).toMatchSnapshot(
extractRecordIdsAndDatesAsExpectAny({ ...relationIndex }),
);
const previousIndex = relationIndexByFieldId[targetFieldMetadata.id];
jestExpectToBeDefined(previousIndex);
if (targetObjectMetadata.id === createdObjectMetadataPersonId) {
expect(previousIndex.name).not.toBe(relationIndex.name);
} else {
expect(previousIndex.name).toBe(relationIndex.name);
}
const parentRelationIndex =
morphParentObjectAfterUpdate.indexMetadataList.filter((index) =>
index.indexFieldMetadataList.some(
(indexField) =>
indexField.fieldMetadataId == sourceFieldMetadata.id,
),
);
expect(parentRelationIndex.length).toBe(0);
}
});
const fields = await makeMetadataAPIRequest(operation);
const field = fields.body.data.fields.edges?.[0]?.node;
return field;
};
it('should not update morph index on MANY_TO_ONE morph field related target object name update', async () => {
const morphRelationField = await createMorphRelationBetweenObjects({
name: 'owner',
objectMetadataId: createdObjectMetadataOpportunityId,
firstTargetObjectMetadataId: createdObjectMetadataPersonId,
secondTargetObjectMetadataId: createdObjectMetadataCompanyId,
type: FieldMetadataType.MORPH_RELATION,
relationType: RelationType.MANY_TO_ONE,
});
expect(morphRelationField.morphRelations.length).toBe(2);
const objects = await findManyObjectMetadataWithIndexes({
expectToFail: false,
});
let relationIndexByFieldId: Record<
string,
IndexMetadataDTO & {
indexFieldMetadataList: IndexFieldMetadataDTO[];
}
> = {};
const morphParentObject = objects.find(
(object) => object.id === createdObjectMetadataOpportunityId,
);
jestExpectToBeDefined(morphParentObject);
for (const {
targetObjectMetadata,
targetFieldMetadata,
sourceFieldMetadata,
} of morphRelationField.morphRelations) {
const relatedObject = objects.find(
(object) => object.id === targetObjectMetadata.id,
);
jestExpectToBeDefined(relatedObject);
const objectRelatedIndexes = relatedObject.indexMetadataList.filter(
(index) =>
index.indexFieldMetadataList.some(
(indexField) =>
indexField.fieldMetadataId === targetFieldMetadata.id,
),
);
expect(objectRelatedIndexes.length).toBe(0);
const parentRelationIndex = morphParentObject.indexMetadataList.filter(
(index) =>
index.indexFieldMetadataList.some(
(indexField) =>
indexField.fieldMetadataId == sourceFieldMetadata.id,
),
);
expect(parentRelationIndex.length).toBe(1);
const [relationIndex] = parentRelationIndex;
relationIndexByFieldId[
relationIndex.indexFieldMetadataList[0].fieldMetadataId
] = relationIndex;
}
await updateOneObjectMetadata({
expectToFail: false,
input: {
idToUpdate: createdObjectMetadataPersonId,
updatePayload: {
nameSingular: 'personForRenameSecondUpdated',
namePlural: 'peopleForRenameSecondUpdated',
labelSingular: 'Person For Rename Updated',
labelPlural: 'People For Rename Updated',
},
},
});
const updatedObjects = await findManyObjectMetadataWithIndexes({
expectToFail: false,
});
const morphParentObjectAfterUpdate = objects.find(
(object) => object.id === createdObjectMetadataOpportunityId,
);
jestExpectToBeDefined(morphParentObjectAfterUpdate);
for (const {
targetObjectMetadata,
targetFieldMetadata,
sourceFieldMetadata,
} of morphRelationField.morphRelations) {
const relatedObject = updatedObjects.find(
(object) => object.id === targetObjectMetadata.id,
);
jestExpectToBeDefined(relatedObject);
const objectRelatedIndexes = relatedObject.indexMetadataList.filter(
(index) =>
index.indexFieldMetadataList.some(
(indexField) =>
indexField.fieldMetadataId === targetFieldMetadata.id,
),
);
expect(objectRelatedIndexes.length).toBe(0);
const parentRelationIndex =
morphParentObjectAfterUpdate.indexMetadataList.filter((index) =>
index.indexFieldMetadataList.some(
(indexField) =>
indexField.fieldMetadataId == sourceFieldMetadata.id,
),
);
expect(parentRelationIndex.length).toBe(1);
const [relationIndex] = parentRelationIndex;
const previousIndex = relationIndexByFieldId[sourceFieldMetadata.id];
expect(previousIndex.name).toBe(relationIndex.name);
}
});
});