diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service-v2.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service-v2.ts index 059c53c6ddd..794d745db96 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service-v2.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service-v2.ts @@ -83,10 +83,14 @@ export class FieldMetadataServiceV2 { const flatObjectMetadataMapsWithImpactedObject = getSubFlatObjectMetadataMapsOrThrow({ flatObjectMetadataMaps: existingFlatObjectMetadataMaps, - objectMetadataIds: flatFieldMetadatasToDelete.map( - (flatFieldMetadataToDelete) => - flatFieldMetadataToDelete.objectMetadataId, - ), + objectMetadataIds: [ + ...new Set( + flatFieldMetadatasToDelete.map( + (flatFieldMetadataToDelete) => + flatFieldMetadataToDelete.objectMetadataId, + ), + ), + ], }); const toFlatObjectMetadataMaps = flatFieldMetadatasToDelete.reduce( @@ -145,20 +149,33 @@ export class FieldMetadataServiceV2 { throw inputTranspilationResult.error; } - const optimisticiallyUpdatedFlatFieldMetadata = + const optimisticallyUpdatedFlatFieldMetadatas = inputTranspilationResult.result; + const objectMetadataIdWithRelatedObjectMetadataIds = [ + ...new Set( + optimisticallyUpdatedFlatFieldMetadatas.flatMap( + ({ objectMetadataId, relationTargetObjectMetadataId }) => + isDefined(relationTargetObjectMetadataId) + ? [objectMetadataId, relationTargetObjectMetadataId] + : [objectMetadataId], + ), + ), + ]; const fromFlatObjectMetadataMaps = getSubFlatObjectMetadataMapsOrThrow({ flatObjectMetadataMaps: existingFlatObjectMetadataMaps, - objectMetadataIds: [ - optimisticiallyUpdatedFlatFieldMetadata.objectMetadataId, - ], + objectMetadataIds: objectMetadataIdWithRelatedObjectMetadataIds, }); + const toFlatObjectMetadataMaps = - replaceFlatFieldMetadataInFlatObjectMetadataMapsOrThrow({ - flatObjectMetadataMaps: fromFlatObjectMetadataMaps, - flatFieldMetadata: optimisticiallyUpdatedFlatFieldMetadata, - }); + optimisticallyUpdatedFlatFieldMetadatas.reduce( + (flatObjectMetadataMaps, flatFieldMetadata) => + replaceFlatFieldMetadataInFlatObjectMetadataMapsOrThrow({ + flatObjectMetadataMaps, + flatFieldMetadata, + }), + fromFlatObjectMetadataMaps, + ); const validateAndBuildResult = await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( @@ -182,7 +199,7 @@ export class FieldMetadataServiceV2 { return this.fieldMetadataRepository.findOneOrFail({ where: { - id: optimisticiallyUpdatedFlatFieldMetadata.id, + id: optimisticallyUpdatedFlatFieldMetadatas[0].id, workspaceId, }, }); diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-validator.service.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-validator.service.ts index 8334a78d8d6..83db46c1fa1 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-validator.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-validator.service.ts @@ -133,7 +133,7 @@ export class FlatFieldMetadataValidatorService { validationResult.errors.push( ...validateFlatFieldMetadataName(updatedFlatFieldMetadata.name), ...validateFlatFieldMetadataNameAvailability({ - name: updatedFlatFieldMetadata.name, + flatFieldMetadata: updatedFlatFieldMetadata, flatObjectMetadata: flatObjectMetadata, }), ); @@ -298,7 +298,7 @@ export class FlatFieldMetadataValidatorService { validationResult.errors.push( ...validateFlatFieldMetadataNameAvailability({ - name: flatFieldMetadataToValidate.name, + flatFieldMetadata: flatFieldMetadataToValidate, flatObjectMetadata: parentFlatObjectMetadata, }), ); 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 new file mode 100644 index 00000000000..9a68d841a4b --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/compute-flat-field-metadata-related-flat-field-metadata.util.ts @@ -0,0 +1,43 @@ +import { FieldMetadataType } from 'twenty-shared/types'; + +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 { isFlatFieldMetadataEntityOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util'; +import { type FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type'; + +export const computeFlatFieldMetadataRelatedFlatFieldMetadata = ({ + flatFieldMetadata, + flatObjectMetadataMaps, +}: { + flatFieldMetadata: FlatFieldMetadata; + flatObjectMetadataMaps: FlatObjectMetadataMaps; +}): FlatFieldMetadata[] => { + if ( + isFlatFieldMetadataEntityOfType( + flatFieldMetadata, + FieldMetadataType.RELATION, + ) + ) { + return [ + findRelationFlatFieldMetadataTargetFlatFieldMetadataOrThrow({ + flatFieldMetadata, + flatObjectMetadataMaps, + }), + ]; + } + + if ( + isFlatFieldMetadataEntityOfType( + flatFieldMetadata, + FieldMetadataType.MORPH_RELATION, + ) + ) { + return findFlatFieldMetadatasRelatedToMorphRelationOrThrow({ + flatFieldMetadata, + flatObjectMetadataMaps, + }); + } + + return []; +}; 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 4dca45a0a00..7c05880752b 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 @@ -62,6 +62,10 @@ export const findFlatFieldMetadatasRelatedToMorphRelationOrThrow = ({ flatObjectMetadataMaps, }); + if (flatFieldMetadata.id === morphRelationFlatFieldMetadata.id) { + return [relationTargetFlatFieldMetadata]; + } + return [flatFieldMetadata, relationTargetFlatFieldMetadata]; }, ); diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-delete-field-input-to-flat-field-metadatas-to-delete.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-delete-field-input-to-flat-field-metadatas-to-delete.util.ts index 954602ba214..c55d180b7a7 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-delete-field-input-to-flat-field-metadatas-to-delete.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-delete-field-input-to-flat-field-metadatas-to-delete.util.ts @@ -1,4 +1,3 @@ -import { FieldMetadataType } from 'twenty-shared/types'; import { isDefined, trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties, @@ -10,9 +9,7 @@ import { FieldMetadataExceptionCode, } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; 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 { isFlatFieldMetadataEntityOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util'; +import { computeFlatFieldMetadataRelatedFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/compute-flat-field-metadata-related-flat-field-metadata.util'; import { type FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type'; import { findFlatFieldMetadataInFlatObjectMetadataMapsWithOnlyFieldId } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/find-flat-field-metadata-in-flat-object-metadata-maps-with-field-id-only.util'; @@ -43,32 +40,11 @@ export const fromDeleteFieldInputToFlatFieldMetadatasToDelete = ({ ); } - if ( - isFlatFieldMetadataEntityOfType( - flatFieldMetadataToDelete, - FieldMetadataType.MORPH_RELATION, - ) - ) { - return findFlatFieldMetadatasRelatedToMorphRelationOrThrow({ - flatObjectMetadataMaps: existingFlatObjectMetadataMaps, + const relatedFlatFieldMetadataToDelete = + computeFlatFieldMetadataRelatedFlatFieldMetadata({ flatFieldMetadata: flatFieldMetadataToDelete, + flatObjectMetadataMaps: existingFlatObjectMetadataMaps, }); - } - if ( - isFlatFieldMetadataEntityOfType( - flatFieldMetadataToDelete, - FieldMetadataType.RELATION, - ) - ) { - const relationTargetFlatFieldMetadata = - findRelationFlatFieldMetadataTargetFlatFieldMetadataOrThrow({ - flatObjectMetadataMaps: existingFlatObjectMetadataMaps, - flatFieldMetadata: flatFieldMetadataToDelete, - }); - - return [flatFieldMetadataToDelete, relationTargetFlatFieldMetadata]; - } - - return [flatFieldMetadataToDelete]; + return [flatFieldMetadataToDelete, ...relatedFlatFieldMetadataToDelete]; }; 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 dbf103e110d..2f734640b3b 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 @@ -13,11 +13,36 @@ import { FLAT_FIELD_METADATA_PROPERTIES_TO_COMPARE } from 'src/engine/metadata-m import { type FieldInputTranspilationResult } from 'src/engine/metadata-modules/flat-field-metadata/types/field-input-transpilation-result.type'; import { type FlatFieldMetadataPropertiesToCompare } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-properties-to-compare.type'; import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; -import {} from 'src/engine/metadata-modules/flat-field-metadata/utils/compare-two-flat-field-metadata.util'; +import { computeFlatFieldMetadataRelatedFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/compute-flat-field-metadata-related-flat-field-metadata.util'; import { type FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type'; import { findFlatFieldMetadataInFlatObjectMetadataMapsWithOnlyFieldId } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/find-flat-field-metadata-in-flat-object-metadata-maps-with-field-id-only.util'; +import { fromFlatObjectMetadataWithFlatFieldMapsToFlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-flat-object-metadata-with-flat-field-maps-to-flat-object-metadatas.util'; import { isStandardMetadata } from 'src/engine/metadata-modules/utils/is-standard-metadata.util'; +const applyUpdatesToFlatFieldMetadata = ({ + updatedEditableFieldProperties, + flatFieldMetadata, +}: { + updatedEditableFieldProperties: SanitizedUpdateFieldInput; + flatFieldMetadata: FlatFieldMetadata; +}) => { + return fieldMetadataEditableProperties.reduce((acc, property) => { + let newValue = updatedEditableFieldProperties[property]; + + if (property === 'options' && isDefined(newValue)) { + newValue = updatedEditableFieldProperties[property]?.map((option) => ({ + id: v4(), + ...option, + })); + } + + return { + ...acc, + ...(newValue !== undefined ? { [property]: newValue } : {}), + }; + }, flatFieldMetadata); +}; + const fieldMetadataEditableProperties = FLAT_FIELD_METADATA_PROPERTIES_TO_COMPARE.filter( ( @@ -28,6 +53,13 @@ const fieldMetadataEditableProperties = > => property !== 'standardOverrides', ); +type SanitizedUpdateFieldInput = ReturnType< + typeof extractAndSanitizeObjectStringFields< + UpdateFieldInput, + (typeof fieldMetadataEditableProperties)[number][] + > +>; + type FromUpdateFieldInputToFlatFieldMetadataArgs = { existingFlatObjectMetadataMaps: FlatObjectMetadataMaps; updateFieldInput: UpdateFieldInput; @@ -35,7 +67,9 @@ type FromUpdateFieldInputToFlatFieldMetadataArgs = { export const fromUpdateFieldInputToFlatFieldMetadata = ({ existingFlatObjectMetadataMaps, updateFieldInput: rawUpdateFieldInput, -}: FromUpdateFieldInputToFlatFieldMetadataArgs): FieldInputTranspilationResult => { +}: FromUpdateFieldInputToFlatFieldMetadataArgs): FieldInputTranspilationResult< + FlatFieldMetadata[] +> => { const updateFieldInputInformalProperties = extractAndSanitizeObjectStringFields(rawUpdateFieldInput, [ 'objectMetadataId', @@ -46,13 +80,13 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({ fieldMetadataEditableProperties, ); - const relatedFlatFieldMetadata = + const existingFlatFieldMetadataToUpdate = findFlatFieldMetadataInFlatObjectMetadataMapsWithOnlyFieldId({ fieldMetadataId: updateFieldInputInformalProperties.id, flatObjectMetadataMaps: existingFlatObjectMetadataMaps, }); - if (!isDefined(relatedFlatFieldMetadata)) { + if (!isDefined(existingFlatFieldMetadataToUpdate)) { return { status: 'fail', error: { @@ -65,7 +99,7 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({ const flatObjectMetadataWithFlatFieldMaps = existingFlatObjectMetadataMaps.byId[ - relatedFlatFieldMetadata.objectMetadataId + existingFlatFieldMetadataToUpdate.objectMetadataId ]; if (!isDefined(flatObjectMetadataWithFlatFieldMaps)) { @@ -79,7 +113,12 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({ }; } - if (flatObjectMetadataWithFlatFieldMaps.isRemote) { + const flatObjectMetadata = + fromFlatObjectMetadataWithFlatFieldMapsToFlatObjectMetadata( + flatObjectMetadataWithFlatFieldMaps, + ); + + if (flatObjectMetadata.isRemote) { return { status: 'fail', error: { @@ -89,7 +128,7 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({ }; } - if (isStandardMetadata(relatedFlatFieldMetadata)) { + if (isStandardMetadata(existingFlatFieldMetadataToUpdate)) { const invalidUpdatedProperties = Object.keys( updatedEditableFieldProperties, ).filter((property) => @@ -125,35 +164,35 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({ : {}), }, }; - }, relatedFlatFieldMetadata); + }, existingFlatFieldMetadataToUpdate); return { status: 'success', - result: updatedStandardFlatFieldMetadata, + result: [updatedStandardFlatFieldMetadata], }; } - const updatedFlatFieldMetadata = fieldMetadataEditableProperties.reduce( - (acc, property) => { - let newValue = updatedEditableFieldProperties[property]; + const relatedFlatFieldMetadatasToUpdate = + computeFlatFieldMetadataRelatedFlatFieldMetadata({ + flatFieldMetadata: existingFlatFieldMetadataToUpdate, + flatObjectMetadataMaps: existingFlatObjectMetadataMaps, + }); - if (property === 'options' && isDefined(newValue)) { - newValue = updatedEditableFieldProperties[property]?.map((option) => ({ - id: v4(), - ...option, - })); - } + const flatFieldMetadatasToUpdate = [ + existingFlatFieldMetadataToUpdate, + ...relatedFlatFieldMetadatasToUpdate, + ]; - return { - ...acc, - ...(newValue !== undefined ? { [property]: newValue } : {}), - }; - }, - relatedFlatFieldMetadata, - ); + const optimisticiallyUpdatedFlatFieldMetadatas = + flatFieldMetadatasToUpdate.map((flatFieldMetadata) => { + return applyUpdatesToFlatFieldMetadata({ + flatFieldMetadata, + updatedEditableFieldProperties, + }); + }); return { status: 'success', - result: updatedFlatFieldMetadata, + result: optimisticiallyUpdatedFlatFieldMetadatas, }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-flat-field-metadata-name-availability.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-flat-field-metadata-name-availability.util.ts index 58d86921a4b..24585a7ed2f 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-flat-field-metadata-name-availability.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-flat-field-metadata-name-availability.util.ts @@ -6,6 +6,8 @@ import { FieldMetadataExceptionCode } from 'src/engine/metadata-modules/field-me import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; import { type FlatFieldMetadataValidationError } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-validation-error.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util'; import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { isMorphOrRelationFieldMetadataType } from 'src/engine/utils/is-morph-or-relation-field-metadata-type.util'; @@ -32,39 +34,43 @@ const getReservedCompositeFieldNames = ( }); }; +// Should implement Morph relation nameObjectId col availability export const validateFlatFieldMetadataNameAvailability = ({ - name, + flatFieldMetadata, flatObjectMetadata, }: { - name: string; + flatFieldMetadata: FlatFieldMetadata; flatObjectMetadata: FlatObjectMetadata; }): FlatFieldMetadataValidationError[] => { const errors: FlatFieldMetadataValidationError[] = []; const reservedCompositeFieldsNames = getReservedCompositeFieldNames(flatObjectMetadata); + const flatFieldMetadataName = flatFieldMetadata.name; + if ( + !isMorphOrRelationFlatFieldMetadata(flatFieldMetadata) && flatObjectMetadata.flatFieldMetadatas.some( (field) => - field.name === name || + field.name === flatFieldMetadataName || (isMorphOrRelationFieldMetadataType(field.type) && - `${field.name}Id` === name), + `${field.name}Id` === flatFieldMetadataName), ) ) { errors.push({ code: FieldMetadataExceptionCode.NOT_AVAILABLE, - value: name, - message: `Name "${name}" is not available as it is already used by another field`, - userFriendlyMessage: t`Name "${name}" is not available as it is already used by another field`, + value: flatFieldMetadataName, + message: `Name "${flatFieldMetadataName}" is not available as it is already used by another field`, + userFriendlyMessage: t`Name "${flatFieldMetadataName}" is not available as it is already used by another field`, }); } - if (reservedCompositeFieldsNames.includes(name)) { + if (reservedCompositeFieldsNames.includes(flatFieldMetadataName)) { errors.push({ code: FieldMetadataExceptionCode.RESERVED_KEYWORD, - message: `Name "${name}" is reserved composite field name`, - value: name, - userFriendlyMessage: t`Name "${name}" is not available`, + message: `Name "${flatFieldMetadataName}" is reserved composite field name`, + value: flatFieldMetadataName, + userFriendlyMessage: t`Name "${flatFieldMetadataName}" is not available`, }); } diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-delete-object-input-to-flat-field-metadatas-to-delete.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-delete-object-input-to-flat-field-metadatas-to-delete.util.ts index c96aeafd5c9..336f6c1d2ae 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-delete-object-input-to-flat-field-metadatas-to-delete.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-delete-object-input-to-flat-field-metadatas-to-delete.util.ts @@ -1,12 +1,10 @@ -import { FieldMetadataType } from 'twenty-shared/types'; import { isDefined, trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties, } from 'twenty-shared/utils'; import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; -import { findRelationFlatFieldMetadataTargetFlatFieldMetadataOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-relation-flat-field-metadatas-target-flat-field-metadata-or-throw.util'; -import { isFlatFieldMetadataEntityOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util'; +import { computeFlatFieldMetadataRelatedFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/compute-flat-field-metadata-related-flat-field-metadata.util'; import { type FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type'; import { findFlatObjectMetadataInFlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/find-flat-object-metadata-in-flat-object-metadata-maps.util'; import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; @@ -48,26 +46,13 @@ export const fromDeleteObjectInputToFlatFieldMetadatasToDelete = ({ const flatFieldMetadatasToDelete = flatObjectMetadataToDelete.flatFieldMetadatas.flatMap( (flatFieldMetadata) => { - if ( - isFlatFieldMetadataEntityOfType( + const relatedFlatFieldMetadata = + computeFlatFieldMetadataRelatedFlatFieldMetadata({ flatFieldMetadata, - FieldMetadataType.RELATION, - ) || - isFlatFieldMetadataEntityOfType( - flatFieldMetadata, - FieldMetadataType.MORPH_RELATION, - ) - ) { - const relationTargetFlatFieldMetadata = - findRelationFlatFieldMetadataTargetFlatFieldMetadataOrThrow({ - flatObjectMetadataMaps: existingFlatObjectMetadataMaps, - flatFieldMetadata, - }); + flatObjectMetadataMaps: existingFlatObjectMetadataMaps, + }); - return [flatFieldMetadata, relationTargetFlatFieldMetadata]; - } - - return flatFieldMetadata; + return [flatFieldMetadata, ...relatedFlatFieldMetadata]; }, ); diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/successful-create-one-field-metadata-morph-relation-v2.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/successful-create-one-field-metadata-morph-relation-v2.integration-spec.ts new file mode 100644 index 00000000000..26df0d101e6 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/successful-create-one-field-metadata-morph-relation-v2.integration-spec.ts @@ -0,0 +1,294 @@ +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 { updateOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/update-one-field-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 { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util'; +import { + eachTestingContextFilter, + type EachTestingContext, +} from 'twenty-shared/testing'; +import { FieldMetadataType } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; +import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; + +import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; + +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +import { type CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input'; +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'; +import { SEED_APPLE_WORKSPACE_ID } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-workspaces.util'; + +describe('createOne FieldMetadataService morph relation fields', () => { + let createdObjectMetadataPersonId: string; + let createdObjectMetadataOpportunityId: string; + let createdObjectMetadataCompanyId: string; + let createdFieldMetadataId: string | undefined = undefined; + + beforeAll(async () => { + await updateFeatureFlag({ + expectToFail: false, + featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED, + value: true, + workspaceId: SEED_APPLE_WORKSPACE_ID, + }); + + const { + data: { + createOneObject: { id: objectMetadataPersonId }, + }, + } = await createOneObjectMetadata({ + expectToFail: false, + input: { + nameSingular: 'personForMorphRelationSecond', + namePlural: 'peopleForMorphRelationSecond', + labelSingular: 'Person For Morph Relation', + labelPlural: 'People For Morph Relation', + icon: 'IconPerson', + }, + }); + + createdObjectMetadataPersonId = objectMetadataPersonId; + + const { + data: { + createOneObject: { id: objectMetadataCompanyId }, + }, + } = await createOneObjectMetadata({ + expectToFail: false, + input: { + nameSingular: 'companyForMorphRelationSecond', + namePlural: 'companiesForMorphRelationSecond', + labelSingular: 'Company For Morph Relation', + labelPlural: 'Companies For Morph Relation', + icon: 'IconCompany', + }, + }); + + createdObjectMetadataCompanyId = objectMetadataCompanyId; + + const { + data: { + createOneObject: { id: objectMetadataOpportunityId }, + }, + } = await createOneObjectMetadata({ + expectToFail: false, + input: { + nameSingular: 'opportunityForMorphRelationSecond', + namePlural: 'opportunitiesForMorphRelationSecond', + labelSingular: 'Opportunity For Morph Relation', + labelPlural: 'Opportunities For Morph Relation', + icon: 'IconOpportunity', + }, + }); + + createdObjectMetadataOpportunityId = objectMetadataOpportunityId; + }); + + afterAll(async () => { + await Promise.all( + [ + createdObjectMetadataPersonId, + createdObjectMetadataOpportunityId, + createdObjectMetadataCompanyId, + ].map( + async (objectMetadataId) => + await updateOneObjectMetadata({ + expectToFail: false, + input: { + idToUpdate: objectMetadataId, + updatePayload: { isActive: false }, + }, + }), + ), + ); + + await Promise.all( + [ + createdObjectMetadataPersonId, + createdObjectMetadataOpportunityId, + createdObjectMetadataCompanyId, + ].map( + async (objectMetadataId) => + await deleteOneObjectMetadata({ + expectToFail: false, + input: { idToDelete: objectMetadataId }, + }), + ), + ); + + await updateFeatureFlag({ + expectToFail: false, + featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED, + value: false, + workspaceId: SEED_APPLE_WORKSPACE_ID, + }); + }); + + afterEach(async () => { + if (!isDefined(createdFieldMetadataId)) { + return; + } + + await updateOneFieldMetadata({ + input: { + idToUpdate: createdFieldMetadataId, + updatePayload: { + isActive: false, + }, + }, + expectToFail: false, + }); + + await deleteOneFieldMetadata({ + input: { + idToDelete: createdFieldMetadataId, + }, + expectToFail: false, + }); + + createdFieldMetadataId = undefined; + }); + + type EachTestingContextArray = EachTestingContext< + (args: { + createdObjectMetadataPersonId: string; + createdObjectMetadataOpportunityId: string; + createdObjectMetadataCompanyId: string; + }) => Omit< + CreateFieldInput, + 'morphRelationsCreationPayload' | 'workspaceId' + > & + Required> + >[]; + + const eachTestingContextArray: EachTestingContextArray = [ + { + title: 'should create a MORPH_RELATION field type MANY_TO_ONE', + context: ({ + createdObjectMetadataCompanyId, + createdObjectMetadataOpportunityId, + createdObjectMetadataPersonId, + }) => ({ + label: 'field label', + name: 'fieldName', + objectMetadataId: createdObjectMetadataCompanyId, + type: FieldMetadataType.MORPH_RELATION, + morphRelationsCreationPayload: [ + { + targetFieldIcon: 'Icon123', + targetFieldLabel: 'toto', + targetObjectMetadataId: createdObjectMetadataOpportunityId, + type: RelationType.MANY_TO_ONE, + }, + { + targetFieldIcon: 'Icon123', + targetFieldLabel: 'tata', + targetObjectMetadataId: createdObjectMetadataPersonId, + type: RelationType.MANY_TO_ONE, + }, + ], + }), + }, + { + title: 'should create a MORPH_RELATION field type ONE_TO_MANY', + context: ({ + createdObjectMetadataCompanyId, + createdObjectMetadataOpportunityId, + createdObjectMetadataPersonId, + }) => ({ + label: 'field label', + name: 'fieldName', + objectMetadataId: createdObjectMetadataCompanyId, + type: FieldMetadataType.MORPH_RELATION, + morphRelationsCreationPayload: [ + { + targetFieldIcon: 'Icon123', + targetFieldLabel: 'toto', + targetObjectMetadataId: createdObjectMetadataOpportunityId, + type: RelationType.ONE_TO_MANY, + }, + { + targetFieldIcon: 'Icon123', + targetFieldLabel: 'tata', + targetObjectMetadataId: createdObjectMetadataPersonId, + type: RelationType.ONE_TO_MANY, + }, + ], + }), + }, + ]; + + it.each(eachTestingContextFilter(eachTestingContextArray))( + '$title', + async ({ context }) => { + const contextPayload = context({ + createdObjectMetadataOpportunityId, + createdObjectMetadataPersonId, + createdObjectMetadataCompanyId, + }); + + const { + data: { createOneField: rawCreateOneField }, + } = await createOneFieldMetadata({ + input: contextPayload, + expectToFail: false, + gqlFields: ` + id + name + label + isLabelSyncedWithName + settings + object { + id + nameSingular + } + morphRelations { + type + targetFieldMetadata { + id + } + targetObjectMetadata { + id + } + sourceFieldMetadata { + id + } + sourceObjectMetadata { + id + } + } + `, + }); + const createOneField = rawCreateOneField as FieldMetadataDTO & { + morphRelations: RelationDTO[]; + }; + + createdFieldMetadataId = createOneField.id; + + const expectedMorphRelations = + contextPayload.morphRelationsCreationPayload.map( + ({ targetObjectMetadataId, type }) => ({ + sourceFieldMetadata: { + id: expect.any(String), + }, + sourceObjectMetadata: { + id: contextPayload.objectMetadataId, + }, + targetFieldMetadata: { + id: expect.any(String), + }, + targetObjectMetadata: { + id: targetObjectMetadataId, + }, + type, + }), + ); + + expect(createOneField.morphRelations).toMatchObject( + expectedMorphRelations, + ); + }, + ); +}); 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 new file mode 100644 index 00000000000..9b337b071b2 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/successful-update-one-field-metadata-morph-relation-v2.integration-spec.ts @@ -0,0 +1,183 @@ +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 { updateOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/update-one-field-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 { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util'; +import { FieldMetadataType } from 'twenty-shared/types'; +import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; + +import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; + +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +import { SEED_APPLE_WORKSPACE_ID } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-workspaces.util'; + +describe('updateOne FieldMetadataService morph relation fields v2', () => { + let createdObjectMetadataPersonId: string; + let createdObjectMetadataOpportunityId: string; + let createdObjectMetadataCompanyId: string; + let createdFieldMetadataId: string; + + beforeAll(async () => { + await updateFeatureFlag({ + expectToFail: false, + featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED, + value: true, + workspaceId: SEED_APPLE_WORKSPACE_ID, + }); + + const { + data: { + createOneObject: { id: objectMetadataPersonId }, + }, + } = await createOneObjectMetadata({ + expectToFail: false, + input: { + nameSingular: 'personForMorphRelationSecond', + namePlural: 'peopleForMorphRelationSecond', + labelSingular: 'Person For Morph Relation', + labelPlural: 'People For Morph Relation', + icon: 'IconPerson', + }, + }); + + createdObjectMetadataPersonId = objectMetadataPersonId; + + const { + data: { + createOneObject: { id: objectMetadataCompanyId }, + }, + } = await createOneObjectMetadata({ + expectToFail: false, + input: { + nameSingular: 'companyForMorphRelationSecond', + namePlural: 'companiesForMorphRelationSecond', + labelSingular: 'Company For Morph Relation', + labelPlural: 'Companies For Morph Relation', + icon: 'IconCompany', + }, + }); + + createdObjectMetadataCompanyId = objectMetadataCompanyId; + + const { + data: { + createOneObject: { id: objectMetadataOpportunityId }, + }, + } = await createOneObjectMetadata({ + expectToFail: false, + input: { + nameSingular: 'opportunityForMorphRelationSecond', + namePlural: 'opportunitiesForMorphRelationSecond', + labelSingular: 'Opportunity For Morph Relation', + labelPlural: 'Opportunities For Morph Relation', + icon: 'IconOpportunity', + }, + }); + + createdObjectMetadataOpportunityId = objectMetadataOpportunityId; + }); + + afterAll(async () => { + await Promise.all( + [ + createdObjectMetadataPersonId, + createdObjectMetadataOpportunityId, + createdObjectMetadataCompanyId, + ].map( + async (objectMetadataId) => + await updateOneObjectMetadata({ + expectToFail: false, + input: { + idToUpdate: objectMetadataId, + updatePayload: { isActive: false }, + }, + }), + ), + ); + + await Promise.all( + [ + createdObjectMetadataPersonId, + createdObjectMetadataOpportunityId, + createdObjectMetadataCompanyId, + ].map( + async (objectMetadataId) => + await deleteOneObjectMetadata({ + expectToFail: false, + input: { idToDelete: objectMetadataId }, + }), + ), + ); + + await updateFeatureFlag({ + expectToFail: false, + featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED, + value: false, + workspaceId: SEED_APPLE_WORKSPACE_ID, + }); + }); + + beforeEach(async () => { + const { + data: { createOneField: rawCreateOneField }, + } = await createOneFieldMetadata({ + input: { + label: 'field label', + name: 'fieldName', + objectMetadataId: createdObjectMetadataCompanyId, + type: FieldMetadataType.MORPH_RELATION, + morphRelationsCreationPayload: [ + { + targetFieldIcon: 'Icon123', + targetFieldLabel: 'toto', + targetObjectMetadataId: createdObjectMetadataOpportunityId, + type: RelationType.MANY_TO_ONE, + }, + { + targetFieldIcon: 'Icon123', + targetFieldLabel: 'tata', + targetObjectMetadataId: createdObjectMetadataPersonId, + type: RelationType.MANY_TO_ONE, + }, + ], + }, + expectToFail: false, + }); + + createdFieldMetadataId = rawCreateOneField.id; + }); + + it('It should update all morph related flat field metadata allowing its deletion', async () => { + const input = { + idToUpdate: createdFieldMetadataId, + updatePayload: { + isActive: false, + label: 'new label', + description: 'new description', + }, + }; + const { + data: { updateOneField }, + } = await updateOneFieldMetadata({ + expectToFail: false, + input, + gqlFields: ` + id + isActive + description + name + label + `, + }); + + expect(updateOneField).toMatchObject(input.updatePayload); + + await deleteOneFieldMetadata({ + input: { + idToDelete: createdFieldMetadataId, + }, + expectToFail: false, + }); + }); +});