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 f71e819abf9..d352068cdcf 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 @@ -172,7 +172,14 @@ export class FieldMetadataServiceV2 { }); if (inputTranspilationResult.status === 'fail') { - throw inputTranspilationResult.error; + throw new FieldMetadataException( + inputTranspilationResult.error.message, + inputTranspilationResult.error.code, + { + userFriendlyMessage: + inputTranspilationResult.error.userFriendlyMessage, + }, + ); } const { diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-flat-entity-properties-to-compare-and-stringify.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-flat-entity-properties-to-compare-and-stringify.constant.ts index d93e17c4b89..44cdc9e2b3a 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-flat-entity-properties-to-compare-and-stringify.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-flat-entity-properties-to-compare-and-stringify.constant.ts @@ -3,6 +3,7 @@ import { FLAT_DATABASE_EVENT_TRIGGER_EDITABLE_PROPERTIES } from 'src/engine/meta import { type AllMetadataName } from 'src/engine/metadata-modules/flat-entity/types/all-metadata-name.type'; import { type MetadataFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity.type'; import { FLAT_FIELD_METADATA_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-editable-properties.constant'; +import { FLAT_OBJECT_METADATA_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-object-metadata/constants/flat-object-metadata-editable-properties.constant'; import { FLAT_VIEW_FIELD_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-view-field/constants/flat-view-field-editable-properties.constant'; import { FLAT_VIEW_FILTER_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-view-filter/constants/flat-view-filter-editable-properties.constant'; import { FLAT_VIEW_GROUP_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-view-group/constants/flat-view-group-editable-properties.constant'; @@ -17,7 +18,7 @@ type OneFlatEntityConfiguration = { export const ALL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY = { fieldMetadata: { propertiesToCompare: [ - ...FLAT_FIELD_METADATA_EDITABLE_PROPERTIES, + ...FLAT_FIELD_METADATA_EDITABLE_PROPERTIES.custom, 'standardOverrides', ], propertiesToStringify: [ @@ -29,16 +30,8 @@ export const ALL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY = { }, objectMetadata: { propertiesToCompare: [ - 'description', - 'icon', - 'isActive', - 'isLabelSyncedWithName', - 'labelPlural', - 'labelSingular', - 'namePlural', - 'nameSingular', + ...FLAT_OBJECT_METADATA_EDITABLE_PROPERTIES.custom, 'standardOverrides', - 'labelIdentifierFieldMetadataId', ], propertiesToStringify: ['standardOverrides'], }, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-editable-properties.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-editable-properties.constant.ts index 37b27ee48ee..9f8807e0f2e 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-editable-properties.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-editable-properties.constant.ts @@ -1,14 +1,25 @@ import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; -export const FLAT_FIELD_METADATA_EDITABLE_PROPERTIES = [ - 'defaultValue', - 'description', - 'icon', - 'isActive', - 'isLabelSyncedWithName', - 'isUnique', - 'label', - 'name', - 'options', - 'settings', -] as const satisfies (keyof FlatFieldMetadata)[]; +export const FLAT_FIELD_METADATA_EDITABLE_PROPERTIES = { + custom: [ + 'defaultValue', + 'description', + 'icon', + 'isActive', + 'isLabelSyncedWithName', + 'isUnique', + 'label', + 'name', + 'options', + 'settings', + ], + standard: [ + 'defaultValue', + 'description', + 'icon', + 'isActive', + 'label', + 'options', + 'settings', + ], +} as const satisfies Record<'standard' | 'custom', (keyof FlatFieldMetadata)[]>; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-editable-properties.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-editable-properties.constant.ts index 097444c01f7..09588b37540 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-editable-properties.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-editable-properties.constant.ts @@ -1,4 +1,4 @@ import { type FLAT_FIELD_METADATA_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-editable-properties.constant'; export type FlatFieldMetadataEditableProperties = - (typeof FLAT_FIELD_METADATA_EDITABLE_PROPERTIES)[number]; + (typeof FLAT_FIELD_METADATA_EDITABLE_PROPERTIES.custom)[number]; 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 330b63f2a1b..9b676d4f07d 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 @@ -5,13 +5,11 @@ import { } from 'twenty-shared/utils'; import { v4 } from 'uuid'; -import { FIELD_METADATA_STANDARD_OVERRIDES_PROPERTIES } from 'src/engine/metadata-modules/field-metadata/constants/field-metadata-standard-overrides-properties.constant'; import { type UpdateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/update-field.input'; 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 { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type'; import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; import { FLAT_FIELD_METADATA_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-editable-properties.constant'; @@ -23,6 +21,7 @@ import { type FlatFieldMetadataUpdateSideEffects, handleFlatFieldMetadataUpdateSideEffect, } from 'src/engine/metadata-modules/flat-field-metadata/utils/handle-flat-field-metadata-update-side-effect.util'; +import { sanitizeRawUpdateFieldInput } from 'src/engine/metadata-modules/flat-field-metadata/utils/sanitize-raw-update-field-input'; import { isStandardMetadata } from 'src/engine/metadata-modules/utils/is-standard-metadata.util'; import { mergeUpdateInExistingRecord } from 'src/utils/merge-update-in-existing-record.util'; @@ -57,10 +56,6 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({ 'objectMetadataId', 'id', ]); - const updatedEditableFieldProperties = extractAndSanitizeObjectStringFields( - rawUpdateFieldInput, - FLAT_FIELD_METADATA_EDITABLE_PROPERTIES, - ); const existingFlatFieldMetadataToUpdate = findFlatEntityByIdInFlatEntityMaps({ flatEntityId: updateFieldInputInformalProperties.id, @@ -78,62 +73,12 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({ }; } - if (isStandardMetadata(existingFlatFieldMetadataToUpdate)) { - const invalidUpdatedProperties = Object.keys( - updatedEditableFieldProperties, - ).filter((property) => - FIELD_METADATA_STANDARD_OVERRIDES_PROPERTIES.includes( - property as FieldMetadataStandardOverridesProperties, - ), - ); - - if (invalidUpdatedProperties.length > 0) { - const invalidProperties = invalidUpdatedProperties.join(', '); - - return { - status: 'fail', - error: { - code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT, - message: `Cannot update standard field metadata properties: ${invalidProperties}`, - userFriendlyMessage: msg`Cannot update standard field properties: ${invalidProperties}`, - }, - }; - } - - const updatedStandardFlatFieldMetadata = - FIELD_METADATA_STANDARD_OVERRIDES_PROPERTIES.reduce((acc, property) => { - const isPropertyUpdated = - updatedEditableFieldProperties[property] !== undefined; - - return { - ...acc, - standardOverrides: { - ...acc.standardOverrides, - ...(isPropertyUpdated - ? { [property]: updatedEditableFieldProperties[property] } - : {}), - }, - }; - }, existingFlatFieldMetadataToUpdate); - - return { - status: 'success', - result: { - flatViewsToUpdate: [], - flatViewsToDelete: [], - flatViewGroupsToCreate: [], - flatViewGroupsToDelete: [], - flatViewGroupsToUpdate: [], - flatFieldMetadatasToUpdate: [updatedStandardFlatFieldMetadata], - flatIndexMetadatasToUpdate: [], - flatIndexMetadatasToDelete: [], - flatIndexMetadatasToCreate: [], - flatViewFiltersToDelete: [], - flatViewFiltersToUpdate: [], - flatViewFieldsToDelete: [], - }, - }; - } + const isStandardField = isStandardMetadata(existingFlatFieldMetadataToUpdate); + const { standardOverrides, updatedEditableFieldProperties } = + sanitizeRawUpdateFieldInput({ + existingFlatFieldMetadata: existingFlatFieldMetadataToUpdate, + rawUpdateFieldInput, + }); const flatObjectMetadata = findFlatEntityByIdInFlatEntityMaps({ flatEntityId: existingFlatFieldMetadataToUpdate.objectMetadataId, @@ -176,11 +121,17 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({ const optimisticiallyUpdatedFlatFieldMetadatas = flatFieldMetadatasToUpdate.reduce( (accumulator, fromFlatFieldMetadata) => { - const toFlatFieldMetadata = mergeUpdateInExistingRecord({ - existing: fromFlatFieldMetadata, - properties: FLAT_FIELD_METADATA_EDITABLE_PROPERTIES, - update: updatedEditableFieldProperties, - }); + const toFlatFieldMetadata = { + ...mergeUpdateInExistingRecord({ + existing: fromFlatFieldMetadata, + properties: + FLAT_FIELD_METADATA_EDITABLE_PROPERTIES[ + isStandardField ? 'standard' : 'custom' + ], + update: updatedEditableFieldProperties, + }), + standardOverrides, + }; const { flatViewGroupsToCreate, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/sanitize-raw-update-field-input.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/sanitize-raw-update-field-input.ts new file mode 100644 index 00000000000..b628f18fb47 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/sanitize-raw-update-field-input.ts @@ -0,0 +1,106 @@ +import { + extractAndSanitizeObjectStringFields, + isDefined, +} from 'twenty-shared/utils'; + +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 { FLAT_FIELD_METADATA_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-editable-properties.constant'; +import { type FlatFieldMetadataEditableProperties } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-editable-properties.constant'; +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'; + +type SanitizeRawUpdateFieldInputArgs = { + rawUpdateFieldInput: UpdateFieldInput; + existingFlatFieldMetadata: FlatFieldMetadata; +}; +export const sanitizeRawUpdateFieldInput = ({ + existingFlatFieldMetadata, + rawUpdateFieldInput, +}: SanitizeRawUpdateFieldInputArgs) => { + const isStandardField = isStandardMetadata(existingFlatFieldMetadata); + const updatedEditableFieldProperties = extractAndSanitizeObjectStringFields( + rawUpdateFieldInput, + [ + ...new Set([ + ...FLAT_FIELD_METADATA_EDITABLE_PROPERTIES.standard, + ...FLAT_FIELD_METADATA_EDITABLE_PROPERTIES.custom, + ]), + ], + ); + + if (!isStandardField) { + return { + updatedEditableFieldProperties, + standardOverrides: null, + }; + } + + const invalidUpdatedProperties = Object.keys( + updatedEditableFieldProperties, + ).filter( + (property: FlatFieldMetadataEditableProperties) => + !FLAT_FIELD_METADATA_EDITABLE_PROPERTIES.standard.includes( + property as (typeof FLAT_FIELD_METADATA_EDITABLE_PROPERTIES.standard)[number], + ), + ); + + if (invalidUpdatedProperties.length > 0) { + throw new FieldMetadataException( + `Cannot edit standard field metadata properties: ${invalidUpdatedProperties.join(', ')}`, + FieldMetadataExceptionCode.FIELD_MUTATION_NOT_ALLOWED, + ); + } + + const standardOverrides = FIELD_METADATA_STANDARD_OVERRIDES_PROPERTIES.reduce( + (standardOverrides, property) => { + const propertyValue = updatedEditableFieldProperties[property]; + + const isPropertyUpdated = + updatedEditableFieldProperties[property] !== undefined; + + if (!isPropertyUpdated) { + return standardOverrides; + } + delete updatedEditableFieldProperties[property]; + + if (propertyValue === existingFlatFieldMetadata[property]) { + if ( + isDefined(standardOverrides) && + Object.prototype.hasOwnProperty.call(standardOverrides, property) + ) { + const { [property]: _, ...restOverrides } = standardOverrides; + + return restOverrides; + } + + return standardOverrides; + } + + return { + ...standardOverrides, + [property]: propertyValue, + }; + }, + existingFlatFieldMetadata.standardOverrides, + ); + + if ( + isDefined(standardOverrides) && + Object.keys(standardOverrides).length === 0 + ) { + return { + standardOverrides: null, + updatedEditableFieldProperties, + }; + } + + return { + standardOverrides, + updatedEditableFieldProperties, + }; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/constants/flat-object-metadata-editable-properties.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/constants/flat-object-metadata-editable-properties.constant.ts new file mode 100644 index 00000000000..2cac35dcc71 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/constants/flat-object-metadata-editable-properties.constant.ts @@ -0,0 +1,19 @@ +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; + +export const FLAT_OBJECT_METADATA_EDITABLE_PROPERTIES = { + custom: [ + 'description', + 'icon', + 'isActive', + 'isLabelSyncedWithName', + 'labelPlural', + 'labelSingular', + 'namePlural', + 'nameSingular', + 'labelIdentifierFieldMetadataId', + ], + standard: ['description', 'icon', 'isActive', 'labelPlural', 'labelSingular'], +} as const satisfies Record< + 'standard' | 'custom', + (keyof FlatObjectMetadata)[] +>; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-update-object-input-to-flat-object-metadata-and-related-flat-entities.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-update-object-input-to-flat-object-metadata-and-related-flat-entities.util.ts index 33a67e022b0..b9d829ab61a 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-update-object-input-to-flat-object-metadata-and-related-flat-entities.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-update-object-input-to-flat-object-metadata-and-related-flat-entities.util.ts @@ -1,28 +1,24 @@ import { - extractAndSanitizeObjectStringFields, isDefined, trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties, } from 'twenty-shared/utils'; -import { ALL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY } from 'src/engine/metadata-modules/flat-entity/constant/all-flat-entity-properties-to-compare-and-stringify.constant'; import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type'; -import { type FlatEntityPropertiesToCompare } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-properties-to-compare.type'; import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; -import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; -import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type'; +import { FLAT_OBJECT_METADATA_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-object-metadata/constants/flat-object-metadata-editable-properties.constant'; import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; -import { recomputeIndexAfterFlatObjectMetadataSingularNameUpdate } from 'src/engine/metadata-modules/flat-object-metadata/utils/recompute-index-after-flat-object-metadata-singular-name-update.util'; -import { recomputeViewFieldIdentifierAfterFlatObjectIdentifierUpdate } from 'src/engine/metadata-modules/flat-object-metadata/utils/recompute-view-field-identifier-after-flat-object-identifier-update.util'; -import { renameRelatedMorphFieldOnObjectNamesUpdate } from 'src/engine/metadata-modules/flat-object-metadata/utils/rename-related-morph-field-on-object-names-update.util'; -import { type FlatViewField } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field.type'; -import { OBJECT_METADATA_STANDARD_OVERRIDES_PROPERTIES } from 'src/engine/metadata-modules/object-metadata/constants/object-metadata-standard-overrides-properties.constant'; +import { + type FlatObjectMetadataUpdateSideEffects, + handleFlatObjectMetadataUpdateSideEffect, +} from 'src/engine/metadata-modules/flat-object-metadata/utils/handle-flat-object-metadata-update-side-effect.util'; +import { sanitizeRawUpdateObjectInput } from 'src/engine/metadata-modules/flat-object-metadata/utils/sanitize-raw-update-object-input'; 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'; +import { mergeUpdateInExistingRecord } from 'src/utils/merge-update-in-existing-record.util'; type FromUpdateObjectInputToFlatObjectMetadataArgs = { updateObjectInput: UpdateOneObjectInput; @@ -35,24 +31,6 @@ type FromUpdateObjectInputToFlatObjectMetadataArgs = { | 'flatViewMaps' >; -const objectMetadataEditableProperties = - ALL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY.objectMetadata.propertiesToCompare.filter( - ( - property, - ): property is Exclude< - FlatEntityPropertiesToCompare<'objectMetadata'>, - 'standardOverrides' - > => property !== 'standardOverrides', - ); - -type UpdatedFlatObjectAndRelatedFlatEntities = { - flatObjectMetadata: FlatObjectMetadata; - otherObjectFlatFieldMetadataToUpdate: FlatFieldMetadata[]; - flatIndexMetadataToUpdate: FlatIndexMetadata[]; - flatViewFieldToUpdate: FlatViewField[]; - flatViewFieldToCreate: FlatViewField[]; -}; - export const fromUpdateObjectInputToFlatObjectMetadataAndRelatedFlatEntities = ({ flatObjectMetadataMaps: existingFlatObjectMetadataMaps, @@ -61,171 +39,65 @@ export const fromUpdateObjectInputToFlatObjectMetadataAndRelatedFlatEntities = flatFieldMetadataMaps, flatViewFieldMaps, flatViewMaps, - }: FromUpdateObjectInputToFlatObjectMetadataArgs): UpdatedFlatObjectAndRelatedFlatEntities => { + }: FromUpdateObjectInputToFlatObjectMetadataArgs): FlatObjectMetadataUpdateSideEffects & { + flatObjectMetadataToUpdate: FlatObjectMetadata; + } => { const { id: objectMetadataIdToUpdate } = trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties( rawUpdateObjectInput, ['id'], ); - const updatedEditableObjectProperties = - extractAndSanitizeObjectStringFields( - rawUpdateObjectInput.update, - objectMetadataEditableProperties, - ); - const flatObjectMetadataToUpdate = findFlatEntityByIdInFlatEntityMaps({ + const existingFlatObjectMetadata = findFlatEntityByIdInFlatEntityMaps({ flatEntityMaps: existingFlatObjectMetadataMaps, flatEntityId: objectMetadataIdToUpdate, }); - if (!isDefined(flatObjectMetadataToUpdate)) { + if (!isDefined(existingFlatObjectMetadata)) { 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, - ), - ); + const isStandardObject = isStandardMetadata(existingFlatObjectMetadata); + const { standardOverrides, updatedEditableObjectProperties } = + sanitizeRawUpdateObjectInput({ + existingFlatObjectMetadata, + rawUpdateObjectInput, + }); - if (invalidUpdatedProperties.length > 0) { - throw new ObjectMetadataException( - `Cannot edit standard object metadata properties: ${invalidUpdatedProperties.join(', ')}`, - ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT, - ); - } - - const updatedStandardFlatObjectMetadata = - OBJECT_METADATA_STANDARD_OVERRIDES_PROPERTIES.reduce( - (acc, property) => { - const isPropertyUpdated = - updatedEditableObjectProperties[property] !== undefined; - - return { - ...acc, - standardOverrides: { - ...acc.standardOverrides, - ...(isPropertyUpdated - ? { [property]: updatedEditableObjectProperties[property] } - : {}), - }, - }; - }, - flatObjectMetadataToUpdate, - ); - - return { - flatObjectMetadata: updatedStandardFlatObjectMetadata, - otherObjectFlatFieldMetadataToUpdate: [], - flatIndexMetadataToUpdate: [], - flatViewFieldToUpdate: [], - flatViewFieldToCreate: [], - }; - } - - const initialAccumulator: UpdatedFlatObjectAndRelatedFlatEntities = { - flatObjectMetadata: flatObjectMetadataToUpdate, - otherObjectFlatFieldMetadataToUpdate: [], - flatIndexMetadataToUpdate: [], - flatViewFieldToUpdate: [], - flatViewFieldToCreate: [], + const toFlatObjectMetadata = { + ...mergeUpdateInExistingRecord({ + existing: existingFlatObjectMetadata, + properties: + FLAT_OBJECT_METADATA_EDITABLE_PROPERTIES[ + isStandardObject ? 'standard' : 'custom' + ], + update: updatedEditableObjectProperties, + }), + standardOverrides, }; - return objectMetadataEditableProperties.reduce( - ( - { - flatObjectMetadata, - otherObjectFlatFieldMetadataToUpdate, - flatIndexMetadataToUpdate, - flatViewFieldToUpdate, - flatViewFieldToCreate, - }, - property, - ) => { - const updatedPropertyValue = updatedEditableObjectProperties[property]; - const isPropertyUpdated = - updatedPropertyValue !== undefined && - flatObjectMetadata[property] !== updatedPropertyValue; + const { + flatIndexMetadatasToUpdate, + flatViewFieldsToCreate, + flatViewFieldsToUpdate, + otherObjectFlatFieldMetadatasToUpdate, + } = handleFlatObjectMetadataUpdateSideEffect({ + fromFlatObjectMetadata: existingFlatObjectMetadata, + toFlatObjectMetadata, + flatFieldMetadataMaps, + flatIndexMaps, + flatViewFieldMaps, + flatViewMaps, + }); - if (!isPropertyUpdated) { - return { - flatObjectMetadata, - otherObjectFlatFieldMetadataToUpdate, - flatIndexMetadataToUpdate, - flatViewFieldToUpdate, - flatViewFieldToCreate, - }; - } - - const updatedFlatObjectMetadata = { - ...flatObjectMetadata, - [property]: updatedPropertyValue, - }; - - const newUpdatedOtherObjectFlatFieldMetadatas = - property === 'nameSingular' || property === 'namePlural' - ? renameRelatedMorphFieldOnObjectNamesUpdate({ - flatFieldMetadataMaps, - fromFlatObjectMetadata: updatedFlatObjectMetadata, - toFlatObjectMetadata: updatedFlatObjectMetadata, - }) - : []; - - const newUpdatedFlatIndexMetadatas = - property === 'nameSingular' - ? recomputeIndexAfterFlatObjectMetadataSingularNameUpdate({ - flatFieldMetadataMaps, - existingFlatObjectMetadata: flatObjectMetadataToUpdate, - flatIndexMaps, - updatedSingularName: updatedFlatObjectMetadata.nameSingular, - }) - : []; - - const { - flatViewFieldToCreate: newFlatViewFieldToCreate, - flatViewFieldToUpdate: newFlatViewFieldToUpdate, - } = - property === 'labelIdentifierFieldMetadataId' && - isDefined(updatedFlatObjectMetadata.labelIdentifierFieldMetadataId) - ? recomputeViewFieldIdentifierAfterFlatObjectIdentifierUpdate({ - existingFlatObjectMetadata: flatObjectMetadataToUpdate, - flatViewFieldMaps, - flatViewMaps, - updatedLabelIdentifierFieldMetadataId: - updatedFlatObjectMetadata.labelIdentifierFieldMetadataId, - }) - : { - flatViewFieldToCreate: [], - flatViewFieldToUpdate: [], - }; - - return { - flatObjectMetadata: updatedFlatObjectMetadata, - otherObjectFlatFieldMetadataToUpdate: [ - ...otherObjectFlatFieldMetadataToUpdate, - ...newUpdatedOtherObjectFlatFieldMetadatas, - ], - flatViewFieldToUpdate: [ - ...flatViewFieldToUpdate, - ...newFlatViewFieldToUpdate, - ], - flatViewFieldToCreate: [ - ...flatViewFieldToCreate, - ...newFlatViewFieldToCreate, - ], - flatIndexMetadataToUpdate: [ - ...flatIndexMetadataToUpdate, - ...newUpdatedFlatIndexMetadatas, - ], - }; - }, - initialAccumulator, - ); + return { + flatIndexMetadatasToUpdate, + flatObjectMetadataToUpdate: toFlatObjectMetadata, + flatViewFieldsToCreate, + flatViewFieldsToUpdate, + otherObjectFlatFieldMetadatasToUpdate, + }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/handle-flat-object-metadata-update-side-effect.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/handle-flat-object-metadata-update-side-effect.util.ts new file mode 100644 index 00000000000..2e011519fa8 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/handle-flat-object-metadata-update-side-effect.util.ts @@ -0,0 +1,83 @@ +import { type FromTo } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; + +import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; +import { recomputeIndexAfterFlatObjectMetadataSingularNameUpdate } from 'src/engine/metadata-modules/flat-object-metadata/utils/recompute-index-after-flat-object-metadata-singular-name-update.util'; +import { recomputeViewFieldIdentifierAfterFlatObjectIdentifierUpdate } from 'src/engine/metadata-modules/flat-object-metadata/utils/recompute-view-field-identifier-after-flat-object-identifier-update.util'; +import { renameRelatedMorphFieldOnObjectNamesUpdate } from 'src/engine/metadata-modules/flat-object-metadata/utils/rename-related-morph-field-on-object-names-update.util'; +import { type FlatViewField } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field.type'; + +export type FlatObjectMetadataUpdateSideEffects = { + otherObjectFlatFieldMetadatasToUpdate: FlatFieldMetadata[]; + flatViewFieldsToUpdate: FlatViewField[]; + flatViewFieldsToCreate: FlatViewField[]; + flatIndexMetadatasToUpdate: FlatIndexMetadata[]; +}; + +type HandleFlatObjectMetadataUpdateSideEffectArgs = FromTo< + FlatObjectMetadata, + 'flatObjectMetadata' +> & + Pick< + AllFlatEntityMaps, + | 'flatFieldMetadataMaps' + | 'flatViewFieldMaps' + | 'flatIndexMaps' + | 'flatViewMaps' + >; + +export const handleFlatObjectMetadataUpdateSideEffect = ({ + flatIndexMaps, + flatFieldMetadataMaps, + flatViewFieldMaps, + flatViewMaps, + fromFlatObjectMetadata, + toFlatObjectMetadata, +}: HandleFlatObjectMetadataUpdateSideEffectArgs): FlatObjectMetadataUpdateSideEffects => { + const otherObjectFlatFieldMetadatasToUpdate = + fromFlatObjectMetadata.nameSingular !== toFlatObjectMetadata.nameSingular || + fromFlatObjectMetadata.namePlural !== toFlatObjectMetadata.namePlural + ? renameRelatedMorphFieldOnObjectNamesUpdate({ + flatFieldMetadataMaps, + fromFlatObjectMetadata, + toFlatObjectMetadata, + }) + : []; + + const flatIndexMetadatasToUpdate = + fromFlatObjectMetadata.nameSingular !== toFlatObjectMetadata.nameSingular + ? recomputeIndexAfterFlatObjectMetadataSingularNameUpdate({ + flatFieldMetadataMaps, + existingFlatObjectMetadata: fromFlatObjectMetadata, + flatIndexMaps, + updatedSingularName: toFlatObjectMetadata.nameSingular, + }) + : []; + + const { flatViewFieldsToCreate, flatViewFieldsToUpdate } = + fromFlatObjectMetadata.labelIdentifierFieldMetadataId !== + toFlatObjectMetadata.labelIdentifierFieldMetadataId && + isDefined(toFlatObjectMetadata.labelIdentifierFieldMetadataId) && + isDefined(fromFlatObjectMetadata.labelIdentifierFieldMetadataId) + ? recomputeViewFieldIdentifierAfterFlatObjectIdentifierUpdate({ + existingFlatObjectMetadata: fromFlatObjectMetadata, + flatViewFieldMaps, + flatViewMaps, + updatedLabelIdentifierFieldMetadataId: + toFlatObjectMetadata.labelIdentifierFieldMetadataId, + }) + : { + flatViewFieldsToCreate: [], + flatViewFieldsToUpdate: [], + }; + + return { + flatIndexMetadatasToUpdate, + flatViewFieldsToCreate, + flatViewFieldsToUpdate, + otherObjectFlatFieldMetadatasToUpdate, + }; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/recompute-view-field-identifier-after-flat-object-identifier-update.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/recompute-view-field-identifier-after-flat-object-identifier-update.util.ts index 9f2544881a0..f3eb3d2b142 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/recompute-view-field-identifier-after-flat-object-identifier-update.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/recompute-view-field-identifier-after-flat-object-identifier-update.util.ts @@ -13,8 +13,8 @@ type RecomputeViewFieldIdentifierAfterFlatObjectIdentifierUpdateArgs = { } & Pick; type FlatViewFieldToCreateAndUpdate = { - flatViewFieldToCreate: FlatViewField[]; - flatViewFieldToUpdate: FlatViewField[]; + flatViewFieldsToCreate: FlatViewField[]; + flatViewFieldsToUpdate: FlatViewField[]; }; export const recomputeViewFieldIdentifierAfterFlatObjectIdentifierUpdate = ({ existingFlatObjectMetadata, @@ -28,8 +28,8 @@ export const recomputeViewFieldIdentifierAfterFlatObjectIdentifierUpdate = ({ }); const accumulator: FlatViewFieldToCreateAndUpdate = { - flatViewFieldToCreate: [], - flatViewFieldToUpdate: [], + flatViewFieldsToCreate: [], + flatViewFieldsToUpdate: [], }; for (const flatView of flatViews) { @@ -69,7 +69,7 @@ export const recomputeViewFieldIdentifierAfterFlatObjectIdentifierUpdate = ({ applicationId: existingFlatObjectMetadata.applicationId, }; - accumulator.flatViewFieldToCreate.push(flatViewFieldToCreate); + accumulator.flatViewFieldsToCreate.push(flatViewFieldToCreate); } else if ( labelMetadataIdentifierViewField.position > lowestViewFieldPosition ) { @@ -78,7 +78,7 @@ export const recomputeViewFieldIdentifierAfterFlatObjectIdentifierUpdate = ({ position: lowestViewFieldPosition - 1, }; - accumulator.flatViewFieldToUpdate.push(updatedFlatViewField); + accumulator.flatViewFieldsToUpdate.push(updatedFlatViewField); } } diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/sanitize-raw-update-object-input.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/sanitize-raw-update-object-input.ts new file mode 100644 index 00000000000..2166214451d --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/sanitize-raw-update-object-input.ts @@ -0,0 +1,108 @@ +import { + extractAndSanitizeObjectStringFields, + isDefined, +} from 'twenty-shared/utils'; + +import { FLAT_OBJECT_METADATA_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-object-metadata/constants/flat-object-metadata-editable-properties.constant'; +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 SanitizeRawUpdateObjectInputArgs = { + rawUpdateObjectInput: UpdateOneObjectInput; + existingFlatObjectMetadata: FlatObjectMetadata; +}; + +export const sanitizeRawUpdateObjectInput = ({ + existingFlatObjectMetadata, + rawUpdateObjectInput, +}: SanitizeRawUpdateObjectInputArgs) => { + const isStandardObject = isStandardMetadata(existingFlatObjectMetadata); + const updatedEditableObjectProperties = extractAndSanitizeObjectStringFields( + rawUpdateObjectInput.update, + [ + ...new Set([ + ...FLAT_OBJECT_METADATA_EDITABLE_PROPERTIES.standard, + ...FLAT_OBJECT_METADATA_EDITABLE_PROPERTIES.custom, + ]), + ], + ); + + if (!isStandardObject) { + return { + updatedEditableObjectProperties, + standardOverrides: null, + }; + } + + const invalidUpdatedProperties = Object.keys( + updatedEditableObjectProperties, + ).filter( + (property) => + !FLAT_OBJECT_METADATA_EDITABLE_PROPERTIES.standard.includes( + property as ObjectMetadataStandardOverridesProperties, + ), + ); + + if (invalidUpdatedProperties.length > 0) { + throw new ObjectMetadataException( + `Cannot edit standard object metadata properties: ${invalidUpdatedProperties.join(', ')}`, + ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT, + ); + } + + const standardOverrides = + OBJECT_METADATA_STANDARD_OVERRIDES_PROPERTIES.reduce( + (standardOverrides, property) => { + const propertyValue = updatedEditableObjectProperties[property]; + + const isPropertyUpdated = + updatedEditableObjectProperties[property] !== undefined; + + if (!isPropertyUpdated) { + return standardOverrides; + } + delete updatedEditableObjectProperties[property]; + + if (propertyValue === existingFlatObjectMetadata[property]) { + if ( + isDefined(standardOverrides) && + Object.prototype.hasOwnProperty.call(standardOverrides, property) + ) { + const { [property]: _, ...restOverrides } = standardOverrides; + + return restOverrides; + } + + return standardOverrides; + } + + return { + ...standardOverrides, + [property]: propertyValue, + }; + }, + existingFlatObjectMetadata.standardOverrides, + ); + + if ( + isDefined(standardOverrides) && + Object.keys(standardOverrides).length === 0 + ) { + return { + standardOverrides: null, + updatedEditableObjectProperties, + }; + } + + return { + standardOverrides, + updatedEditableObjectProperties, + }; +}; 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 03f679b6f39..b774a47b2f4 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 @@ -67,11 +67,11 @@ export class ObjectMetadataServiceV2 { ); const { - flatObjectMetadata: optimisticallyUpdatedFlatObjectMetadata, - otherObjectFlatFieldMetadataToUpdate: otherObjectFlatFieldMetadatas, - flatIndexMetadataToUpdate, - flatViewFieldToUpdate, - flatViewFieldToCreate, + otherObjectFlatFieldMetadatasToUpdate, + flatObjectMetadataToUpdate, + flatIndexMetadatasToUpdate, + flatViewFieldsToCreate, + flatViewFieldsToUpdate, } = fromUpdateObjectInputToFlatObjectMetadataAndRelatedFlatEntities({ flatFieldMetadataMaps: existingFlatFieldMetadataMaps, flatObjectMetadataMaps: existingFlatObjectMetadataMaps, @@ -89,25 +89,25 @@ export class ObjectMetadataServiceV2 { flatEntityMaps: existingFlatObjectMetadataMaps, flatEntityToCreate: [], flatEntityToDelete: [], - flatEntityToUpdate: [optimisticallyUpdatedFlatObjectMetadata], + flatEntityToUpdate: [flatObjectMetadataToUpdate], }), flatIndexMaps: computeFlatEntityMapsFromTo({ flatEntityMaps: existingFlatIndexMaps, flatEntityToCreate: [], flatEntityToDelete: [], - flatEntityToUpdate: flatIndexMetadataToUpdate, + flatEntityToUpdate: flatIndexMetadatasToUpdate, }), flatFieldMetadataMaps: computeFlatEntityMapsFromTo({ flatEntityMaps: existingFlatFieldMetadataMaps, flatEntityToCreate: [], flatEntityToDelete: [], - flatEntityToUpdate: otherObjectFlatFieldMetadatas, + flatEntityToUpdate: otherObjectFlatFieldMetadatasToUpdate, }), flatViewFieldMaps: computeFlatEntityMapsFromTo({ flatEntityMaps: existingFlatViewFieldMaps, - flatEntityToCreate: flatViewFieldToCreate, + flatEntityToCreate: flatViewFieldsToCreate, flatEntityToDelete: [], - flatEntityToUpdate: flatViewFieldToUpdate, + flatEntityToUpdate: flatViewFieldsToUpdate, }), }, dependencyAllFlatEntityMaps: { @@ -136,9 +136,7 @@ export class ObjectMetadataServiceV2 { ); const updatedFlatObjectMetadata = - recomputedFlatObjectMetadataMaps.byId[ - optimisticallyUpdatedFlatObjectMetadata.id - ]; + recomputedFlatObjectMetadataMaps.byId[flatObjectMetadataToUpdate.id]; if (!isDefined(updatedFlatObjectMetadata)) { throw new ObjectMetadataException( diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-view-group-validator.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-view-group-validator.service.ts index 40a09c6ca40..ce5295cf348 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-view-group-validator.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-view-group-validator.service.ts @@ -108,8 +108,6 @@ export class FlatViewGroupValidatorService { message: t`View group to delete not found`, userFriendlyMessage: msg`View group to delete not found`, }); - - return validationResult; } return validationResult; diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/__snapshots__/failing-update-one-standard-field-metadata.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/field-metadata/__snapshots__/failing-update-one-standard-field-metadata.integration-spec.ts.snap new file mode 100644 index 00000000000..2ef96b9dd6a --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/__snapshots__/failing-update-one-standard-field-metadata.integration-spec.ts.snap @@ -0,0 +1,37 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Standard field metadata update should be ignored when trying to update isLabelSyncedWithName on standard field 1`] = ` +{ + "extensions": { + "code": "FORBIDDEN", + "subCode": "FIELD_MUTATION_NOT_ALLOWED", + "userFriendlyMessage": "An error occurred.", + }, + "message": "Cannot edit standard field metadata properties: isLabelSyncedWithName", + "name": "ForbiddenError", +} +`; + +exports[`Standard field metadata update should be ignored when trying to update name on standard field 1`] = ` +{ + "extensions": { + "code": "FORBIDDEN", + "subCode": "FIELD_MUTATION_NOT_ALLOWED", + "userFriendlyMessage": "An error occurred.", + }, + "message": "Cannot edit standard field metadata properties: name", + "name": "ForbiddenError", +} +`; + +exports[`Standard field metadata update should be ignored when trying to update several forbidden properties 1`] = ` +{ + "extensions": { + "code": "FORBIDDEN", + "subCode": "FIELD_MUTATION_NOT_ALLOWED", + "userFriendlyMessage": "An error occurred.", + }, + "message": "Cannot edit standard field metadata properties: isLabelSyncedWithName, name", + "name": "ForbiddenError", +} +`; diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/__snapshots__/successful-update-one-standard-field-metadata.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/field-metadata/__snapshots__/successful-update-one-standard-field-metadata.integration-spec.ts.snap new file mode 100644 index 00000000000..b2cdef322ed --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/__snapshots__/successful-update-one-standard-field-metadata.integration-spec.ts.snap @@ -0,0 +1,326 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Standard field metadata update should succeed when setting isActive to false 1`] = ` +{ + "defaultValue": "'NEW'", + "description": "Opportunity stage", + "icon": "IconProgressCheck", + "id": Any, + "isActive": false, + "isCustom": false, + "label": "Stage", + "name": "stage", + "options": [ + { + "color": "red", + "id": Any, + "label": "New", + "position": 0, + "value": "NEW", + }, + { + "color": "purple", + "id": Any, + "label": "Screening", + "position": 1, + "value": "SCREENING", + }, + { + "color": "sky", + "id": Any, + "label": "Meeting", + "position": 2, + "value": "MEETING", + }, + { + "color": "turquoise", + "id": Any, + "label": "Proposal", + "position": 3, + "value": "PROPOSAL", + }, + { + "color": "yellow", + "id": Any, + "label": "Customer", + "position": 4, + "value": "CUSTOMER", + }, + ], + "standardOverrides": null, +} +`; + +exports[`Standard field metadata update should succeed when updating defaultValue 1`] = ` +{ + "defaultValue": "'SCREENING'", + "description": "Opportunity stage", + "icon": "IconProgressCheck", + "id": Any, + "isActive": true, + "isCustom": false, + "label": "Stage", + "name": "stage", + "options": [ + { + "color": "red", + "id": Any, + "label": "New", + "position": 0, + "value": "NEW", + }, + { + "color": "purple", + "id": Any, + "label": "Screening", + "position": 1, + "value": "SCREENING", + }, + { + "color": "sky", + "id": Any, + "label": "Meeting", + "position": 2, + "value": "MEETING", + }, + { + "color": "turquoise", + "id": Any, + "label": "Proposal", + "position": 3, + "value": "PROPOSAL", + }, + { + "color": "yellow", + "id": Any, + "label": "Customer", + "position": 4, + "value": "CUSTOMER", + }, + ], + "standardOverrides": null, +} +`; + +exports[`Standard field metadata update should succeed when updating description 1`] = ` +{ + "defaultValue": "'NEW'", + "description": "Opportunity stage", + "icon": "IconProgressCheck", + "id": Any, + "isActive": true, + "isCustom": false, + "label": "Stage", + "name": "stage", + "options": [ + { + "color": "red", + "id": Any, + "label": "New", + "position": 0, + "value": "NEW", + }, + { + "color": "purple", + "id": Any, + "label": "Screening", + "position": 1, + "value": "SCREENING", + }, + { + "color": "sky", + "id": Any, + "label": "Meeting", + "position": 2, + "value": "MEETING", + }, + { + "color": "turquoise", + "id": Any, + "label": "Proposal", + "position": 3, + "value": "PROPOSAL", + }, + { + "color": "yellow", + "id": Any, + "label": "Customer", + "position": 4, + "value": "CUSTOMER", + }, + ], + "standardOverrides": { + "description": "Updated test description for company name field", + "icon": null, + "label": null, + }, +} +`; + +exports[`Standard field metadata update should succeed when updating icon 1`] = ` +{ + "defaultValue": "'NEW'", + "description": "Opportunity stage", + "icon": "IconProgressCheck", + "id": Any, + "isActive": true, + "isCustom": false, + "label": "Stage", + "name": "stage", + "options": [ + { + "color": "red", + "id": Any, + "label": "New", + "position": 0, + "value": "NEW", + }, + { + "color": "purple", + "id": Any, + "label": "Screening", + "position": 1, + "value": "SCREENING", + }, + { + "color": "sky", + "id": Any, + "label": "Meeting", + "position": 2, + "value": "MEETING", + }, + { + "color": "turquoise", + "id": Any, + "label": "Proposal", + "position": 3, + "value": "PROPOSAL", + }, + { + "color": "yellow", + "id": Any, + "label": "Customer", + "position": 4, + "value": "CUSTOMER", + }, + ], + "standardOverrides": { + "description": null, + "icon": "IconBuildingFactory", + "label": null, + }, +} +`; + +exports[`Standard field metadata update should succeed when updating label 1`] = ` +{ + "defaultValue": "'NEW'", + "description": "Opportunity stage", + "icon": "IconProgressCheck", + "id": Any, + "isActive": true, + "isCustom": false, + "label": "Stage", + "name": "stage", + "options": [ + { + "color": "red", + "id": Any, + "label": "New", + "position": 0, + "value": "NEW", + }, + { + "color": "purple", + "id": Any, + "label": "Screening", + "position": 1, + "value": "SCREENING", + }, + { + "color": "sky", + "id": Any, + "label": "Meeting", + "position": 2, + "value": "MEETING", + }, + { + "color": "turquoise", + "id": Any, + "label": "Proposal", + "position": 3, + "value": "PROPOSAL", + }, + { + "color": "yellow", + "id": Any, + "label": "Customer", + "position": 4, + "value": "CUSTOMER", + }, + ], + "standardOverrides": { + "description": null, + "icon": null, + "label": "Business Name", + }, +} +`; + +exports[`Standard field metadata update should succeed when updating options 1`] = ` +{ + "defaultValue": "'NEW'", + "description": "Opportunity stage", + "icon": "IconProgressCheck", + "id": Any, + "isActive": true, + "isCustom": false, + "label": "Stage", + "name": "stage", + "options": [ + { + "color": "red", + "id": Any, + "label": "New Lead", + "position": 0, + "value": "NEW", + }, + { + "color": "purple", + "id": Any, + "label": "Under Review", + "position": 1, + "value": "SCREENING", + }, + { + "color": "sky", + "id": Any, + "label": "Meeting", + "position": 2, + "value": "MEETING", + }, + { + "color": "turquoise", + "id": Any, + "label": "Proposal Sent", + "position": 3, + "value": "PROPOSAL", + }, + { + "color": "yellow", + "id": Any, + "label": "Customer", + "position": 4, + "value": "CUSTOMER", + }, + { + "color": "green", + "id": Any, + "label": "Closed", + "position": 5, + "value": "CLOSED", + }, + ], + "standardOverrides": null, +} +`; diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/failing-update-one-standard-field-metadata.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/field-metadata/failing-update-one-standard-field-metadata.integration-spec.ts new file mode 100644 index 00000000000..f69407fb6cd --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/failing-update-one-standard-field-metadata.integration-spec.ts @@ -0,0 +1,105 @@ +import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util'; +import { updateOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/update-one-field-metadata.util'; +import { findManyObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata.util'; +import { jestExpectToBeDefined } from 'test/utils/expect-to-be-defined.util.test'; +import { + eachTestingContextFilter, + type EachTestingContext, +} from 'twenty-shared/testing'; + +import { type UpdateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/update-field.input'; + +type UpdateOneStandardFieldMetadataTestingContext = EachTestingContext< + Partial +>[]; + +const failingUpdateTestsUseCase: UpdateOneStandardFieldMetadataTestingContext = + [ + { + title: 'when trying to update name on standard field', + context: { + name: 'newName', + }, + }, + { + title: 'when trying to update isLabelSyncedWithName on standard field', + context: { + isLabelSyncedWithName: false, + }, + }, + { + title: 'when trying to update several forbidden properties', + context: { + name: 'newName', + isLabelSyncedWithName: true, + }, + }, + ]; + +const allTestsUseCases = [...failingUpdateTestsUseCase]; + +describe('Standard field metadata update should be ignored', () => { + let companyNameFieldMetadataId: string; + + beforeAll(async () => { + const { objects } = await findManyObjectMetadata({ + expectToFail: false, + input: { + filter: {}, + paging: { first: 100 }, + }, + gqlFields: ` + id + nameSingular + fieldsList { + id + name + label + description + icon + isActive + isCustom + isLabelSyncedWithName + } + `, + }); + + const companyObject = objects.find((o) => o.nameSingular === 'company'); + + jestExpectToBeDefined(companyObject); + + const companyNameField = companyObject.fieldsList?.find( + (field) => field.name === 'name' && !field.isCustom, + ); + + jestExpectToBeDefined(companyNameField); + companyNameFieldMetadataId = companyNameField.id; + }); + + it.each(eachTestingContextFilter(allTestsUseCases))( + '$title', + async ({ context }) => { + const { errors } = await updateOneFieldMetadata({ + input: { + idToUpdate: companyNameFieldMetadataId, + updatePayload: context, + }, + expectToFail: true, + gqlFields: ` + id + name + label + description + icon + isActive + isCustom + isLabelSyncedWithName + `, + }); + + expectOneNotInternalServerErrorSnapshot({ + errors, + }); + }, + ); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/successful-update-one-standard-field-metadata.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/field-metadata/successful-update-one-standard-field-metadata.integration-spec.ts new file mode 100644 index 00000000000..e037c8b3964 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/successful-update-one-standard-field-metadata.integration-spec.ts @@ -0,0 +1,179 @@ +import { updateOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/update-one-field-metadata.util'; +import { findManyObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata.util'; +import { jestExpectToBeDefined } from 'test/utils/expect-to-be-defined.util.test'; +import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any'; +import { + eachTestingContextFilter, + type EachTestingContext, +} from 'twenty-shared/testing'; + +import { type FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto'; +import { type UpdateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/update-field.input'; + +type UpdateOneStandardFieldMetadataTestingContext = EachTestingContext< + Partial +>[]; + +const successfulUpdateTestsUseCase: UpdateOneStandardFieldMetadataTestingContext = + [ + { + title: 'when updating description', + context: { + description: 'Updated test description for company name field', + }, + }, + { + title: 'when updating icon', + context: { + icon: 'IconBuildingFactory', + }, + }, + { + title: 'when setting isActive to false', + context: { + isActive: false, + }, + }, + { + title: 'when updating label', + context: { + label: 'Business Name', + }, + }, + { + title: 'when updating options', + context: { + options: [ + { value: 'NEW', label: 'New Lead', position: 0, color: 'red' }, + { + value: 'SCREENING', + label: 'Under Review', + position: 1, + color: 'purple', + }, + { value: 'MEETING', label: 'Meeting', position: 2, color: 'sky' }, + { + value: 'PROPOSAL', + label: 'Proposal Sent', + position: 3, + color: 'turquoise', + }, + { + value: 'CUSTOMER', + label: 'Customer', + position: 4, + color: 'yellow', + }, + { value: 'CLOSED', label: 'Closed', position: 5, color: 'green' }, + ], + }, + }, + { + title: 'when updating defaultValue', + context: { + defaultValue: "'SCREENING'", + }, + }, + ]; + +describe('Standard field metadata update should succeed', () => { + let originalStageFieldMetadata: FieldMetadataDTO; + + beforeAll(async () => { + const { objects } = await findManyObjectMetadata({ + expectToFail: false, + input: { + filter: {}, + paging: { first: 100 }, + }, + gqlFields: ` + id + nameSingular + fieldsList { + id + name + label + description + icon + isActive + isCustom + type + options + settings + defaultValue + standardOverrides { + label + description + icon + } + } + `, + }); + + const opportunityObject = objects.find( + (o) => o.nameSingular === 'opportunity', + ); + + jestExpectToBeDefined(opportunityObject); + + const opportunityStageField = opportunityObject.fieldsList?.find( + (field) => field.name === 'stage' && !field.isCustom, + ); + + jestExpectToBeDefined(opportunityStageField); + originalStageFieldMetadata = opportunityStageField; + }); + + afterEach(async () => { + await updateOneFieldMetadata({ + expectToFail: false, + input: { + idToUpdate: originalStageFieldMetadata.id, + updatePayload: { + label: originalStageFieldMetadata.label, + description: originalStageFieldMetadata.description, + icon: originalStageFieldMetadata.icon, + isActive: originalStageFieldMetadata.isActive, + options: originalStageFieldMetadata.options, + defaultValue: originalStageFieldMetadata.defaultValue, + }, + }, + }); + }); + + it.each(eachTestingContextFilter(successfulUpdateTestsUseCase))( + '$title', + async ({ context }) => { + const updatePayload = context; + + const { data } = await updateOneFieldMetadata({ + input: { + idToUpdate: originalStageFieldMetadata.id, + updatePayload, + }, + expectToFail: false, + gqlFields: ` + id + name + label + description + icon + isActive + isCustom + options + defaultValue + standardOverrides { + label + description + icon + } + `, + }); + + expect(data.updateOneField.id).toBe(originalStageFieldMetadata.id); + expect(data.updateOneField).toMatchSnapshot( + extractRecordIdsAndDatesAsExpectAny({ ...data.updateOneField }), + ); + }, + ); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-update-one-standard-object-metadata.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-update-one-standard-object-metadata.integration-spec.ts.snap new file mode 100644 index 00000000000..fb6ce3ceac5 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-update-one-standard-object-metadata.integration-spec.ts.snap @@ -0,0 +1,49 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Standard object metadata update should fail when trying to update labelIdentifierFieldMetadataId on standard object 1`] = ` +{ + "extensions": { + "code": "BAD_USER_INPUT", + "subCode": "INVALID_OBJECT_INPUT", + "userFriendlyMessage": "An error occurred.", + }, + "message": "Cannot edit standard object metadata properties: labelIdentifierFieldMetadataId", + "name": "UserInputError", +} +`; + +exports[`Standard object metadata update should fail when trying to update namePlural on standard object 1`] = ` +{ + "extensions": { + "code": "BAD_USER_INPUT", + "subCode": "INVALID_OBJECT_INPUT", + "userFriendlyMessage": "An error occurred.", + }, + "message": "Cannot edit standard object metadata properties: namePlural", + "name": "UserInputError", +} +`; + +exports[`Standard object metadata update should fail when trying to update nameSingular on standard object 1`] = ` +{ + "extensions": { + "code": "BAD_USER_INPUT", + "subCode": "INVALID_OBJECT_INPUT", + "userFriendlyMessage": "An error occurred.", + }, + "message": "Cannot edit standard object metadata properties: nameSingular", + "name": "UserInputError", +} +`; + +exports[`Standard object metadata update should fail when trying to update several forbidden properties 1`] = ` +{ + "extensions": { + "code": "BAD_USER_INPUT", + "subCode": "INVALID_OBJECT_INPUT", + "userFriendlyMessage": "An error occurred.", + }, + "message": "Cannot edit standard object metadata properties: isLabelSyncedWithName, namePlural", + "name": "UserInputError", +} +`; diff --git a/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/successful-update-one-standard-object-metadata.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/successful-update-one-standard-object-metadata.integration-spec.ts.snap new file mode 100644 index 00000000000..7247356eeba --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/successful-update-one-standard-object-metadata.integration-spec.ts.snap @@ -0,0 +1,71 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Standard object metadata update should succeed when setting isActive to false 1`] = ` +{ + "description": "A company", + "icon": "IconBuildingSkyscraper", + "id": Any, + "isActive": false, + "labelPlural": "Companies", + "labelSingular": "Company", + "namePlural": "companies", + "nameSingular": "company", + "shortcut": "C", + "standardOverrides": null, +} +`; + +exports[`Standard object metadata update should succeed when updating description 1`] = ` +{ + "description": "Updated test description for company", + "icon": "IconBuildingSkyscraper", + "id": Any, + "isActive": true, + "labelPlural": "Companies", + "labelSingular": "Company", + "namePlural": "companies", + "nameSingular": "company", + "shortcut": "C", + "standardOverrides": { + "description": "Updated test description for company", + "icon": null, + "labelPlural": null, + "labelSingular": null, + }, +} +`; + +exports[`Standard object metadata update should succeed when updating icon 1`] = ` +{ + "description": "A company", + "icon": "IconBuildingSkyscraper", + "id": Any, + "isActive": true, + "labelPlural": "Companies", + "labelSingular": "Company", + "namePlural": "companies", + "nameSingular": "company", + "shortcut": "C", + "standardOverrides": null, +} +`; + +exports[`Standard object metadata update should succeed when updating labelSingular and labelPlural 1`] = ` +{ + "description": "A company", + "icon": "IconBuildingSkyscraper", + "id": Any, + "isActive": true, + "labelPlural": "Businesses", + "labelSingular": "Business", + "namePlural": "companies", + "nameSingular": "company", + "shortcut": "C", + "standardOverrides": { + "description": null, + "icon": null, + "labelPlural": "Businesses", + "labelSingular": "Business", + }, +} +`; diff --git a/packages/twenty-server/test/integration/metadata/suites/object-metadata/failing-update-one-standard-object-metadata.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/object-metadata/failing-update-one-standard-object-metadata.integration-spec.ts new file mode 100644 index 00000000000..471b7cc789a --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/object-metadata/failing-update-one-standard-object-metadata.integration-spec.ts @@ -0,0 +1,115 @@ +import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util'; +import { findManyObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata.util'; +import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util'; +import { jestExpectToBeDefined } from 'test/utils/expect-to-be-defined.util.test'; +import { + eachTestingContextFilter, + type EachTestingContext, +} from 'twenty-shared/testing'; + +import { type UpdateObjectPayload } from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input'; + +type TestingRuntimeContext = { + objectMetadataId: string; + textFieldMetadataId: string; +}; + +type UpdateOneStandardObjectMetadataTestingContext = EachTestingContext< + | ((args: TestingRuntimeContext) => Partial) + | Partial +>[]; + +const failingUpdateTestsUseCase: UpdateOneStandardObjectMetadataTestingContext = + [ + { + title: 'when trying to update nameSingular on standard object', + context: { + nameSingular: 'newCompany', + }, + }, + { + title: 'when trying to update namePlural on standard object', + context: { + namePlural: 'newCompanies', + }, + }, + { + title: + 'when trying to update labelIdentifierFieldMetadataId on standard object', + context: ({ textFieldMetadataId }) => ({ + labelIdentifierFieldMetadataId: textFieldMetadataId, + }), + }, + { + title: 'when trying to update several forbidden properties', + context: { + namePlural: 'newCompanies', + isActive: false, + isLabelSyncedWithName: false, + shortcut: 'whatever', + }, + }, + ]; + +describe('Standard object metadata update should fail', () => { + let companyObjectMetadataId: string; + let companyNameFieldMetadataId: string; + + beforeAll(async () => { + const { objects } = await findManyObjectMetadata({ + expectToFail: false, + input: { + filter: {}, + paging: { first: 100 }, + }, + gqlFields: ` + id + nameSingular + namePlural + labelSingular + labelPlural + isLabelSyncedWithName + fieldsList { + id + name + type + } + `, + }); + + const companyObject = objects.find((o) => o.nameSingular === 'company'); + + jestExpectToBeDefined(companyObject); + companyObjectMetadataId = companyObject.id; + + const nameField = companyObject.fieldsList?.find( + (field: { name: string }) => field.name === 'name', + ); + + jestExpectToBeDefined(nameField); + companyNameFieldMetadataId = nameField!.id; + }); + + it.each(eachTestingContextFilter(failingUpdateTestsUseCase))( + '$title', + async ({ context }) => { + const updatePayload = + typeof context === 'function' + ? context({ + objectMetadataId: companyObjectMetadataId, + textFieldMetadataId: companyNameFieldMetadataId, + }) + : context; + + const { errors } = await updateOneObjectMetadata({ + input: { + idToUpdate: companyObjectMetadataId, + updatePayload, + }, + expectToFail: true, + }); + + expectOneNotInternalServerErrorSnapshot({ errors }); + }, + ); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/object-metadata/successful-update-one-standard-object-metadata.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/object-metadata/successful-update-one-standard-object-metadata.integration-spec.ts new file mode 100644 index 00000000000..b0de7b75af7 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/object-metadata/successful-update-one-standard-object-metadata.integration-spec.ts @@ -0,0 +1,150 @@ +import { findManyObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata.util'; +import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util'; +import { jestExpectToBeDefined } from 'test/utils/expect-to-be-defined.util.test'; +import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any'; +import { + eachTestingContextFilter, + type EachTestingContext, +} from 'twenty-shared/testing'; + +import { type ObjectMetadataDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-metadata.dto'; +import { type UpdateObjectPayload } from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input'; + +type TestingRuntimeContext = { + objectMetadataId: string; +}; + +type UpdateOneStandardObjectMetadataTestingContext = EachTestingContext< + | ((args: TestingRuntimeContext) => Partial) + | Partial +>[]; + +const successfulUpdateTestsUseCase: UpdateOneStandardObjectMetadataTestingContext = + [ + { + title: 'when updating description', + context: { + description: 'Updated test description for company', + }, + }, + { + title: 'when updating icon', + context: { + icon: 'IconBuildingSkyscraper', + }, + }, + { + title: 'when setting isActive to false', + context: { + isActive: false, + }, + }, + { + title: 'when updating labelSingular and labelPlural', + context: { + labelSingular: 'Business', + labelPlural: 'Businesses', + }, + }, + ]; + +const allTestsUseCases = [...successfulUpdateTestsUseCase]; + +describe('Standard object metadata update should succeed', () => { + let companyObjectMetadataId: string; + let originalCompanyMetadata: ObjectMetadataDTO; + + beforeAll(async () => { + const { objects } = await findManyObjectMetadata({ + expectToFail: false, + input: { + filter: {}, + paging: { first: 100 }, + }, + gqlFields: ` + id + nameSingular + namePlural + labelSingular + labelPlural + description + icon + isActive + shortcut + `, + }); + + const companyObject = objects.find((o) => o.nameSingular === 'company'); + + jestExpectToBeDefined(companyObject); + companyObjectMetadataId = companyObject.id; + originalCompanyMetadata = companyObject; + }); + + afterEach(async () => { + await updateOneObjectMetadata({ + expectToFail: false, + input: { + idToUpdate: companyObjectMetadataId, + updatePayload: { + labelSingular: originalCompanyMetadata.labelSingular, + labelPlural: originalCompanyMetadata.labelPlural, + description: originalCompanyMetadata.description, + icon: originalCompanyMetadata.icon, + isActive: originalCompanyMetadata.isActive, + shortcut: originalCompanyMetadata.shortcut, + }, + }, + }); + }); + + it.each(eachTestingContextFilter(allTestsUseCases))( + '$title', + async ({ context }) => { + const updatePayload = + typeof context === 'function' + ? context({ objectMetadataId: companyObjectMetadataId }) + : context; + + const { + data: { updateOneObject }, + errors, + } = await updateOneObjectMetadata({ + input: { + idToUpdate: companyObjectMetadataId, + updatePayload, + }, + expectToFail: false, + gqlFields: ` + id + nameSingular + namePlural + labelSingular + labelPlural + description + icon + isActive + shortcut + standardOverrides { + labelSingular + labelPlural + description + icon + } + `, + }); + + expect(errors).toBeUndefined(); + expect(updateOneObject).toBeDefined(); + expect(updateOneObject.id).toBe(companyObjectMetadataId); + + expect(updateOneObject).toMatchObject({ + ...updatePayload, + }); + + expect(updateOneObject).toMatchSnapshot( + extractRecordIdsAndDatesAsExpectAny({ ...updateOneObject }), + ); + }, + ); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util.ts b/packages/twenty-server/test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util.ts index 4e5339cd7f9..08dae47bbf4 100644 --- a/packages/twenty-server/test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util.ts +++ b/packages/twenty-server/test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util.ts @@ -3,15 +3,20 @@ import { type UpdateOneObjectFactoryInput, updateOneObjectMetadataQueryFactory, } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata-query-factory.util'; +import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type'; import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util'; import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util'; +import { type ObjectMetadataDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-metadata.dto'; + export const updateOneObjectMetadata = async ({ input, gqlFields, expectToFail, -}: PerformMetadataQueryParams) => { +}: PerformMetadataQueryParams): CommonResponseBody<{ + updateOneObject: ObjectMetadataDTO; +}> => { const graphqlOperation = updateOneObjectMetadataQueryFactory({ input, gqlFields,