[v2_FIX] Update standard object/field (#15233)

# Introduction
Refactoring the standard overrides dispatcher to only pass over fields
to has to be dispatched in the standardOverrides entry and let the other
side effects resulting from out of standard overrides mutation trigger

Related to https://github.com/twentyhq/core-team-issues/issues/1753

## This allows
- standard field settings, options etc updates and so on

## Remark
- Determine what we should do on object deactivation ( right now in
production we can still access deactivated object relation properties
and so on e.g deactivate opportunities still accessible from a view
field on company ( still have to re-create it as it has been deleted )
=> decided to leave as it is right now, `isActive` could be considered
as uiDeactivated in the end
- We should also add forbidden standard field mutations validation
inside the builder itself ( here we want to early return in the api
input transpiler too as we don't want to spread invalid side effects )
=> or in the end we could just centralize both but it will generate
several errors

## Coverage
```ts
 PASS  test/integration/metadata/suites/object-metadata/successful-update-one-standard-object-metadata.integration-spec.ts
 PASS  test/integration/metadata/suites/field-metadata/successful-update-one-standard-field-metadata.integration-spec.ts
 PASS  test/integration/metadata/suites/object-metadata/failing-update-one-standard-object-metadata.integration-spec.ts
 PASS  test/integration/metadata/suites/field-metadata/failing-update-one-standard-field-metadata.integration-spec.ts

Test Suites: 4 passed, 4 total
Tests:       18 passed, 18 total
Snapshots:   16 passed, 16 total
Time:        8.721 s, estimated 10 s
```

## Update post review
Faced a behavior where updating back the company label to its original
value would result in storing this value in the standard overrides
Refactored both field and object transpilation behavior to rather remove
the standard override value instead and let fallback on original value

Yes it's quite duplicated will factorize once we move this inside the
builder
This commit is contained in:
Paul Rastoin
2025-10-23 11:02:18 +02:00
committed by GitHub
parent 23ae9e5192
commit 2e84c11eae
22 changed files with 1472 additions and 289 deletions
@@ -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 {
@@ -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<T extends AllMetadataName> = {
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'],
},
@@ -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)[]>;
@@ -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];
@@ -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<FlatFieldMetadataAndIndexToUpdate>(
(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,
@@ -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,
};
};
@@ -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)[]
>;
@@ -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<UpdatedFlatObjectAndRelatedFlatEntities>(
(
{
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,
};
};
@@ -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,
};
};
@@ -13,8 +13,8 @@ type RecomputeViewFieldIdentifierAfterFlatObjectIdentifierUpdateArgs = {
} & Pick<AllFlatEntityMaps, 'flatViewFieldMaps' | 'flatViewMaps'>;
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);
}
}
@@ -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,
};
};
@@ -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(