diff --git a/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsUpdateDataModelObjectAboutForm.tsx b/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsUpdateDataModelObjectAboutForm.tsx index 63de1b3d3f..720a21d7fa 100644 --- a/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsUpdateDataModelObjectAboutForm.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsUpdateDataModelObjectAboutForm.tsx @@ -95,8 +95,9 @@ export const SettingsUpdateDataModelObjectAboutForm = ({ if (!objectMetadataItem.isCustom) { const { - nameSingular: _, - namePlural: __, + nameSingular: _nameSingular, + namePlural: _namePlural, + isLabelSyncedWithName: _isLabelSyncedWithName, ...payloadWithoutNames } = updatePayload; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/constants/field-metadata-standard-overrides-properties.constant.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/constants/field-metadata-standard-overrides-properties.constant.ts new file mode 100644 index 0000000000..14479c7575 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/constants/field-metadata-standard-overrides-properties.constant.ts @@ -0,0 +1,7 @@ +import { type FlatFieldMetadataPropertiesToCompare } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-properties-to-compare.type'; + +export const FIELD_METADATA_STANDARD_OVERRIDES_PROPERTIES = [ + 'label', + 'description', + 'icon', +] as const satisfies FlatFieldMetadataPropertiesToCompare[]; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/field-standard-overrides.dto.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/field-standard-overrides.dto.ts index ce8c1023a9..70faec71f5 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/field-standard-overrides.dto.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/field-standard-overrides.dto.ts @@ -4,16 +4,8 @@ import { IsJSON, IsOptional, IsString } from 'class-validator'; import { GraphQLJSON } from 'graphql-type-json'; import { type APP_LOCALES } from 'twenty-shared/translations'; -import { FlatFieldMetadataPropertiesToCompare } from 'src/engine/metadata-modules/flat-field-metadata/utils/compare-two-flat-field-metadata.util'; +import { FieldMetadataStandardOverridesProperties } from 'src/engine/metadata-modules/field-metadata/types/field-metadata-standard-overrides-properties.type'; -export const fieldMetadataStandardOverridesProperties = [ - 'label', - 'description', - 'icon', -] as const satisfies FlatFieldMetadataPropertiesToCompare[]; - -export type FieldMetadataStandardOverridesProperties = - (typeof fieldMetadataStandardOverridesProperties)[number]; @ObjectType('StandardOverrides') export class FieldStandardOverridesDTO implements Partial> 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 cc6931d2ad..335e7f76d7 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 @@ -24,7 +24,7 @@ import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-m import { fromCreateFieldInputToFlatFieldMetadatasToCreate } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-create-field-input-to-flat-field-metadatas-to-create.util'; import { fromDeleteFieldInputToFlatFieldMetadatasToDelete } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-delete-field-input-to-flat-field-metadatas-to-delete.util'; import { fromFlatFieldMetadataToFieldMetadataDto } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-flat-field-metadata-to-field-metadata-dto.util'; -import { fromUpdateFieldInputToFlatFieldMetadataToUpdate } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-update-field-input-to-flat-field-metadata-to-update.util'; +import { fromUpdateFieldInputToFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-update-field-input-to-flat-field-metadata.util'; import { isFlatFieldMetadataEntityOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util'; import { throwOnFieldInputTranspilationsError } from 'src/engine/metadata-modules/flat-field-metadata/utils/throw-on-field-input-transpilations-error.util'; import { type FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type'; @@ -213,11 +213,10 @@ export class FieldMetadataServiceV2 { { workspaceId }, ); - const inputTranspilationResult = - fromUpdateFieldInputToFlatFieldMetadataToUpdate({ - existingFlatObjectMetadataMaps, - updateFieldInput, - }); + const inputTranspilationResult = fromUpdateFieldInputToFlatFieldMetadata({ + existingFlatObjectMetadataMaps, + updateFieldInput, + }); if (inputTranspilationResult.status === 'fail') { throw inputTranspilationResult.error; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/types/field-metadata-standard-overrides-properties.type.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/types/field-metadata-standard-overrides-properties.type.ts new file mode 100644 index 0000000000..e87cf90d49 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/types/field-metadata-standard-overrides-properties.type.ts @@ -0,0 +1,4 @@ +import { type FIELD_METADATA_STANDARD_OVERRIDES_PROPERTIES } from 'src/engine/metadata-modules/field-metadata/constants/field-metadata-standard-overrides-properties.constant'; + +export type FieldMetadataStandardOverridesProperties = + (typeof FIELD_METADATA_STANDARD_OVERRIDES_PROPERTIES)[number]; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-jsonb-properties.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-jsonb-properties.constant.ts new file mode 100644 index 0000000000..1e2b31013f --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-jsonb-properties.constant.ts @@ -0,0 +1,8 @@ +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; + +export const FLAT_FIELD_METADATA_JSONB_PROPERTIES = [ + 'defaultValue', + 'options', + 'settings', + 'standardOverrides', +] as const satisfies (keyof FlatFieldMetadata)[]; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-properties-to-compare.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-properties-to-compare.constant.ts new file mode 100644 index 0000000000..1b8a9b7ec0 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-properties-to-compare.constant.ts @@ -0,0 +1,15 @@ +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; + +export const FLAT_FIELD_METADATA_PROPERTIES_TO_COMPARE = [ + 'defaultValue', + 'description', + 'icon', + 'isActive', + 'isLabelSyncedWithName', + 'isUnique', + 'label', + 'name', + 'options', + 'standardOverrides', + 'settings', +] 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 new file mode 100644 index 0000000000..6749fb43d3 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-relation-properties-to-compare.constant.ts @@ -0,0 +1,7 @@ +import { type FlatFieldMetadataPropertiesToCompare } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-properties-to-compare.type'; + +export const FLAT_FIELD_METADATA_RELATION_PROPERTIES_TO_COMPARE = [ + 'label', + 'description', + 'isActive', +] as const satisfies FlatFieldMetadataPropertiesToCompare[]; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-type-validator.service.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-type-validator.service.ts index ef32f35101..e8f0d54ca5 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-type-validator.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-type-validator.service.ts @@ -13,8 +13,8 @@ import { ValidateOneFieldMetadataArgs } from 'src/engine/metadata-modules/flat-f import { FailedFlatFieldMetadataValidationExceptions } from 'src/engine/metadata-modules/flat-field-metadata/types/failed-flat-field-metadata-validation.type'; import { type FlatFieldMetadataTypeValidator } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-type-validator.type'; import { isEnumValidateOneFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-enum-validate-one-field-metadata-args.util'; -import { validateEnumSelectFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/validators/validate-enum-flat-field-metadata.validator'; -import { validateRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/validators/validate-relation-flat-field-metadata.validator'; +import { validateEnumSelectFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-enum-flat-field-metadata.util'; +import { validateRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-relation-flat-field-metadata.util'; @Injectable() export class FlatFieldMetadataTypeValidatorService { constructor(private readonly featureFlagService: FeatureFlagService) {} 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 4b61b44ba9..af9bf51967 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 @@ -8,16 +8,16 @@ import { FieldMetadataException, FieldMetadataExceptionCode, } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; +import { FLAT_FIELD_METADATA_RELATION_PROPERTIES_TO_COMPARE } from 'src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-relation-properties-to-compare.constant'; import { FlatFieldMetadataTypeValidatorService } from 'src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-type-validator.service'; import { type FailedFlatFieldMetadataValidationExceptions } from 'src/engine/metadata-modules/flat-field-metadata/types/failed-flat-field-metadata-validation.type'; +import { FlatFieldMetadataRelationPropertiesToCompare } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-relation-properties-to-compare.type'; import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; -import { - compareTwoFlatFieldMetadata, - type FlatFieldMetadataPropertiesToCompare, -} from 'src/engine/metadata-modules/flat-field-metadata/utils/compare-two-flat-field-metadata.util'; +import { compareTwoFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/compare-two-flat-field-metadata.util'; +import { isFlatFieldMetadataNameSyncedWithLabel } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-name-synced-with-label.util'; import { isFlatFieldMetadataEntityOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util'; -import { validateFlatFieldMetadataNameAvailability } from 'src/engine/metadata-modules/flat-field-metadata/validators/validate-flat-field-metadata-name-availability.validator'; -import { validateFlatFieldMetadataName } from 'src/engine/metadata-modules/flat-field-metadata/validators/validate-flat-field-metadata-name.validator'; +import { validateFlatFieldMetadataNameAvailability } from 'src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-flat-field-metadata-name-availability.util'; +import { validateFlatFieldMetadataName } from 'src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-flat-field-metadata-name.util'; import { type FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type'; 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 { @@ -28,7 +28,6 @@ import { InvalidMetadataException, InvalidMetadataExceptionCode, } from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception'; -import { computeMetadataNameFromLabel } from 'src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util'; export type ValidateOneFieldMetadataArgs< T extends FieldMetadataType = FieldMetadataType, @@ -110,15 +109,10 @@ export class FlatFieldMetadataValidatorService { FieldMetadataType.MORPH_RELATION, ) ) { - const relationEditableFields = [ - 'label', - 'description', - 'isActive', - ] as const satisfies FlatFieldMetadataPropertiesToCompare[]; const relationNonEditableUpdatedProperties = updates.flatMap( ({ property }) => - !relationEditableFields.includes( - property as (typeof relationEditableFields)[number], + !FLAT_FIELD_METADATA_RELATION_PROPERTIES_TO_COMPARE.includes( + property as FlatFieldMetadataRelationPropertiesToCompare, ) ? property : [], @@ -155,19 +149,16 @@ export class FlatFieldMetadataValidatorService { } } - if (updatedFlatFieldMetadata.isLabelSyncedWithName) { - const computedName = computeMetadataNameFromLabel( - updatedFlatFieldMetadata.label, + if ( + updatedFlatFieldMetadata.isLabelSyncedWithName && + !isFlatFieldMetadataNameSyncedWithLabel(updatedFlatFieldMetadata) + ) { + errors.push( + new InvalidMetadataException( + t`Name is not synced with label.`, + InvalidMetadataExceptionCode.NAME_NOT_SYNCED_WITH_LABEL, + ), ); - - if (updatedFlatFieldMetadata.name !== computedName) { - errors.push( - new InvalidMetadataException( - `Name is not synced with label. Expected name: "${computedName}", got ${updatedFlatFieldMetadata.name}`, - InvalidMetadataExceptionCode.NAME_NOT_SYNCED_WITH_LABEL, - ), - ); - } } const fieldMetadataTypeValidationErrors = @@ -295,19 +286,16 @@ export class FlatFieldMetadataValidatorService { } } - if (flatFieldMetadataToValidate.isLabelSyncedWithName) { - const computedName = computeMetadataNameFromLabel( - flatFieldMetadataToValidate.label, + if ( + flatFieldMetadataToValidate.isLabelSyncedWithName && + !isFlatFieldMetadataNameSyncedWithLabel(flatFieldMetadataToValidate) + ) { + errors.push( + new InvalidMetadataException( + t`Name is not synced with label.`, + InvalidMetadataExceptionCode.NAME_NOT_SYNCED_WITH_LABEL, + ), ); - - if (flatFieldMetadataToValidate.name !== computedName) { - errors.push( - new InvalidMetadataException( - `Name is not synced with label. Expected name: "${computedName}", got ${flatFieldMetadataToValidate.name}`, - InvalidMetadataExceptionCode.NAME_NOT_SYNCED_WITH_LABEL, - ), - ); - } } errors.push( diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-jsonb-properties.type.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-jsonb-properties.type.ts new file mode 100644 index 0000000000..9cb5af339b --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-jsonb-properties.type.ts @@ -0,0 +1,4 @@ +import { type FLAT_FIELD_METADATA_JSONB_PROPERTIES } from 'src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-jsonb-properties.constant'; + +export type FlatFieldMetadataEntityJsonbProperties = + (typeof FLAT_FIELD_METADATA_JSONB_PROPERTIES)[number]; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-properties-to-compare.type.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-properties-to-compare.type.ts new file mode 100644 index 0000000000..a0aa0835c8 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-properties-to-compare.type.ts @@ -0,0 +1,4 @@ +import { type FLAT_FIELD_METADATA_PROPERTIES_TO_COMPARE } from 'src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-properties-to-compare.constant'; + +export type FlatFieldMetadataPropertiesToCompare = + (typeof FLAT_FIELD_METADATA_PROPERTIES_TO_COMPARE)[number]; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-relation-properties-to-compare.type.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-relation-properties-to-compare.type.ts new file mode 100644 index 0000000000..2413cf34bf --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-relation-properties-to-compare.type.ts @@ -0,0 +1,4 @@ +import { type FLAT_FIELD_METADATA_RELATION_PROPERTIES_TO_COMPARE } from 'src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-relation-properties-to-compare.constant'; + +export type FlatFieldMetadataRelationPropertiesToCompare = + (typeof FLAT_FIELD_METADATA_RELATION_PROPERTIES_TO_COMPARE)[number]; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/compare-two-flat-field-metadata.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/compare-two-flat-field-metadata.util.ts index 14c38cc97f..9701e7e6d5 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/compare-two-flat-field-metadata.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/compare-two-flat-field-metadata.util.ts @@ -2,36 +2,17 @@ import diff from 'microdiff'; import { FieldMetadataType, type FromTo } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; +import { FLAT_FIELD_METADATA_JSONB_PROPERTIES } from 'src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-jsonb-properties.constant'; +import { FLAT_FIELD_METADATA_PROPERTIES_TO_COMPARE } from 'src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-properties-to-compare.constant'; +import { FLAT_FIELD_METADATA_RELATION_PROPERTIES_TO_COMPARE } from 'src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-relation-properties-to-compare.constant'; +import { type FlatFieldMetadataEntityJsonbProperties } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-jsonb-properties.type'; +import { type FlatFieldMetadataPropertiesToCompare } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-properties-to-compare.type'; +import { type FlatFieldMetadataRelationPropertiesToCompare } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-relation-properties-to-compare.type'; import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { isStandardMetadata } from 'src/engine/metadata-modules/utils/is-standard-metadata.util'; import { type UpdateFieldAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-field-action-v2'; import { transformMetadataForComparison } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/utils/transform-metadata-for-comparison.util'; -export const flatFieldMetadataPropertiesToCompare = [ - 'defaultValue', - 'description', - 'icon', - 'isActive', - 'isLabelSyncedWithName', - 'isUnique', - 'label', - 'name', - 'options', - 'standardOverrides', - 'settings', -] as const satisfies (keyof FlatFieldMetadata)[]; - -export type FlatFieldMetadataPropertiesToCompare = - (typeof flatFieldMetadataPropertiesToCompare)[number]; - -export const flatFieldMetadataEntityJsonbProperties = [ - 'defaultValue', - 'options', - 'settings', - 'standardOverrides', -] as const satisfies (keyof FlatFieldMetadata)[]; -export type FlatFieldMetadataEntityJsonbProperties = - (typeof flatFieldMetadataEntityJsonbProperties)[number]; - const shouldNotOverrideDefaultValue = (type: FieldMetadataType) => { return [ FieldMetadataType.BOOLEAN, @@ -51,13 +32,13 @@ export const compareTwoFlatFieldMetadata = ({ from, to, }: GetWorkspaceMigrationUpdateFieldActionArgs) => { - const compareFieldMetadataOptions = { + const transformMetadataForComparisonParameters = { shouldIgnoreProperty: ( property: string, fieldMetadata: FlatFieldMetadata, ) => { if ( - !flatFieldMetadataPropertiesToCompare.includes( + !FLAT_FIELD_METADATA_PROPERTIES_TO_COMPARE.includes( property as FlatFieldMetadataPropertiesToCompare, ) ) { @@ -73,26 +54,38 @@ export const compareTwoFlatFieldMetadata = ({ } // Remove below assertion when we authorize relation edition, see https://github.com/twentyhq/twenty/commit/39f6f3c4bb101272a9014e142a842d0801a3c33b - if ( + const isRelationFieldType = isDefined(fieldMetadata.type) && (fieldMetadata.type === FieldMetadataType.RELATION || - fieldMetadata.type === FieldMetadataType.MORPH_RELATION) && - !['label', 'description', 'isActive'].includes(property) + fieldMetadata.type === FieldMetadataType.MORPH_RELATION); + + if ( + isRelationFieldType && + !FLAT_FIELD_METADATA_RELATION_PROPERTIES_TO_COMPARE.includes( + property as FlatFieldMetadataRelationPropertiesToCompare, + ) + ) { + return true; + } + + if ( + isStandardMetadata(fieldMetadata) && + property !== 'standardOverrides' ) { return true; } return false; }, - propertiesToStringify: flatFieldMetadataEntityJsonbProperties, + propertiesToStringify: FLAT_FIELD_METADATA_JSONB_PROPERTIES, }; const fromCompare = transformMetadataForComparison( from, - compareFieldMetadataOptions, + transformMetadataForComparisonParameters, ); const toCompare = transformMetadataForComparison( to, - compareFieldMetadataOptions, + transformMetadataForComparisonParameters, ); const flatFieldMetadataDifferences = diff(fromCompare, toCompare); @@ -104,7 +97,7 @@ export const compareTwoFlatFieldMetadata = ({ case 'CHANGE': { const { oldValue, path, value } = difference; const property = path[0] as FlatFieldMetadataPropertiesToCompare; - const isJsonb = flatFieldMetadataEntityJsonbProperties.includes( + const isJsonb = FLAT_FIELD_METADATA_JSONB_PROPERTIES.includes( property as FlatFieldMetadataEntityJsonbProperties, ); diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-update-field-input-to-flat-field-metadata-to-update.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-update-field-input-to-flat-field-metadata.util.ts similarity index 67% rename from packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-update-field-input-to-flat-field-metadata-to-update.util.ts rename to packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-update-field-input-to-flat-field-metadata.util.ts index f4109c2dac..6e0b375a47 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-update-field-input-to-flat-field-metadata-to-update.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-update-field-input-to-flat-field-metadata.util.ts @@ -4,30 +4,28 @@ import { trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties, } from 'twenty-shared/utils'; -import { - fieldMetadataStandardOverridesProperties, - type FieldMetadataStandardOverridesProperties, -} from 'src/engine/metadata-modules/field-metadata/dtos/field-standard-overrides.dto'; +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'; import { FieldMetadataException, FieldMetadataExceptionCode, } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; +import { type FieldMetadataStandardOverridesProperties } from 'src/engine/metadata-modules/field-metadata/types/field-metadata-standard-overrides-properties.type'; +import { FLAT_FIELD_METADATA_PROPERTIES_TO_COMPARE } from 'src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-properties-to-compare.constant'; 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 { - flatFieldMetadataPropertiesToCompare, - type FlatFieldMetadataPropertiesToCompare, -} from 'src/engine/metadata-modules/flat-field-metadata/utils/compare-two-flat-field-metadata.util'; +import {} from 'src/engine/metadata-modules/flat-field-metadata/utils/compare-two-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 { ObjectMetadataException, ObjectMetadataExceptionCode, } from 'src/engine/metadata-modules/object-metadata/object-metadata.exception'; +import { isStandardMetadata } from 'src/engine/metadata-modules/utils/is-standard-metadata.util'; const fieldMetadataEditableProperties = - flatFieldMetadataPropertiesToCompare.filter( + FLAT_FIELD_METADATA_PROPERTIES_TO_COMPARE.filter( ( property, ): property is Exclude< @@ -36,20 +34,20 @@ const fieldMetadataEditableProperties = > => property !== 'standardOverrides', ); -type FromUpdateFieldInputToFlatFieldMetadataToUpdateArgs = { +type FromUpdateFieldInputToFlatFieldMetadataArgs = { existingFlatObjectMetadataMaps: FlatObjectMetadataMaps; updateFieldInput: UpdateFieldInput; }; -export const fromUpdateFieldInputToFlatFieldMetadataToUpdate = ({ +export const fromUpdateFieldInputToFlatFieldMetadata = ({ existingFlatObjectMetadataMaps, updateFieldInput: rawUpdateFieldInput, -}: FromUpdateFieldInputToFlatFieldMetadataToUpdateArgs): FieldInputTranspilationResult => { +}: FromUpdateFieldInputToFlatFieldMetadataArgs): FieldInputTranspilationResult => { const updateFieldInputInformalProperties = trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties( rawUpdateFieldInput, ['objectMetadataId', 'id'], ); - const updatedEditableFields = extractAndSanitizeObjectStringFields( + const updatedEditableFieldProperties = extractAndSanitizeObjectStringFields( rawUpdateFieldInput, fieldMetadataEditableProperties, ); @@ -94,16 +92,14 @@ export const fromUpdateFieldInputToFlatFieldMetadataToUpdate = ({ ), }; } - const isStandardField = - relatedFlatFieldMetadata.standardId !== null && - !relatedFlatFieldMetadata.isCustom; - if (isStandardField) { - const invalidUpdatedProperties = Object.keys(updatedEditableFields).filter( - (property) => - fieldMetadataStandardOverridesProperties.includes( - property as FieldMetadataStandardOverridesProperties, - ), + if (isStandardMetadata(relatedFlatFieldMetadata)) { + const invalidUpdatedProperties = Object.keys( + updatedEditableFieldProperties, + ).filter((property) => + FIELD_METADATA_STANDARD_OVERRIDES_PROPERTIES.includes( + property as FieldMetadataStandardOverridesProperties, + ), ); if (invalidUpdatedProperties.length > 0) { @@ -117,15 +113,16 @@ export const fromUpdateFieldInputToFlatFieldMetadataToUpdate = ({ } const updatedStandardFlatFieldMetadata = - fieldMetadataStandardOverridesProperties.reduce((acc, property) => { - const isPropertyUpdated = updatedEditableFields[property] !== undefined; + FIELD_METADATA_STANDARD_OVERRIDES_PROPERTIES.reduce((acc, property) => { + const isPropertyUpdated = + updatedEditableFieldProperties[property] !== undefined; return { ...acc, standardOverrides: { ...acc.standardOverrides, ...(isPropertyUpdated - ? { [property]: updatedEditableFields[property] } + ? { [property]: updatedEditableFieldProperties[property] } : {}), }, }; @@ -139,12 +136,13 @@ export const fromUpdateFieldInputToFlatFieldMetadataToUpdate = ({ const updatedFlatFieldMetadata = fieldMetadataEditableProperties.reduce( (acc, property) => { - const isPropertyUpdated = updatedEditableFields[property] !== undefined; + const isPropertyUpdated = + updatedEditableFieldProperties[property] !== undefined; return { ...acc, ...(isPropertyUpdated - ? { [property]: updatedEditableFields[property] } + ? { [property]: updatedEditableFieldProperties[property] } : {}), }; }, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-name-synced-with-label.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-name-synced-with-label.util.ts new file mode 100644 index 0000000000..5483c69015 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-name-synced-with-label.util.ts @@ -0,0 +1,13 @@ +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { computeMetadataNameFromLabel } from 'src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util'; + +export const isFlatFieldMetadataNameSyncedWithLabel = ( + flatFieldMetadata: Pick< + FlatFieldMetadata, + 'name' | 'isLabelSyncedWithName' | 'label' + >, +) => { + const computedName = computeMetadataNameFromLabel(flatFieldMetadata.label); + + return flatFieldMetadata.name === computedName; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/validate-enum-flat-field-metadata.validator.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-enum-flat-field-metadata.util.ts similarity index 100% rename from packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/validate-enum-flat-field-metadata.validator.ts rename to packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-enum-flat-field-metadata.util.ts diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/validate-flat-field-metadata-name-availability.validator.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-flat-field-metadata-name-availability.util.ts similarity index 100% rename from packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/validate-flat-field-metadata-name-availability.validator.ts rename to packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-flat-field-metadata-name-availability.util.ts diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/validate-flat-field-metadata-name.validator.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-flat-field-metadata-name.util.ts similarity index 100% rename from packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/validate-flat-field-metadata-name.validator.ts rename to packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-flat-field-metadata-name.util.ts diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/validate-relation-flat-field-metadata.validator.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-relation-flat-field-metadata.util.ts similarity index 93% rename from packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/validate-relation-flat-field-metadata.validator.ts rename to packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-relation-flat-field-metadata.util.ts index 1b627f7072..b9540fc50a 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/validate-relation-flat-field-metadata.validator.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-relation-flat-field-metadata.util.ts @@ -8,8 +8,6 @@ import { import { type ValidateOneFieldMetadataArgs } from 'src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-validator.service'; import { type FailedFlatFieldMetadataValidationExceptions } from 'src/engine/metadata-modules/flat-field-metadata/types/failed-flat-field-metadata-validation.type'; -// Remark: This is duplicated with api metadata transpilers exceptions handlers -// We might wanna have an NotValidatedFlatFieldMetadata and NotValidatedFlatObjectMetadata, in order to avoid export const validateRelationFlatFieldMetadata = async ({ existingFlatObjectMetadataMaps, flatFieldMetadataToValidate: { diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/constants/flat-object-metadata-jsonb-properties.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/constants/flat-object-metadata-jsonb-properties.constant.ts new file mode 100644 index 0000000000..7ff562d6c3 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/constants/flat-object-metadata-jsonb-properties.constant.ts @@ -0,0 +1,5 @@ +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; + +export const FLAT_OBJECT_METADATA_JSONB_PROPERTIES = [ + 'standardOverrides', +] as const satisfies (keyof FlatObjectMetadata)[]; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/constants/flat-object-metadata-properties-to-compare.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/constants/flat-object-metadata-properties-to-compare.constant.ts new file mode 100644 index 0000000000..a59a7c1c3d --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/constants/flat-object-metadata-properties-to-compare.constant.ts @@ -0,0 +1,13 @@ +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; + +export const FLAT_OBJECT_METADATA_PROPERTIES_TO_COMPARE = [ + 'description', + 'icon', + 'isActive', + 'isLabelSyncedWithName', + 'labelPlural', + 'labelSingular', + 'namePlural', + 'nameSingular', + 'standardOverrides', // Only if standard +] as const satisfies (keyof FlatObjectMetadata)[]; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/services/flat-object-metadata-validator.service.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/services/flat-object-metadata-validator.service.ts index abfb09462f..2303574678 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/services/flat-object-metadata-validator.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/services/flat-object-metadata-validator.service.ts @@ -10,13 +10,14 @@ import { addFlatFieldMetadataInFlatObjectMetadataMapsOrThrow } from 'src/engine/ import { addFlatObjectMetadataToFlatObjectMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/add-flat-object-metadata-to-flat-object-metadata-maps-or-throw.util'; import { FailedFlatObjectMetadataValidationExceptions } from 'src/engine/metadata-modules/flat-object-metadata/types/failed-flat-object-metadata-validation.type'; import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; -import { validateFlatObjectMetadataLabel } from 'src/engine/metadata-modules/flat-object-metadata/validators/validate-flat-object-metadata-label.validator'; -import { validateFlatObjectMetadataNames } from 'src/engine/metadata-modules/flat-object-metadata/validators/validate-flat-object-metadata-name.validator'; +import { areFlatObjectMetadataNamesSyncedWithLabels } from 'src/engine/metadata-modules/flat-object-metadata/utils/are-flat-object-metadata-names-synced-with-labels.util'; +import { validateFlatObjectMetadataIdentifiers } from 'src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-identifiers.util'; +import { validateFlatObjectMetadataLabel } from 'src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-label.util'; +import { validateFlatObjectMetadataNames } from 'src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-name.util'; import { ObjectMetadataException, ObjectMetadataExceptionCode, } from 'src/engine/metadata-modules/object-metadata/object-metadata.exception'; -import { computeMetadataNameFromLabel } from 'src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util'; import { doesOtherObjectWithSameNameExists } from 'src/engine/metadata-modules/utils/validate-no-other-object-with-same-name-exists-or-throw.util'; export type ValidateOneFlatObjectMetadataArgs = { @@ -32,6 +33,40 @@ export class FlatObjectMetadataValidatorService { private readonly flatFieldMetadataValidatorService: FlatFieldMetadataValidatorService, ) {} + public validateFlatObjectMetadataUpdate({ + existingFlatObjectMetadataMaps, + updatedFlatObjectMetadata, + }: { + existingFlatObjectMetadataMaps: FlatObjectMetadataMaps; + updatedFlatObjectMetadata: FlatObjectMetadata; + }) { + const existingFlatObjectMetadata = + existingFlatObjectMetadataMaps.byId[updatedFlatObjectMetadata.id]; + + if (!isDefined(existingFlatObjectMetadata)) { + return [ + new ObjectMetadataException( + t`Object to update not found`, + ObjectMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND, + ), + ]; + } + const errors: FailedFlatObjectMetadataValidationExceptions[] = []; + + errors.push( + ...this.validateFlatObjectMetadataNameAndLabels({ + existingFlatObjectMetadataMaps, + flatObjectMetadataToValidate: updatedFlatObjectMetadata, + }), + ); + + errors.push( + ...validateFlatObjectMetadataIdentifiers(existingFlatObjectMetadata), + ); + + return errors; + } + public validateFlatObjectMetadataDeletion({ existingFlatObjectMetadataMaps, objectMetadataToDeleteId, @@ -47,7 +82,7 @@ export class FlatObjectMetadataValidatorService { if (!isDefined(flatObjectMetadataToDelete)) { errors.push( new ObjectMetadataException( - t`Object does not exist`, + t`Object to delete not found`, ObjectMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND, ), ); @@ -93,20 +128,6 @@ export class FlatObjectMetadataValidatorService { }: ValidateOneFlatObjectMetadataArgs) { const errors: FailedFlatObjectMetadataValidationExceptions[] = []; - errors.push( - ...validateFlatObjectMetadataNames({ - namePlural: flatObjectMetadataToValidate.namePlural, - nameSingular: flatObjectMetadataToValidate.nameSingular, - }), - ); - - errors.push( - ...validateFlatObjectMetadataLabel({ - labelPlural: flatObjectMetadataToValidate.labelPlural, - labelSingular: flatObjectMetadataToValidate.labelSingular, - }), - ); - if (flatObjectMetadataToValidate.isRemote) { errors.push( new ObjectMetadataException( @@ -116,51 +137,12 @@ export class FlatObjectMetadataValidatorService { ); } - if (flatObjectMetadataToValidate.isLabelSyncedWithName === true) { - const computedNameSingular = computeMetadataNameFromLabel( - flatObjectMetadataToValidate.labelSingular, - ); - - if (computedNameSingular !== flatObjectMetadataToValidate.nameSingular) { - errors.push( - new ObjectMetadataException( - t`Singular name is not synced with singular label`, - ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT, - ), - ); - } - - const computedNamePlural = computeMetadataNameFromLabel( - flatObjectMetadataToValidate.labelPlural, - ); - - if (computedNamePlural !== flatObjectMetadataToValidate.namePlural) { - errors.push( - new ObjectMetadataException( - t`Plural name is not synced with plural label`, - ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT, - ), - ); - } - } - - if ( - doesOtherObjectWithSameNameExists({ - objectMetadataNamePlural: flatObjectMetadataToValidate.namePlural, - objectMetadataNameSingular: flatObjectMetadataToValidate.nameSingular, - objectMetadataMaps: existingFlatObjectMetadataMaps, - }) - ) { - errors.push( - new ObjectMetadataException( - 'Object already exists', - ObjectMetadataExceptionCode.OBJECT_ALREADY_EXISTS, - { - userFriendlyMessage: t`Object already exists`, - }, - ), - ); - } + errors.push( + ...this.validateFlatObjectMetadataNameAndLabels({ + existingFlatObjectMetadataMaps, + flatObjectMetadataToValidate, + }), + ); const allFlatFieldMetadatasValidationErrors: FailedFlatFieldMetadataValidationExceptions[] = []; @@ -206,4 +188,61 @@ export class FlatObjectMetadataValidatorService { return errors; } + + private validateFlatObjectMetadataNameAndLabels({ + existingFlatObjectMetadataMaps, + flatObjectMetadataToValidate, + }: { + flatObjectMetadataToValidate: FlatObjectMetadata; + existingFlatObjectMetadataMaps: FlatObjectMetadataMaps; + }) { + const errors: FailedFlatObjectMetadataValidationExceptions[] = []; + + errors.push( + ...validateFlatObjectMetadataNames({ + namePlural: flatObjectMetadataToValidate.namePlural, + nameSingular: flatObjectMetadataToValidate.nameSingular, + }), + ); + + errors.push( + ...validateFlatObjectMetadataLabel({ + labelPlural: flatObjectMetadataToValidate.labelPlural, + labelSingular: flatObjectMetadataToValidate.labelSingular, + }), + ); + + if ( + flatObjectMetadataToValidate.isLabelSyncedWithName && + !areFlatObjectMetadataNamesSyncedWithLabels(flatObjectMetadataToValidate) + ) { + errors.push( + new ObjectMetadataException( + t`Names are not synced with labels`, + ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT, + ), + ); + } + + if ( + doesOtherObjectWithSameNameExists({ + objectMetadataNamePlural: flatObjectMetadataToValidate.namePlural, + objectMetadataNameSingular: flatObjectMetadataToValidate.nameSingular, + objectMetadataMaps: existingFlatObjectMetadataMaps, + existingObjectMetadataId: flatObjectMetadataToValidate.id, + }) + ) { + errors.push( + new ObjectMetadataException( + 'Object already exists', + ObjectMetadataExceptionCode.OBJECT_ALREADY_EXISTS, + { + userFriendlyMessage: t`Object already exists`, + }, + ), + ); + } + + return errors; + } } diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata-jsonb-properties.type.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata-jsonb-properties.type.ts new file mode 100644 index 0000000000..ca8d6358fe --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata-jsonb-properties.type.ts @@ -0,0 +1,4 @@ +import { type FLAT_OBJECT_METADATA_JSONB_PROPERTIES } from 'src/engine/metadata-modules/flat-object-metadata/constants/flat-object-metadata-jsonb-properties.constant'; + +export type FlatObjectMetadataEntityJsonbProperties = + (typeof FLAT_OBJECT_METADATA_JSONB_PROPERTIES)[number]; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata-properties-to-compare.type.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata-properties-to-compare.type.ts new file mode 100644 index 0000000000..b8a4e9e90a --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata-properties-to-compare.type.ts @@ -0,0 +1,4 @@ +import { type FLAT_OBJECT_METADATA_PROPERTIES_TO_COMPARE } from 'src/engine/metadata-modules/flat-object-metadata/constants/flat-object-metadata-properties-to-compare.constant'; + +export type FlatObjectMetadataPropertiesToCompare = + (typeof FLAT_OBJECT_METADATA_PROPERTIES_TO_COMPARE)[number]; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/are-flat-object-metadata-names-synced-with-labels.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/are-flat-object-metadata-names-synced-with-labels.util.ts new file mode 100644 index 0000000000..7252bd011a --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/are-flat-object-metadata-names-synced-with-labels.util.ts @@ -0,0 +1,19 @@ +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; +import { computeMetadataNameFromLabel } from 'src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util'; + +export const areFlatObjectMetadataNamesSyncedWithLabels = ( + flatObjectdMetadata: Pick< + FlatObjectMetadata, + 'namePlural' | 'nameSingular' | 'labelPlural' | 'labelSingular' + >, +) => { + const [computedSingularName, computedPluralName] = [ + flatObjectdMetadata.labelSingular, + flatObjectdMetadata.labelPlural, + ].map(computeMetadataNameFromLabel); + + return ( + flatObjectdMetadata.nameSingular === computedSingularName && + flatObjectdMetadata.namePlural === computedPluralName + ); +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/compare-two-flat-object-metadata.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/compare-two-flat-object-metadata.util.ts index e8cd06a3f8..a31ba2e673 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/compare-two-flat-object-metadata.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/compare-two-flat-object-metadata.util.ts @@ -1,36 +1,53 @@ import omit from 'lodash.omit'; import diff from 'microdiff'; import { type FromTo } from 'twenty-shared/types'; -import { assertUnreachable } from 'twenty-shared/utils'; +import { assertUnreachable, isDefined } from 'twenty-shared/utils'; +import { FLAT_OBJECT_METADATA_JSONB_PROPERTIES } from 'src/engine/metadata-modules/flat-object-metadata/constants/flat-object-metadata-jsonb-properties.constant'; +import { FLAT_OBJECT_METADATA_PROPERTIES_TO_COMPARE } from 'src/engine/metadata-modules/flat-object-metadata/constants/flat-object-metadata-properties-to-compare.constant'; +import { type FlatObjectMetadataEntityJsonbProperties } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata-jsonb-properties.type'; +import { type FlatObjectMetadataPropertiesToCompare } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata-properties-to-compare.type'; 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 { type UpdateObjectAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-object-action-v2'; import { transformMetadataForComparison } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/utils/transform-metadata-for-comparison.util'; -const flatObjectMetadataPropertiesToCompare = [ - 'description', - 'icon', - 'isActive', - 'isLabelSyncedWithName', - 'labelPlural', - 'labelSingular', - 'namePlural', - 'nameSingular', - 'standardOverrides', // Only if standard -] as const satisfies (keyof FlatObjectMetadata)[]; - -export type FlatObjectMetadataPropertiesToCompare = - (typeof flatObjectMetadataPropertiesToCompare)[number]; - -/** - * This comparator handles update on colliding uniqueIdentifier flatObjectMetadata - */ export const compareTwoFlatObjectMetadata = ({ from, to, }: FromTo) => { - const fromCompare = transformMetadataForComparison(from, {}); - const toCompare = transformMetadataForComparison(to, {}); + const transformMetadataForComparisonParameters = { + propertiesToStringify: FLAT_OBJECT_METADATA_JSONB_PROPERTIES, + shouldIgnoreProperty: ( + property: string, + flatObjectMetadata: FlatObjectMetadata, + ) => { + if ( + !FLAT_OBJECT_METADATA_PROPERTIES_TO_COMPARE.includes( + property as FlatObjectMetadataPropertiesToCompare, + ) + ) { + return true; + } + + if ( + isStandardMetadata(flatObjectMetadata) && + property !== 'standardOverrides' + ) { + return true; + } + + return false; + }, + }; + const fromCompare = transformMetadataForComparison( + from, + transformMetadataForComparisonParameters, + ); + const toCompare = transformMetadataForComparison( + to, + transformMetadataForComparisonParameters, + ); const objectMetadataDifference = diff(fromCompare, omit(toCompare, 'fields')); return objectMetadataDifference.flatMap< @@ -38,32 +55,24 @@ export const compareTwoFlatObjectMetadata = ({ >((difference) => { switch (difference.type) { case 'CHANGE': { - if ( - difference.oldValue === null && - (difference.value === null || difference.value === undefined) - ) { - return []; - } - const property = difference.path[0]; + const { oldValue, path, value } = difference; + const property = path[0] as FlatObjectMetadataPropertiesToCompare; + const isJsonb = FLAT_OBJECT_METADATA_JSONB_PROPERTIES.includes( + property as FlatObjectMetadataEntityJsonbProperties, + ); - // TODO investigate why it would be a number, in case of array I guess ? - if (typeof property === 'number') { - return []; - } - - // Could be handled directly from the diff we do above - if ( - !flatObjectMetadataPropertiesToCompare.includes( - property as FlatObjectMetadataPropertiesToCompare, - ) - ) { - return []; + if (isJsonb) { + return { + from: isDefined(oldValue) ? JSON.parse(oldValue) : oldValue, + to: isDefined(value) ? JSON.parse(value) : value, + property, + }; } return { - property: property as FlatObjectMetadataPropertiesToCompare, - from: difference.oldValue, - to: difference.value, + from: oldValue, + to: value, + property, }; } case 'CREATE': diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-update-object-input-to-flat-object-metadata.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-update-object-input-to-flat-object-metadata.util.ts new file mode 100644 index 0000000000..72a0f350de --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-update-object-input-to-flat-object-metadata.util.ts @@ -0,0 +1,115 @@ +import { + extractAndSanitizeObjectStringFields, + isDefined, + trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties, +} from 'twenty-shared/utils'; + +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 { FLAT_OBJECT_METADATA_PROPERTIES_TO_COMPARE } from 'src/engine/metadata-modules/flat-object-metadata/constants/flat-object-metadata-properties-to-compare.constant'; +import { type FlatObjectMetadataPropertiesToCompare } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata-properties-to-compare.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; +import { OBJECT_METADATA_STANDARD_OVERRIDES_PROPERTIES } from 'src/engine/metadata-modules/object-metadata/constants/object-metadata-standard-overrides-properties.constant'; +import { type UpdateOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input'; +import { + ObjectMetadataException, + ObjectMetadataExceptionCode, +} from 'src/engine/metadata-modules/object-metadata/object-metadata.exception'; +import { type ObjectMetadataStandardOverridesProperties } from 'src/engine/metadata-modules/object-metadata/types/object-metadata-standard-overrides-properties.types'; +import { isStandardMetadata } from 'src/engine/metadata-modules/utils/is-standard-metadata.util'; + +type FromUpdateObjectInputToFlatObjectMetadataArgs = { + existingFlatObjectMetadataMaps: FlatObjectMetadataMaps; + updateObjectInput: UpdateOneObjectInput; +}; + +const objectMetadataEditableProperties = + FLAT_OBJECT_METADATA_PROPERTIES_TO_COMPARE.filter( + ( + property, + ): property is Exclude< + FlatObjectMetadataPropertiesToCompare, + 'standardOverrides' + > => property !== 'standardOverrides', + ); + +export const fromUpdateObjectInputToFlatObjectMetadata = ({ + existingFlatObjectMetadataMaps, + updateObjectInput: rawUpdateObjectInput, +}: FromUpdateObjectInputToFlatObjectMetadataArgs): FlatObjectMetadata => { + const { id: objectMetadataIdToUpdate } = + trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties( + rawUpdateObjectInput, + ['id'], + ); + const updatedEditableObjectProperties = extractAndSanitizeObjectStringFields( + rawUpdateObjectInput.update, + objectMetadataEditableProperties, + ); + + const flatObjectMetadataToUpdate = + findFlatObjectMetadataInFlatObjectMetadataMaps({ + flatObjectMetadataMaps: existingFlatObjectMetadataMaps, + objectMetadataId: objectMetadataIdToUpdate, + }); + + if (!isDefined(flatObjectMetadataToUpdate)) { + throw new ObjectMetadataException( + 'Object to update not found', + ObjectMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND, + ); + } + + if (isStandardMetadata(flatObjectMetadataToUpdate)) { + const invalidUpdatedProperties = Object.keys( + updatedEditableObjectProperties, + ).filter( + (property) => + !OBJECT_METADATA_STANDARD_OVERRIDES_PROPERTIES.includes( + property as ObjectMetadataStandardOverridesProperties, + ), + ); + + if (invalidUpdatedProperties.length > 0) { + throw new ObjectMetadataException( + `Cannot edit standard object metadata properties: ${invalidUpdatedProperties.join(', ')}`, + ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT, + ); + } + + const updatedStandardFlatObjectdMetadata = + OBJECT_METADATA_STANDARD_OVERRIDES_PROPERTIES.reduce((acc, property) => { + const isPropertyUpdated = + updatedEditableObjectProperties[property] !== undefined; + + return { + ...acc, + standardOverrides: { + ...acc.standardOverrides, + ...(isPropertyUpdated + ? { [property]: updatedEditableObjectProperties[property] } + : {}), + }, + }; + }, flatObjectMetadataToUpdate); + + return updatedStandardFlatObjectdMetadata; + } + + const updatedFlatObjectMetadata = objectMetadataEditableProperties.reduce( + (acc, property) => { + const isPropertyUpdated = + updatedEditableObjectProperties[property] !== undefined; + + return { + ...acc, + ...(isPropertyUpdated + ? { [property]: updatedEditableObjectProperties[property] } + : {}), + }; + }, + flatObjectMetadataToUpdate, + ); + + return updatedFlatObjectMetadata; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-identifiers.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-identifiers.util.ts new file mode 100644 index 0000000000..f28474f204 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-identifiers.util.ts @@ -0,0 +1,72 @@ +import { t } from '@lingui/core/macro'; +import { + isDefined, + isLabelIdentifierFieldMetadataTypes, +} from 'twenty-shared/utils'; + +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; +import { + ObjectMetadataException, + ObjectMetadataExceptionCode, +} from 'src/engine/metadata-modules/object-metadata/object-metadata.exception'; + +export const validateFlatObjectMetadataIdentifiers = ( + flatObjectMetadata: FlatObjectMetadata, +) => { + const errors: ObjectMetadataException[] = []; + + const { labelIdentifierFieldMetadataId, imageIdentifierFieldMetadataId } = + flatObjectMetadata; + + if (isDefined(labelIdentifierFieldMetadataId)) { + const relatedFlatFieldMetadata = flatObjectMetadata.flatFieldMetadatas.find( + (flatFieldMetadata) => + flatFieldMetadata.id === labelIdentifierFieldMetadataId, + ); + + if (!isDefined(relatedFlatFieldMetadata)) { + errors.push( + new ObjectMetadataException( + 'labelIdentifierFieldMetadataId validation failed: related field metadata not found', + ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT, + { + userFriendlyMessage: t`Field declared as label identifier not found`, + }, + ), + ); + } else if ( + !isLabelIdentifierFieldMetadataTypes(relatedFlatFieldMetadata.type) + ) { + errors.push( + new ObjectMetadataException( + 'labelIdentifierFieldMetadataId validation failed: field type not compatible', + ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT, + { + userFriendlyMessage: t`Field cannot be used as label identifier`, + }, + ), + ); + } + } + + if (isDefined(imageIdentifierFieldMetadataId)) { + const relatedFlatFieldMetadata = flatObjectMetadata.flatFieldMetadatas.find( + (flatFieldMetadata) => + flatFieldMetadata.id === imageIdentifierFieldMetadataId, + ); + + if (!isDefined(relatedFlatFieldMetadata)) { + errors.push( + new ObjectMetadataException( + 'imageIdentifierFieldMetadataId validation failed: related field metadata not found', + ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT, + { + userFriendlyMessage: t`Field declared as image identifier not found`, + }, + ), + ); + } + } + + return errors; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/validate-flat-object-metadata-label.validator.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-label.util.ts similarity index 100% rename from packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/validate-flat-object-metadata-label.validator.ts rename to packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-label.util.ts diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/validate-flat-object-metadata-name.validator.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-name.util.ts similarity index 100% rename from packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/validate-flat-object-metadata-name.validator.ts rename to packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-name.util.ts diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/constants/object-metadata-standard-overrides-properties.constant.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/constants/object-metadata-standard-overrides-properties.constant.ts new file mode 100644 index 0000000000..4df5fb0943 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/constants/object-metadata-standard-overrides-properties.constant.ts @@ -0,0 +1,8 @@ +import { type FlatObjectMetadataPropertiesToCompare } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata-properties-to-compare.type'; + +export const OBJECT_METADATA_STANDARD_OVERRIDES_PROPERTIES = [ + 'labelSingular', + 'labelPlural', + 'description', + 'icon', +] as const satisfies FlatObjectMetadataPropertiesToCompare[]; diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata-v2.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata-v2.service.ts index b81da9debe..92c1831aae 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata-v2.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata-v2.service.ts @@ -8,14 +8,17 @@ import { addFlatObjectMetadataToFlatObjectMetadataMapsOrThrow } from 'src/engine import { deleteFieldFromFlatObjectMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/delete-field-from-flat-object-metadata-maps-or-throw.util'; import { deleteObjectFromFlatObjectMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/delete-object-from-flat-object-metadata-maps-or-throw.util'; import { getSubFlatObjectMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/get-sub-flat-object-metadata-maps-or-throw.util'; +import { replaceFlatObjectMetadataInFlatObjectMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/replace-flat-object-metadata-in-flat-object-metadata-maps-or-throw.util'; import { FlatObjectMetadataValidatorService } from 'src/engine/metadata-modules/flat-object-metadata/services/flat-object-metadata-validator.service'; import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { fromCreateObjectInputToFlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-create-object-input-to-flat-object-metadata.util'; import { fromDeleteObjectInputToFlatFieldMetadatasToDelete } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-delete-object-input-to-flat-field-metadatas-to-delete.util'; import { fromFlatObjectMetadataToObjectMetadataDto } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-flat-object-metadata-to-object-metadata-dto.util'; +import { fromUpdateObjectInputToFlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-update-object-input-to-flat-object-metadata.util'; import { CreateObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/create-object.input'; import { DeleteOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/delete-object.input'; import { ObjectMetadataDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-metadata.dto'; +import { UpdateOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input'; import { ObjectMetadataException, ObjectMetadataExceptionCode, @@ -33,6 +36,86 @@ export class ObjectMetadataServiceV2 { private readonly flatObjectMetadataValidatorService: FlatObjectMetadataValidatorService, ) {} + async updateOne({ + updateObjectInput, + workspaceId, + }: { + workspaceId: string; + updateObjectInput: UpdateOneObjectInput; + }): Promise { + const { flatObjectMetadataMaps: existingFlatObjectMetadataMaps } = + await this.workspaceMetadataCacheService.getExistingOrRecomputeFlatObjectMetadataMaps( + { + workspaceId, + }, + ); + + const optimisticallyUpdatedFlatObjectMetadata = + fromUpdateObjectInputToFlatObjectMetadata({ + existingFlatObjectMetadataMaps, + updateObjectInput, + }); + + const validationErrors = + this.flatObjectMetadataValidatorService.validateFlatObjectMetadataUpdate({ + existingFlatObjectMetadataMaps, + updatedFlatObjectMetadata: optimisticallyUpdatedFlatObjectMetadata, + }); + + if (validationErrors.length > 0) { + throw new MultipleMetadataValidationErrors( + validationErrors, + 'Multiple validation errors occurred while updating object', + ); + } + + try { + const fromFlatObjectMetadataMaps = getSubFlatObjectMetadataMapsOrThrow({ + flatObjectMetadataMaps: existingFlatObjectMetadataMaps, + objectMetadataIds: [optimisticallyUpdatedFlatObjectMetadata.id], + }); + const toFlatObjectMetadataMaps = + replaceFlatObjectMetadataInFlatObjectMetadataMapsOrThrow({ + flatObjectMetadata: optimisticallyUpdatedFlatObjectMetadata, + flatObjectMetadataMaps: fromFlatObjectMetadataMaps, + }); + const workspaceMigration = this.workspaceMigrationBuilderV2.build({ + fromFlatObjectMetadataMaps, + toFlatObjectMetadataMaps, + inferDeletionFromMissingObjectFieldIndex: false, + workspaceId, + }); + + await this.workspaceMigrationRunnerV2Service.run(workspaceMigration); + } catch { + throw new ObjectMetadataException( + 'Workspace migration failed to run', + ObjectMetadataExceptionCode.INTERNAL_SERVER_ERROR, + ); + } + + const { flatObjectMetadataMaps: recomputedFlatObjectMetadataMaps } = + await this.workspaceMetadataCacheService.getExistingOrRecomputeFlatObjectMetadataMaps( + { + workspaceId, + }, + ); + + const updatedFlatObjectMetadata = + recomputedFlatObjectMetadataMaps.byId[ + optimisticallyUpdatedFlatObjectMetadata.id + ]; + + if (!isDefined(updatedFlatObjectMetadata)) { + throw new ObjectMetadataException( + 'Updated object metadata not found in recomputed cache', + ObjectMetadataExceptionCode.INTERNAL_SERVER_ERROR, + ); + } + + return fromFlatObjectMetadataToObjectMetadataDto(updatedFlatObjectMetadata); + } + async deleteOne({ deleteObjectInput, workspaceId, @@ -189,7 +272,7 @@ export class ObjectMetadataServiceV2 { if (!isDefined(createdFlatObjectMetadata)) { throw new ObjectMetadataException( - 'Fail to find just created object metadata', + 'Created object metadata not found in recomputed cache', ObjectMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND, ); } diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.resolver.ts index 72889e9014..a89fe4c635 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.resolver.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.resolver.ts @@ -8,6 +8,8 @@ import { Resolver, } from '@nestjs/graphql'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter'; import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe'; import { I18nContext } from 'src/engine/core-modules/i18n/types/i18n-context.type'; @@ -25,6 +27,7 @@ import { type UpdateObjectPayload, } from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input'; import { BeforeUpdateOneObject } from 'src/engine/metadata-modules/object-metadata/hooks/before-update-one-object.hook'; +import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service'; import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service'; import { objectMetadataGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/object-metadata/utils/object-metadata-graphql-api-exception-handler.util'; import { resolveObjectMetadataStandardOverride } from 'src/engine/metadata-modules/object-metadata/utils/resolve-object-metadata-standard-override.util'; @@ -42,6 +45,8 @@ export class ObjectMetadataResolver { constructor( private readonly objectMetadataService: ObjectMetadataService, private readonly beforeUpdateOneObject: BeforeUpdateOneObject, + private readonly featureFlagService: FeatureFlagService, + private readonly objectMetadataServiceV2: ObjectMetadataServiceV2, ) {} @ResolveField(() => String, { nullable: true }) @@ -112,15 +117,35 @@ export class ObjectMetadataResolver { @UseGuards(SettingsPermissionsGuard(PermissionFlagType.DATA_MODEL)) @Mutation(() => ObjectMetadataDTO) async updateOneObject( - @Args('input') input: UpdateOneObjectInput, + @Args('input') updateObjectInput: UpdateOneObjectInput, @AuthWorkspace() { id: workspaceId }: Workspace, @Context() context: I18nContext, ) { - try { - const updatedInput = (await this.beforeUpdateOneObject.run(input, { + const isWorkspaceMigrationV2Enabled = + await this.featureFlagService.isFeatureEnabled( + FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED, workspaceId, - locale: context.req.locale, - })) as UpdateOneObjectInput; + ); + + if (isWorkspaceMigrationV2Enabled) { + try { + return await this.objectMetadataServiceV2.updateOne({ + updateObjectInput, + workspaceId, + }); + } catch (error) { + objectMetadataGraphqlApiExceptionHandler(error); + } + } + + try { + const updatedInput = (await this.beforeUpdateOneObject.run( + updateObjectInput, + { + workspaceId, + locale: context.req.locale, + }, + )) as UpdateOneObjectInput; return await this.objectMetadataService.updateOneObject( updatedInput, diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts index 57946e28ac..89b487814f 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts @@ -110,7 +110,6 @@ export class ObjectMetadataService extends TypeOrmQueryService { const mainDataSource = @@ -306,14 +305,16 @@ export class ObjectMetadataService extends TypeOrmQueryService + | Pick, +) => !metadata.isCustom && isDefined(metadata.standardId); diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/__tests__/__snapshots__/workspace-migration-builder.spec.ts.snap b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/__tests__/__snapshots__/workspace-migration-builder.spec.ts.snap index 090c735763..66338744b3 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/__tests__/__snapshots__/workspace-migration-builder.spec.ts.snap +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/__tests__/__snapshots__/workspace-migration-builder.spec.ts.snap @@ -139,39 +139,6 @@ exports[`Workspace migration builder field actions test suite It should build an ] `; -exports[`Workspace migration builder field actions test suite It should build an update_field action 1`] = ` -[ - { - "fieldMetadataId": Any, - "objectMetadataId": Any, - "type": "update_field", - "updates": [ - { - "from": "Opportunity stage", - "property": "description", - "to": "new description", - }, - { - "from": "IconProgressCheck", - "property": "icon", - "to": "new icon", - }, - { - "from": true, - "property": "isActive", - "to": false, - }, - { - "from": "stage", - "property": "name", - "to": "new name", - }, - ], - "workspaceId": Any, - }, -] -`; - exports[`Workspace migration builder field actions test suite It should build an update_field action for a RELATION field 1`] = ` [ { @@ -195,6 +162,50 @@ exports[`Workspace migration builder field actions test suite It should build an ] `; +exports[`Workspace migration builder field actions test suite It should build an update_field action for custom fields 1`] = ` +[ + { + "fieldMetadataId": Any, + "objectMetadataId": Any, + "type": "update_field", + "updates": [ + { + "from": null, + "property": "description", + "to": "new description", + }, + { + "from": null, + "property": "icon", + "to": "new icon", + }, + ], + "workspaceId": Any, + }, +] +`; + +exports[`Workspace migration builder field actions test suite It should build an update_field action for standard field 1`] = ` +[ + { + "fieldMetadataId": Any, + "objectMetadataId": Any, + "type": "update_field", + "updates": [ + { + "from": null, + "property": "standardOverrides", + "to": { + "description": "new description", + "icon": "new icon", + }, + }, + ], + "workspaceId": Any, + }, +] +`; + exports[`Workspace migration builder field actions test suite It should not infer any actions as from and to fields are identical 1`] = `[]`; exports[`Workspace migration builder index actions test suite It should build a delete_index action 1`] = ` @@ -1633,7 +1644,7 @@ exports[`Workspace migration builder object actions test suite It should build a ] `; -exports[`Workspace migration builder object actions test suite It should build an update_object action with all object updated fields 1`] = ` +exports[`Workspace migration builder object actions test suite It should build an update_object for custom object 1`] = ` [ { "objectMetadataId": Any, @@ -1654,4 +1665,22 @@ exports[`Workspace migration builder object actions test suite It should build a ] `; +exports[`Workspace migration builder object actions test suite It should build an update_object for standard object 1`] = ` +[ + { + "objectMetadataId": Any, + "type": "update_object", + "updates": [ + { + "from": null, + "property": "standardOverrides", + "to": { + "description": "Updated description", + }, + }, + ], + }, +] +`; + exports[`Workspace migration builder object actions test suite It should not infer any actions as from and to are identical 1`] = `[]`; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/__tests__/common/workspace-migration-builder-field-test-case.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/__tests__/common/workspace-migration-builder-field-test-case.ts index 9e70e55af3..fdc3bb6532 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/__tests__/common/workspace-migration-builder-field-test-case.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/__tests__/common/workspace-migration-builder-field-test-case.ts @@ -123,7 +123,7 @@ const basicCrudTestCases: WorkspaceMigrationBuilderTestCase[] = [ }, }, { - title: 'It should build an update_field action', + title: 'It should build an update_field action for standard field', context: { input: { fromFlatObjectMetadataMaps: FLAT_OBJECT_METADATA_MAPS_MOCKS, @@ -132,9 +132,29 @@ const basicCrudTestCases: WorkspaceMigrationBuilderTestCase[] = [ flatObjectMetadataMaps: FLAT_OBJECT_METADATA_MAPS_MOCKS, flatFieldMetadata: getFlatFieldMetadataMock({ ...OPPORTUNITY_FLAT_FIELDS_MOCK.stage, + standardOverrides: { + description: 'new description', + icon: 'new icon', + }, + }), + }), + }, + expectedActionsTypeCounter: { + updateField: 1, + }, + }, + }, + { + title: 'It should build an update_field action for custom fields', + context: { + input: { + fromFlatObjectMetadataMaps: FLAT_OBJECT_METADATA_MAPS_MOCKS, + toFlatObjectMetadataMaps: + replaceFlatFieldMetadataInFlatObjectMetadataMapsOrThrow({ + flatObjectMetadataMaps: FLAT_OBJECT_METADATA_MAPS_MOCKS, + flatFieldMetadata: getFlatFieldMetadataMock({ + ...PET_FLAT_FIELDS_MOCK.species, description: 'new description', - name: 'new name', - isActive: false, icon: 'new icon', }), }), diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/__tests__/common/workspace-migration-builder-object-test-case.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/__tests__/common/workspace-migration-builder-object-test-case.ts index e0f6b661ea..6a86e04f5f 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/__tests__/common/workspace-migration-builder-object-test-case.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/__tests__/common/workspace-migration-builder-object-test-case.ts @@ -5,6 +5,7 @@ import { deleteObjectFromFlatObjectMetadataMapsOrThrow } from 'src/engine/metada import { replaceFlatObjectMetadataInFlatObjectMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/replace-flat-object-metadata-in-flat-object-metadata-maps-or-throw.util'; import { COMPANY_FLAT_OBJECT_MOCK } from 'src/engine/metadata-modules/flat-object-metadata/__mocks__/company-flat-object.mock'; import { getFlatObjectMetadataMock } from 'src/engine/metadata-modules/flat-object-metadata/__mocks__/get-flat-object-metadata.mock'; +import { OPPORTUNITY_FLAT_OBJECT_MOCK } from 'src/engine/metadata-modules/flat-object-metadata/__mocks__/opportunity-flat-object.mock'; import { PET_FLAT_OBJECT_MOCK } from 'src/engine/metadata-modules/flat-object-metadata/__mocks__/pet-flat-object.mock'; import { ROCKET_FLAT_OBJECT_MOCK } from 'src/engine/metadata-modules/flat-object-metadata/__mocks__/rocket-flat-object.mock'; import { fromFlatObjectMetadatasToFlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-flat-object-metadatas-to-flat-object-metadata-maps.util'; @@ -13,8 +14,7 @@ import { type WorkspaceMigrationBuilderTestCase } from 'src/engine/workspace-man export const WORKSPACE_MIGRATION_OBJECT_BUILDER_TEST_CASES: WorkspaceMigrationBuilderTestCase[] = [ { - title: - 'It should build an update_object action with all object updated fields', + title: 'It should build an update_object for custom object', context: { input: { fromFlatObjectMetadataMaps: FLAT_OBJECT_METADATA_MAPS_MOCKS, @@ -34,6 +34,27 @@ export const WORKSPACE_MIGRATION_OBJECT_BUILDER_TEST_CASES: WorkspaceMigrationBu }, }, }, + { + title: 'It should build an update_object for standard object', + context: { + input: { + fromFlatObjectMetadataMaps: FLAT_OBJECT_METADATA_MAPS_MOCKS, + toFlatObjectMetadataMaps: + replaceFlatObjectMetadataInFlatObjectMetadataMapsOrThrow({ + flatObjectMetadataMaps: FLAT_OBJECT_METADATA_MAPS_MOCKS, + flatObjectMetadata: getFlatObjectMetadataMock({ + ...OPPORTUNITY_FLAT_OBJECT_MOCK, + standardOverrides: { + description: 'Updated description', + }, + }), + }), + }, + expectedActionsTypeCounter: { + updateObject: 1, + }, + }, + }, { title: 'It should build a create_object action', context: { diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-field-action-v2.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-field-action-v2.ts index bc35c2046e..1536829e09 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-field-action-v2.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-field-action-v2.ts @@ -1,8 +1,8 @@ import { type FromTo } from 'twenty-shared/types'; import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +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 { type FlatFieldMetadataPropertiesToCompare } from 'src/engine/metadata-modules/flat-field-metadata/utils/compare-two-flat-field-metadata.util'; export type CreateFieldAction = { type: 'create_field'; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-object-action-v2.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-object-action-v2.ts index 1d0e21db94..7c0af51bbe 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-object-action-v2.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-object-action-v2.ts @@ -1,7 +1,7 @@ import { type FromTo } from 'twenty-shared/types'; +import { type FlatObjectMetadataPropertiesToCompare } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata-properties-to-compare.type'; import { type FlatObjectMetadataWithoutFields } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; -import { type FlatObjectMetadataPropertiesToCompare } from 'src/engine/metadata-modules/flat-object-metadata/utils/compare-two-flat-object-metadata.util'; import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { type CreateFieldAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-field-action-v2';