diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-morph-relation-editable-properties-on-sibling-morph-relation-update.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-morph-relation-editable-properties-on-sibling-morph-relation-update.constant.ts new file mode 100644 index 0000000000000..af0bf1aebc365 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-morph-relation-editable-properties-on-sibling-morph-relation-update.constant.ts @@ -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)[] + >; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-relation-editable-properties-on-sibling-morph-or-relation-update.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-relation-editable-properties-on-sibling-morph-or-relation-update.constant.ts new file mode 100644 index 0000000000000..27a9ee11b527c --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-relation-editable-properties-on-sibling-morph-or-relation-update.constant.ts @@ -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)[]; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-relation-properties-to-compare.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-relation-properties-to-compare.constant.ts index 33cf391778c90..c5158c7c1c888 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-relation-properties-to-compare.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-relation-properties-to-compare.constant.ts @@ -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][]; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/compute-flat-field-metadata-related-flat-field-metadata.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/compute-flat-field-metadata-related-flat-field-metadata.util.ts index 39924da19137b..712f211c058a8 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/compute-flat-field-metadata-related-flat-field-metadata.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/compute-flat-field-metadata-related-flat-field-metadata.util.ts @@ -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 []; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/compute-flat-field-to-update-and-related-flat-field-to-update.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/compute-flat-field-to-update-and-related-flat-field-to-update.util.ts new file mode 100644 index 0000000000000..52c93c159fb4f --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/compute-flat-field-to-update-and-related-flat-field-to-update.util.ts @@ -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; + relatedFlatFieldMetadatasFromTo: FromTo< + FlatFieldMetadata, + 'flatFieldMetadata' + >[]; +}; + +type ComputeFlatFieldToUpdateAndRelatedFlatFieldToUpdateArgs = { + rawUpdateFieldInput: UpdateFieldInput; + fromFlatFieldMetadata: FlatFieldMetadata; + flatObjectMetadata: FlatObjectMetadata; +} & Pick; +// 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 + >((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 + >((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: [], + }; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/find-field-related-index.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/find-field-related-index.util.ts new file mode 100644 index 0000000000000..cf4cc38e7648c --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/find-field-related-index.util.ts @@ -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[] => { + const objectIndexes = findManyFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityMaps: flatIndexMaps, + flatEntityIds: flatObjectMetadata.indexMetadataIds, + }); + + return objectIndexes.filter((index) => + index.flatIndexFieldMetadatas.some( + (indexField) => indexField.fieldMetadataId === flatFieldMetadata.id, + ), + ); +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/find-flat-field-metadatas-related-to-morph-relation-or-throw.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/find-flat-field-metadatas-related-to-morph-relation-or-throw.util.ts index 1b212dbcb11a3..c0a53c2fcdaf9 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/find-flat-field-metadatas-related-to-morph-relation-or-throw.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/find-flat-field-metadatas-related-to-morph-relation-or-throw.util.ts @@ -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; flatObjectMetadata: FlatObjectMetadata; }; + +type FindFlatFieldMetadatasRelatedToMorphRelationOrThrowReturnType = { + morphRelationFlatFieldMetadatas: FlatFieldMetadata[]; + relationFlatFieldMetadatas: FlatFieldMetadata[]; +}; export const findFlatFieldMetadatasRelatedToMorphRelationOrThrow = ({ flatFieldMetadataMaps, flatFieldMetadata: morphRelationFlatFieldMetadata, flatObjectMetadata, -}: FindFlatFieldMetadatasRelatedToMorphRelationOrThrowArgs): FlatFieldMetadata[] => { - const allMorphFlatFieldMetadatas = +}: FindFlatFieldMetadatasRelatedToMorphRelationOrThrowArgs): FindFlatFieldMetadatasRelatedToMorphRelationOrThrowReturnType => { + const morphRelationFlatFieldMetadatas = findAllOthersMorphRelationFlatFieldMetadatasOrThrow({ flatFieldMetadata: morphRelationFlatFieldMetadata, flatFieldMetadataMaps, flatObjectMetadata, }); - return [ + const relationFlatFieldMetadatas = [ morphRelationFlatFieldMetadata, - ...allMorphFlatFieldMetadatas, - ].flatMap((flatFieldMetadata) => { - const relationTargetFlatFieldMetadata = - findRelationFlatFieldMetadataTargetFlatFieldMetadataOrThrow({ - flatFieldMetadata, - flatFieldMetadataMaps, - }); - - if (flatFieldMetadata.id === morphRelationFlatFieldMetadata.id) { - return [relationTargetFlatFieldMetadata]; - } + ...morphRelationFlatFieldMetadatas, + ].map((flatFieldMetadata) => + findRelationFlatFieldMetadataTargetFlatFieldMetadataOrThrow({ + flatFieldMetadata, + flatFieldMetadataMaps, + }), + ) as FlatFieldMetadata[]; - return [flatFieldMetadata, relationTargetFlatFieldMetadata]; - }); + return { + morphRelationFlatFieldMetadatas, + relationFlatFieldMetadatas, + }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-update-field-input-to-flat-field-metadata.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-update-field-input-to-flat-field-metadata.util.ts index 9b676d4f07d85..de5f60c79877e 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-update-field-input-to-flat-field-metadata.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-update-field-input-to-flat-field-metadata.util.ts @@ -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 = - flatFieldMetadatasToUpdate.reduce( - (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, + const optimisticiallyUpdatedFlatFieldMetadatas = [ + flatFieldMetadataFromTo, + ...relatedFlatFieldMetadatasFromTo, + ].reduce( + (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, + }); + + 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', diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/handle-index-changes-during-field-update.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/handle-index-changes-during-field-update.util.ts index 4368830fbc3bd..234d062c855b7 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/handle-index-changes-during-field-update.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/handle-index-changes-during-field-update.util.ts @@ -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[] => { - 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, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/recompute-index-on-flat-field-metadata-name-update.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/recompute-index-on-flat-field-metadata-name-update.util.ts index 892b33fb583c2..78f4513a7815a 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/recompute-index-on-flat-field-metadata-name-update.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/recompute-index-on-flat-field-metadata-name-update.util.ts @@ -42,13 +42,11 @@ export const recomputeIndexOnFlatFieldMetadataNameUpdate = ({ }, ); - return relatedFlatIndexMetadata.map((flatIndex) => { - const newIndex = generateFlatIndexMetadataWithNameOrThrow({ + return relatedFlatIndexMetadata.map((flatIndex) => + generateFlatIndexMetadataWithNameOrThrow({ flatIndex, flatObjectMetadata, objectFlatFieldMetadatas: optimisticObjectFlatFieldMetadatas, - }); - - return newIndex; - }); + }), + ); }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/sanitize-raw-update-field-input.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/sanitize-raw-update-field-input.ts index b628f18fb4782..1a5da3b5c8a72 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/sanitize-raw-update-field-input.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/sanitize-raw-update-field-input.ts @@ -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, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-update-object-input-to-flat-object-metadata-and-related-flat-entities.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-update-object-input-to-flat-object-metadata-and-related-flat-entities.util.ts index b9d829ab61a53..54755160b890c 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-update-object-input-to-flat-object-metadata-and-related-flat-entities.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-update-object-input-to-flat-object-metadata-and-related-flat-entities.util.ts @@ -88,6 +88,7 @@ export const fromUpdateObjectInputToFlatObjectMetadataAndRelatedFlatEntities = fromFlatObjectMetadata: existingFlatObjectMetadata, toFlatObjectMetadata, flatFieldMetadataMaps, + flatObjectMetadataMaps: existingFlatObjectMetadataMaps, flatIndexMaps, flatViewFieldMaps, flatViewMaps, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/handle-flat-object-metadata-update-side-effect.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/handle-flat-object-metadata-update-side-effect.util.ts index 2e011519fa8e3..590ebb2f2d817 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/handle-flat-object-metadata-update-side-effect.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/handle-flat-object-metadata-update-side-effect.util.ts @@ -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, }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/recompute-index-after-flat-object-metadata-singular-name-update.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/recompute-index-after-flat-object-metadata-singular-name-update.util.ts index 8342e58c34b3d..06949d2453206 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/recompute-index-after-flat-object-metadata-singular-name-update.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/recompute-index-after-flat-object-metadata-singular-name-update.util.ts @@ -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 []; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/rename-related-morph-field-on-object-names-update.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/rename-related-morph-field-on-object-names-update.util.ts index 4e5821d806e87..5bcc9da6a8b12 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/rename-related-morph-field-on-object-names-update.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/rename-related-morph-field-on-object-names-update.util.ts @@ -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; +}; +const updateMorphFlatFieldName = ({ + fromMorphFlatFieldMetadata, + fromRelationTargetFlatObjectMetadata, + toRelationTargetFlatObjectMetadata, +}: UpdateMorphFlatFieldNameArgs): FlatFieldMetadata => { + 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; -// TODO We should recompute each index here too + Pick< + AllFlatEntityMaps, + 'flatFieldMetadataMaps' | 'flatObjectMetadataMaps' | 'flatIndexMaps' + >; + +type RenameRelatedMorphFieldOnObjectNamesUpdateReturnType = { + morphFlatFieldMetadatasToUpdate: FlatFieldMetadata[]; + morphRelatedFlatIndexesToUpdate: FlatIndexMetadata[]; +}; export const renameRelatedMorphFieldOnObjectNamesUpdate = ({ fromFlatObjectMetadata, toFlatObjectMetadata, flatFieldMetadataMaps, -}: RenameRelatedMorphFieldOnObjectNamesUpdateArgs): FlatFieldMetadata[] => { + 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: [], + }; + + return allMorphRelationFlatFieldMetadatas.reduce( + (acc, fromMorphFlatFieldMetadata) => { + const morphFlatFieldMetadataTo = updateMorphFlatFieldName({ + fromMorphFlatFieldMetadata, + fromRelationTargetFlatObjectMetadata: fromFlatObjectMetadata, + toRelationTargetFlatObjectMetadata: toFlatObjectMetadata, + }); - const newMorphFieldName = computeMorphRelationFieldName({ - fieldName: initialMorphRelationFieldName, - relationType: morphRelationFlatFieldMetadata.settings.relationType, - targetObjectMetadataNameSingular: toFlatObjectMetadata.nameSingular, - targetObjectMetadataNamePlural: toFlatObjectMetadata.namePlural, + 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, + ); }; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-field-metadata-validator.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-field-metadata-validator.service.ts index ccdc632c5094b..86fe78281c6b5 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-field-metadata-validator.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-field-metadata-validator.service.ts @@ -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( diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/field/services/update-field-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/field/services/update-field-action-handler.service.ts index bd00eddc99958..a6d5b0e584424 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/field/services/update-field-action-handler.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/field/services/update-field-action-handler.service.ts @@ -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, - '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, diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/__snapshots__/successful-update-one-field-metadata-morph-relation-v2.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/__snapshots__/successful-update-one-field-metadata-morph-relation-v2.integration-spec.ts.snap new file mode 100644 index 0000000000000..e1c7c80680936 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/__snapshots__/successful-update-one-field-metadata-morph-relation-v2.integration-spec.ts.snap @@ -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", + ], +} +`; diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/successful-update-one-field-metadata-morph-relation-v2.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/successful-update-one-field-metadata-morph-relation-v2.integration-spec.ts index ee1e99a78cf2f..af50750d204c1 100644 --- a/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/successful-update-one-field-metadata-morph-relation-v2.integration-spec.ts +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/successful-update-one-field-metadata-morph-relation-v2.integration-spec.ts @@ -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>({ + 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({ + 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({ + 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: { diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/relation/__snapshots__/failing-field-metadata-relation-update.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/field-metadata/relation/__snapshots__/failing-field-metadata-relation-update.integration-spec.ts.snap index eab957c4af0b2..322ccc98a6d29 100644 --- a/packages/twenty-server/test/integration/metadata/suites/field-metadata/relation/__snapshots__/failing-field-metadata-relation-update.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/relation/__snapshots__/failing-field-metadata-relation-update.integration-spec.ts.snap @@ -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, - "name": "newName", - "objectMetadataId": Any, - }, - "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, - "name": "newName", - "objectMetadataId": Any, - }, - "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, - "name": "New Name", - "objectMetadataId": Any, - }, - "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", diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/relation/failing-field-metadata-relation-update.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/field-metadata/relation/failing-field-metadata-relation-update.integration-spec.ts index 476e5bd9fd384..c68d90e4d7e96 100644 --- a/packages/twenty-server/test/integration/metadata/suites/field-metadata/relation/failing-field-metadata-relation-update.integration-spec.ts +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/relation/failing-field-metadata-relation-update.integration-spec.ts @@ -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 () => { diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/relation/successful-field-metadata-relation-update.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/field-metadata/relation/successful-field-metadata-relation-update.integration-spec.ts index 1a1543a399f64..0c8dadb85aff7 100644 --- a/packages/twenty-server/test/integration/metadata/suites/field-metadata/relation/successful-field-metadata-relation-update.integration-spec.ts +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/relation/successful-field-metadata-relation-update.integration-spec.ts @@ -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', () => { diff --git a/packages/twenty-server/test/integration/metadata/suites/object-metadata/morph-relation/__snapshots__/rename-object-metadata-with-morph-relation-v2.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/object-metadata/morph-relation/__snapshots__/rename-object-metadata-with-morph-relation-v2.integration-spec.ts.snap new file mode 100644 index 0000000000000..d7fc401c97611 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/object-metadata/morph-relation/__snapshots__/rename-object-metadata-with-morph-relation-v2.integration-spec.ts.snap @@ -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, + "fieldMetadataId": Any, + "id": Any, + "order": 0, + "updatedAt": Any, + }, + ], + "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, + "fieldMetadataId": Any, + "id": Any, + "order": 0, + "updatedAt": Any, + }, + ], + "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, + "fieldMetadataId": Any, + "id": Any, + "order": 0, + "updatedAt": Any, + }, + ], + "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, + "fieldMetadataId": Any, + "id": Any, + "order": 0, + "updatedAt": Any, + }, + ], + "indexType": "BTREE", + "isCustom": true, + "isUnique": false, + "name": "IDX_270c22872e92d429bb4f07a15dd", +} +`; diff --git a/packages/twenty-server/test/integration/metadata/suites/object-metadata/morph-relation/rename-object-metadata-with-morph-relation-v2.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/object-metadata/morph-relation/rename-object-metadata-with-morph-relation-v2.integration-spec.ts index a322eb567cc2e..35360589ee253 100644 --- a/packages/twenty-server/test/integration/metadata/suites/object-metadata/morph-relation/rename-object-metadata-with-morph-relation-v2.integration-spec.ts +++ b/packages/twenty-server/test/integration/metadata/suites/object-metadata/morph-relation/rename-object-metadata-with-morph-relation-v2.integration-spec.ts @@ -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); + } + }); +});