ViewGroup and ViewFilters side effect in v2 (#15096)
# Introduction ### Summary Implements side effect handling for `ViewGroup` and `ViewFilters` when field metadata is updated in the v2 architecture. This ensures that view-related records are properly maintained when enum field options are modified, deleted, or created. ### Side effects - **Side Effect System**: Added side effect handling for field metadata updates that manages related view groups and view filters - **Enum Field Updates**: When enum field options are modified, the system now: - **View Groups**: Creates new groups for added options, updates existing groups for modified options, and deletes groups for removed options - **View Filters**: Updates filter values to reflect option changes and removes filters that reference deleted options ### Enum runner fix Update now works for both atomic enum and array enum ( multi select for instance ) ### Compute flat entity maps from to Standardized this method usage across v2 services Next step is to require dependencies dynamically ## Conclusion closes https://github.com/twentyhq/core-team-issues/issues/1649
This commit is contained in:
+349
@@ -0,0 +1,349 @@
|
||||
import {
|
||||
type EachTestingContext,
|
||||
eachTestingContextFilter,
|
||||
} from 'twenty-shared/testing';
|
||||
|
||||
import { type FieldMetadataDefaultOption } from 'src/engine/metadata-modules/field-metadata/dtos/options.input';
|
||||
import {
|
||||
type CompareToFlatFieldMetadataEnumOptionsArgs,
|
||||
compareTwoFlatFieldMetadataEnumOptions,
|
||||
} from 'src/engine/metadata-modules/flat-field-metadata/utils/compare-two-flat-field-metadata-enum-options.util';
|
||||
|
||||
type GetOptionsDifferencesTestContext = EachTestingContext<
|
||||
CompareToFlatFieldMetadataEnumOptionsArgs & {
|
||||
expected: {
|
||||
created: FieldMetadataDefaultOption[];
|
||||
updated: {
|
||||
from: FieldMetadataDefaultOption;
|
||||
to: FieldMetadataDefaultOption;
|
||||
}[];
|
||||
deleted: FieldMetadataDefaultOption[];
|
||||
};
|
||||
}
|
||||
>;
|
||||
|
||||
describe('compareTwoFlatFieldMetadataEnumOptions', () => {
|
||||
const testCases: GetOptionsDifferencesTestContext[] = [
|
||||
{
|
||||
title: 'should identify created options',
|
||||
context: {
|
||||
compareLabel: false,
|
||||
fromOptions: [
|
||||
{ id: '1', label: 'Option 1', value: 'value1', position: 0 },
|
||||
],
|
||||
toOptions: [
|
||||
{ id: '1', label: 'Option 1', value: 'value1', position: 0 },
|
||||
{ id: '2', label: 'Option 2', value: 'value2', position: 1 },
|
||||
],
|
||||
expected: {
|
||||
created: [
|
||||
{ id: '2', label: 'Option 2', value: 'value2', position: 1 },
|
||||
],
|
||||
updated: [],
|
||||
deleted: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should identify updated options',
|
||||
context: {
|
||||
compareLabel: false,
|
||||
fromOptions: [
|
||||
{ id: '1', label: 'Option 1', value: 'value1', position: 0 },
|
||||
],
|
||||
toOptions: [
|
||||
{
|
||||
id: '1',
|
||||
label: 'Option 1',
|
||||
value: 'updated-value1',
|
||||
position: 0,
|
||||
},
|
||||
],
|
||||
expected: {
|
||||
created: [],
|
||||
updated: [
|
||||
{
|
||||
from: {
|
||||
id: '1',
|
||||
label: 'Option 1',
|
||||
value: 'value1',
|
||||
position: 0,
|
||||
},
|
||||
to: {
|
||||
id: '1',
|
||||
label: 'Option 1',
|
||||
value: 'updated-value1',
|
||||
position: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
deleted: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should identify deleted options',
|
||||
context: {
|
||||
compareLabel: false,
|
||||
fromOptions: [
|
||||
{ id: '1', label: 'Option 1', value: 'value1', position: 0 },
|
||||
{ id: '2', label: 'Option 2', value: 'value2', position: 1 },
|
||||
],
|
||||
toOptions: [
|
||||
{ id: '1', label: 'Option 1', value: 'value1', position: 0 },
|
||||
],
|
||||
expected: {
|
||||
created: [],
|
||||
updated: [],
|
||||
deleted: [
|
||||
{ id: '2', label: 'Option 2', value: 'value2', position: 1 },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should identify all types of changes',
|
||||
context: {
|
||||
compareLabel: false,
|
||||
fromOptions: [
|
||||
{ id: '1', label: 'Option 1', value: 'value1', position: 0 },
|
||||
{ id: '2', label: 'Option 2', value: 'value2', position: 1 },
|
||||
{ id: '3', label: 'Option 3', value: 'value3', position: 2 },
|
||||
],
|
||||
toOptions: [
|
||||
{
|
||||
id: '1',
|
||||
label: 'Option 1',
|
||||
value: 'updated-value1',
|
||||
position: 0,
|
||||
},
|
||||
{ id: '3', label: 'Option 3', value: 'value3', position: 1 },
|
||||
{ id: '4', label: 'Option 4', value: 'value4', position: 2 },
|
||||
],
|
||||
expected: {
|
||||
created: [
|
||||
{ id: '4', label: 'Option 4', value: 'value4', position: 2 },
|
||||
],
|
||||
updated: [
|
||||
{
|
||||
from: {
|
||||
id: '1',
|
||||
label: 'Option 1',
|
||||
value: 'value1',
|
||||
position: 0,
|
||||
},
|
||||
to: {
|
||||
id: '1',
|
||||
label: 'Option 1',
|
||||
value: 'updated-value1',
|
||||
position: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
deleted: [
|
||||
{ id: '2', label: 'Option 2', value: 'value2', position: 1 },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should handle empty arrays',
|
||||
context: {
|
||||
compareLabel: false,
|
||||
fromOptions: [],
|
||||
toOptions: [],
|
||||
expected: {
|
||||
created: [],
|
||||
updated: [],
|
||||
deleted: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should handle all new options',
|
||||
context: {
|
||||
compareLabel: false,
|
||||
fromOptions: [],
|
||||
toOptions: [
|
||||
{ id: '1', label: 'Option 1', value: 'value1', position: 0 },
|
||||
{ id: '2', label: 'Option 2', value: 'value2', position: 1 },
|
||||
],
|
||||
expected: {
|
||||
created: [
|
||||
{ id: '1', label: 'Option 1', value: 'value1', position: 0 },
|
||||
{ id: '2', label: 'Option 2', value: 'value2', position: 1 },
|
||||
],
|
||||
updated: [],
|
||||
deleted: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should handle all deleted options',
|
||||
context: {
|
||||
compareLabel: false,
|
||||
fromOptions: [
|
||||
{ id: '1', label: 'Option 1', value: 'value1', position: 0 },
|
||||
{ id: '2', label: 'Option 2', value: 'value2', position: 1 },
|
||||
],
|
||||
toOptions: [],
|
||||
expected: {
|
||||
created: [],
|
||||
updated: [],
|
||||
deleted: [
|
||||
{ id: '1', label: 'Option 1', value: 'value1', position: 0 },
|
||||
{ id: '2', label: 'Option 2', value: 'value2', position: 1 },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'should not consider changes to label as updates when value remains the same',
|
||||
context: {
|
||||
compareLabel: false,
|
||||
fromOptions: [
|
||||
{
|
||||
id: 'f86eaffd-b773-4c9a-957b-86dca4a62731',
|
||||
label: 'Option 0',
|
||||
value: 'option0',
|
||||
position: 1,
|
||||
},
|
||||
{
|
||||
id: '28d80b3c-79bd-4a1b-a868-9616534de0fa',
|
||||
label: 'Option 1',
|
||||
value: 'option1',
|
||||
position: 2,
|
||||
},
|
||||
{
|
||||
id: '25a05cd8-256f-4652-9e4a-6d9ca0b96f4d',
|
||||
label: 'Option 2',
|
||||
value: 'option2',
|
||||
position: 3,
|
||||
},
|
||||
],
|
||||
toOptions: [
|
||||
{
|
||||
id: 'f86eaffd-b773-4c9a-957b-86dca4a62731',
|
||||
label: 'Option 0_UPDATED', // Label changed but value remains the same
|
||||
value: 'option0',
|
||||
position: 1,
|
||||
},
|
||||
{
|
||||
id: '28d80b3c-79bd-4a1b-a868-9616534de0fa',
|
||||
label: 'Option 1', // No change
|
||||
value: 'option1',
|
||||
position: 2,
|
||||
},
|
||||
{
|
||||
id: '25a05cd8-256f-4652-9e4a-6d9ca0b96f4d',
|
||||
label: 'Option 2_UPDATED', // Label changed but value remains the same
|
||||
value: 'option2',
|
||||
position: 3,
|
||||
},
|
||||
],
|
||||
expected: {
|
||||
created: [],
|
||||
updated: [], // No updates because only labels changed, not values
|
||||
deleted: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'should consider changes to label as updates when value remains the same if compareLabel is true',
|
||||
context: {
|
||||
fromOptions: [
|
||||
{
|
||||
id: 'f86eaffd-b773-4c9a-957b-86dca4a62731',
|
||||
label: 'Option 0',
|
||||
value: 'option0',
|
||||
position: 1,
|
||||
},
|
||||
{
|
||||
id: '28d80b3c-79bd-4a1b-a868-9616534de0fa',
|
||||
label: 'Option 1',
|
||||
value: 'option1',
|
||||
position: 2,
|
||||
},
|
||||
{
|
||||
id: '25a05cd8-256f-4652-9e4a-6d9ca0b96f4d',
|
||||
label: 'Option 2',
|
||||
value: 'option2',
|
||||
position: 3,
|
||||
},
|
||||
],
|
||||
toOptions: [
|
||||
{
|
||||
id: 'f86eaffd-b773-4c9a-957b-86dca4a62731',
|
||||
label: 'Option 0_UPDATED', // Label changed but value remains the same
|
||||
value: 'option0',
|
||||
position: 1,
|
||||
},
|
||||
{
|
||||
id: '28d80b3c-79bd-4a1b-a868-9616534de0fa',
|
||||
label: 'Option 1', // No change
|
||||
value: 'option1',
|
||||
position: 2,
|
||||
},
|
||||
{
|
||||
id: '25a05cd8-256f-4652-9e4a-6d9ca0b96f4d',
|
||||
label: 'Option 2_UPDATED', // Label changed but value remains the same
|
||||
value: 'option2',
|
||||
position: 3,
|
||||
},
|
||||
],
|
||||
expected: {
|
||||
created: [],
|
||||
updated: [
|
||||
{
|
||||
to: {
|
||||
id: 'f86eaffd-b773-4c9a-957b-86dca4a62731',
|
||||
label: 'Option 0_UPDATED',
|
||||
position: 1,
|
||||
value: 'option0',
|
||||
},
|
||||
from: {
|
||||
id: 'f86eaffd-b773-4c9a-957b-86dca4a62731',
|
||||
label: 'Option 0',
|
||||
position: 1,
|
||||
value: 'option0',
|
||||
},
|
||||
},
|
||||
{
|
||||
to: {
|
||||
id: '25a05cd8-256f-4652-9e4a-6d9ca0b96f4d',
|
||||
label: 'Option 2_UPDATED',
|
||||
position: 3,
|
||||
value: 'option2',
|
||||
},
|
||||
from: {
|
||||
id: '25a05cd8-256f-4652-9e4a-6d9ca0b96f4d',
|
||||
label: 'Option 2',
|
||||
position: 3,
|
||||
value: 'option2',
|
||||
},
|
||||
},
|
||||
],
|
||||
deleted: [],
|
||||
},
|
||||
compareLabel: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
test.each(eachTestingContextFilter(testCases))(
|
||||
'$title',
|
||||
({ context: { fromOptions, toOptions, expected, compareLabel } }) => {
|
||||
const result = compareTwoFlatFieldMetadataEnumOptions({
|
||||
compareLabel: compareLabel ?? false,
|
||||
fromOptions: fromOptions,
|
||||
toOptions: toOptions,
|
||||
});
|
||||
|
||||
expect(result.created).toEqual(expected.created);
|
||||
expect(result.updated).toEqual(expected.updated);
|
||||
expect(result.deleted).toEqual(expected.deleted);
|
||||
},
|
||||
);
|
||||
});
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
import { type EnumFieldMetadataType, type FromTo } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
|
||||
type FieldMetadataEnumOptions =
|
||||
FlatFieldMetadata<EnumFieldMetadataType>['options'];
|
||||
|
||||
type Differences<T> = {
|
||||
created: T[];
|
||||
updated: { from: T; to: T }[];
|
||||
deleted: T[];
|
||||
};
|
||||
export type CompareToFlatFieldMetadataEnumOptionsArgs = FromTo<
|
||||
FieldMetadataEnumOptions,
|
||||
'options'
|
||||
> & {
|
||||
compareLabel: boolean;
|
||||
};
|
||||
export const compareTwoFlatFieldMetadataEnumOptions = ({
|
||||
compareLabel = false,
|
||||
fromOptions,
|
||||
toOptions,
|
||||
}: CompareToFlatFieldMetadataEnumOptionsArgs): Differences<
|
||||
FieldMetadataEnumOptions[number]
|
||||
> => {
|
||||
const differences: Differences<FieldMetadataEnumOptions[number]> = {
|
||||
created: [],
|
||||
updated: [],
|
||||
deleted: [],
|
||||
};
|
||||
|
||||
const fromOptionsMap = new Map(fromOptions.map((opt) => [opt.id, opt]));
|
||||
|
||||
for (const newOption of toOptions) {
|
||||
const oldOption = fromOptionsMap.get(newOption.id);
|
||||
|
||||
if (!isDefined(oldOption)) {
|
||||
differences.created.push(newOption);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
oldOption.value !== newOption.value ||
|
||||
(compareLabel && oldOption.label !== newOption.label)
|
||||
) {
|
||||
differences.updated.push({ from: oldOption, to: newOption });
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const toOptionsMap = new Map(toOptions.map((opt) => [opt.id, opt]));
|
||||
|
||||
for (const oldOption of fromOptions) {
|
||||
if (!toOptionsMap.has(oldOption.id)) {
|
||||
differences.deleted.push(oldOption);
|
||||
}
|
||||
}
|
||||
|
||||
return differences;
|
||||
};
|
||||
+5
-5
@@ -4,7 +4,7 @@ import { removePropertiesFromRecord } from 'twenty-shared/utils';
|
||||
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import {
|
||||
type FlatFieldMetadata,
|
||||
fieldMetadataRelationProperties,
|
||||
FIELD_METADATA_RELATION_PROPERTIES,
|
||||
} from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
|
||||
export const fromFieldMetadataEntityToFlatFieldMetadata = <
|
||||
@@ -15,14 +15,14 @@ export const fromFieldMetadataEntityToFlatFieldMetadata = <
|
||||
): FlatFieldMetadata => {
|
||||
const fieldMetadataWithoutRelations = removePropertiesFromRecord(
|
||||
fieldMetadataEntity,
|
||||
fieldMetadataRelationProperties,
|
||||
FIELD_METADATA_RELATION_PROPERTIES,
|
||||
);
|
||||
|
||||
return {
|
||||
...fieldMetadataWithoutRelations,
|
||||
viewFieldIds: fieldMetadataEntity.viewFields.map(
|
||||
(viewField) => viewField.id,
|
||||
),
|
||||
viewGroupIds: fieldMetadataEntity.viewGroups.map(({ id }) => id),
|
||||
viewFieldIds: fieldMetadataEntity.viewFields.map(({ id }) => id),
|
||||
viewFilterIds: fieldMetadataEntity.viewFilters.map(({ id }) => id),
|
||||
universalIdentifier:
|
||||
fieldMetadataWithoutRelations.standardId ??
|
||||
fieldMetadataWithoutRelations.id,
|
||||
|
||||
+84
-105
@@ -16,110 +16,36 @@ import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { FLAT_FIELD_METADATA_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-editable-properties.constant';
|
||||
import { type FieldInputTranspilationResult } from 'src/engine/metadata-modules/flat-field-metadata/types/field-input-transpilation-result.type';
|
||||
import { type 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 { computeFlatFieldMetadataRelatedFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/compute-flat-field-metadata-related-flat-field-metadata.util';
|
||||
import { handleIndexChangesDuringFieldUpdate } from 'src/engine/metadata-modules/flat-field-metadata/utils/handle-index-changes-during-field-update.util';
|
||||
import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
|
||||
import {
|
||||
type FlatFieldMetadataUpdateSideEffects,
|
||||
handleFlatFieldMetadataUpdateSideEffect,
|
||||
} from 'src/engine/metadata-modules/flat-field-metadata/utils/handle-flat-field-metadata-update-side-effect.util';
|
||||
import { isStandardMetadata } from 'src/engine/metadata-modules/utils/is-standard-metadata.util';
|
||||
|
||||
type UpdatedFlatFieldMetadataAndIndexToUpdate = {
|
||||
flatFieldMetadata: FlatFieldMetadata;
|
||||
};
|
||||
|
||||
type SanitizedUpdateFieldInput = ReturnType<
|
||||
typeof extractAndSanitizeObjectStringFields<
|
||||
UpdateFieldInput,
|
||||
FlatFieldMetadataEditableProperties[]
|
||||
>
|
||||
>;
|
||||
|
||||
type ApplyUpdatesToFlatFieldMetadataArgs = {
|
||||
updatedEditableFieldProperties: SanitizedUpdateFieldInput;
|
||||
fromFlatFieldMetadata: FlatFieldMetadata;
|
||||
} & Pick<
|
||||
AllFlatEntityMaps,
|
||||
'flatIndexMaps' | 'flatObjectMetadataMaps' | 'flatFieldMetadataMaps'
|
||||
>;
|
||||
|
||||
const applyUpdatesToFlatFieldMetadata = ({
|
||||
updatedEditableFieldProperties,
|
||||
fromFlatFieldMetadata,
|
||||
flatObjectMetadataMaps,
|
||||
flatIndexMaps,
|
||||
flatFieldMetadataMaps,
|
||||
}: ApplyUpdatesToFlatFieldMetadataArgs): {
|
||||
flatFieldMetadata: FlatFieldMetadata;
|
||||
flatIndexMetadataToUpdate: FlatIndexMetadata[];
|
||||
flatIndexMetadatasToDelete: FlatIndexMetadata[];
|
||||
flatIndexMetadatasToCreate: FlatIndexMetadata[];
|
||||
} => {
|
||||
const { flatFieldMetadata: updatedFlatFieldMetadata } =
|
||||
FLAT_FIELD_METADATA_EDITABLE_PROPERTIES.reduce<UpdatedFlatFieldMetadataAndIndexToUpdate>(
|
||||
({ flatFieldMetadata }, property) => {
|
||||
const updatedPropertyValue = updatedEditableFieldProperties[property];
|
||||
const isPropertyUpdated =
|
||||
updatedPropertyValue !== undefined &&
|
||||
flatFieldMetadata[property] !== updatedPropertyValue;
|
||||
|
||||
if (!isPropertyUpdated) {
|
||||
return { flatFieldMetadata };
|
||||
}
|
||||
|
||||
const updatedFlatFieldMetadata = {
|
||||
...flatFieldMetadata,
|
||||
[property]: updatedPropertyValue,
|
||||
};
|
||||
|
||||
if (property === 'options') {
|
||||
updatedFlatFieldMetadata.options =
|
||||
updatedEditableFieldProperties[property]?.map((option) => ({
|
||||
id: v4(),
|
||||
...option,
|
||||
})) ?? [];
|
||||
}
|
||||
|
||||
return {
|
||||
flatFieldMetadata: updatedFlatFieldMetadata,
|
||||
};
|
||||
},
|
||||
{
|
||||
flatFieldMetadata: structuredClone(fromFlatFieldMetadata),
|
||||
},
|
||||
);
|
||||
|
||||
const indexChanges = handleIndexChangesDuringFieldUpdate({
|
||||
originalFlatFieldMetadata: fromFlatFieldMetadata,
|
||||
updatedFlatFieldMetadata,
|
||||
flatIndexMaps,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
});
|
||||
|
||||
return {
|
||||
flatFieldMetadata: updatedFlatFieldMetadata,
|
||||
...indexChanges,
|
||||
};
|
||||
};
|
||||
import { mergeUpdateInExistingRecord } from 'src/utils/merge-update-in-existing-record.util';
|
||||
|
||||
type FromUpdateFieldInputToFlatFieldMetadataArgs = {
|
||||
updateFieldInput: UpdateFieldInput;
|
||||
} & Pick<
|
||||
AllFlatEntityMaps,
|
||||
'flatObjectMetadataMaps' | 'flatIndexMaps' | 'flatFieldMetadataMaps'
|
||||
| 'flatObjectMetadataMaps'
|
||||
| 'flatIndexMaps'
|
||||
| 'flatFieldMetadataMaps'
|
||||
| 'flatViewFilterMaps'
|
||||
| 'flatViewGroupMaps'
|
||||
>;
|
||||
|
||||
type FlatFieldMetadataAndIndexToUpdate = {
|
||||
flatFieldMetadatasToUpdate: FlatFieldMetadata[];
|
||||
flatIndexMetadatasToUpdate: FlatIndexMetadata[];
|
||||
flatIndexMetadatasToDelete: FlatIndexMetadata[];
|
||||
flatIndexMetadatasToCreate: FlatIndexMetadata[];
|
||||
};
|
||||
} & FlatFieldMetadataUpdateSideEffects;
|
||||
export const fromUpdateFieldInputToFlatFieldMetadata = ({
|
||||
flatIndexMaps,
|
||||
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
updateFieldInput: rawUpdateFieldInput,
|
||||
flatViewFilterMaps,
|
||||
flatViewGroupMaps,
|
||||
}: FromUpdateFieldInputToFlatFieldMetadataArgs): FieldInputTranspilationResult<FlatFieldMetadataAndIndexToUpdate> => {
|
||||
const updateFieldInputInformalProperties =
|
||||
extractAndSanitizeObjectStringFields(rawUpdateFieldInput, [
|
||||
@@ -188,10 +114,15 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({
|
||||
return {
|
||||
status: 'success',
|
||||
result: {
|
||||
flatViewGroupsToCreate: [],
|
||||
flatViewGroupsToDelete: [],
|
||||
flatViewGroupsToUpdate: [],
|
||||
flatFieldMetadatasToUpdate: [updatedStandardFlatFieldMetadata],
|
||||
flatIndexMetadatasToUpdate: [],
|
||||
flatIndexMetadatasToDelete: [],
|
||||
flatIndexMetadatasToCreate: [],
|
||||
flatViewFiltersToDelete: [],
|
||||
flatViewFiltersToUpdate: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -220,47 +151,95 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({
|
||||
...relatedFlatFieldMetadatasToUpdate,
|
||||
];
|
||||
|
||||
const initialAccumulator: FlatFieldMetadataAndIndexToUpdate = {
|
||||
flatFieldMetadatasToUpdate: [],
|
||||
flatIndexMetadatasToUpdate: [],
|
||||
flatViewFiltersToDelete: [],
|
||||
flatViewFiltersToUpdate: [],
|
||||
flatViewGroupsToCreate: [],
|
||||
flatViewGroupsToDelete: [],
|
||||
flatIndexMetadatasToCreate: [],
|
||||
flatIndexMetadatasToDelete: [],
|
||||
flatViewGroupsToUpdate: [],
|
||||
};
|
||||
|
||||
updatedEditableFieldProperties.options = !isDefined(
|
||||
updatedEditableFieldProperties.options,
|
||||
)
|
||||
? updatedEditableFieldProperties.options
|
||||
: updatedEditableFieldProperties.options.map((option) => ({
|
||||
id: v4(),
|
||||
...option,
|
||||
}));
|
||||
|
||||
const optimisticiallyUpdatedFlatFieldMetadatas =
|
||||
flatFieldMetadatasToUpdate.reduce<FlatFieldMetadataAndIndexToUpdate>(
|
||||
(acc, fromFlatFieldMetadata) => {
|
||||
(accumulator, fromFlatFieldMetadata) => {
|
||||
const toFlatFieldMetadata = mergeUpdateInExistingRecord({
|
||||
existing: fromFlatFieldMetadata,
|
||||
properties: FLAT_FIELD_METADATA_EDITABLE_PROPERTIES,
|
||||
update: updatedEditableFieldProperties,
|
||||
});
|
||||
|
||||
const {
|
||||
flatFieldMetadata,
|
||||
flatIndexMetadataToUpdate,
|
||||
flatIndexMetadatasToDelete,
|
||||
flatViewGroupsToCreate,
|
||||
flatViewGroupsToDelete,
|
||||
flatViewGroupsToUpdate,
|
||||
flatIndexMetadatasToUpdate,
|
||||
flatViewFiltersToDelete,
|
||||
flatViewFiltersToUpdate,
|
||||
flatIndexMetadatasToCreate,
|
||||
} = applyUpdatesToFlatFieldMetadata({
|
||||
flatIndexMetadatasToDelete,
|
||||
} = handleFlatFieldMetadataUpdateSideEffect({
|
||||
flatViewFilterMaps,
|
||||
flatViewGroupMaps,
|
||||
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
|
||||
fromFlatFieldMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
flatIndexMaps,
|
||||
updatedEditableFieldProperties,
|
||||
toFlatFieldMetadata,
|
||||
});
|
||||
|
||||
return {
|
||||
flatFieldMetadatasToUpdate: [
|
||||
...acc.flatFieldMetadatasToUpdate,
|
||||
flatFieldMetadata,
|
||||
...accumulator.flatFieldMetadatasToUpdate,
|
||||
toFlatFieldMetadata,
|
||||
],
|
||||
flatIndexMetadatasToUpdate: [
|
||||
...acc.flatIndexMetadatasToUpdate,
|
||||
...flatIndexMetadataToUpdate,
|
||||
...accumulator.flatIndexMetadatasToUpdate,
|
||||
...flatIndexMetadatasToUpdate,
|
||||
],
|
||||
flatViewFiltersToDelete: [
|
||||
...accumulator.flatViewFiltersToDelete,
|
||||
...flatViewFiltersToDelete,
|
||||
],
|
||||
flatViewFiltersToUpdate: [
|
||||
...accumulator.flatViewFiltersToUpdate,
|
||||
...flatViewFiltersToUpdate,
|
||||
],
|
||||
flatViewGroupsToCreate: [
|
||||
...accumulator.flatViewGroupsToCreate,
|
||||
...flatViewGroupsToCreate,
|
||||
],
|
||||
flatViewGroupsToDelete: [
|
||||
...accumulator.flatViewGroupsToDelete,
|
||||
...flatViewGroupsToDelete,
|
||||
],
|
||||
flatViewGroupsToUpdate: [
|
||||
...accumulator.flatViewGroupsToUpdate,
|
||||
...flatViewGroupsToUpdate,
|
||||
],
|
||||
flatIndexMetadatasToDelete: [
|
||||
...acc.flatIndexMetadatasToDelete,
|
||||
...accumulator.flatIndexMetadatasToDelete,
|
||||
...flatIndexMetadatasToDelete,
|
||||
],
|
||||
flatIndexMetadatasToCreate: [
|
||||
...acc.flatIndexMetadatasToCreate,
|
||||
...accumulator.flatIndexMetadatasToCreate,
|
||||
...flatIndexMetadatasToCreate,
|
||||
],
|
||||
};
|
||||
},
|
||||
{
|
||||
flatFieldMetadatasToUpdate: [],
|
||||
flatIndexMetadatasToUpdate: [],
|
||||
flatIndexMetadatasToDelete: [],
|
||||
flatIndexMetadatasToCreate: [],
|
||||
},
|
||||
initialAccumulator,
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
+4
-2
@@ -37,8 +37,8 @@ export const getDefaultFlatFieldMetadata = ({
|
||||
),
|
||||
isSystem: false,
|
||||
isUnique: createFieldInput.isUnique ?? null,
|
||||
label: createFieldInput.label ?? null,
|
||||
name: createFieldInput.name ?? null,
|
||||
label: createFieldInput.label,
|
||||
name: createFieldInput.name,
|
||||
objectMetadataId: createFieldInput.objectMetadataId,
|
||||
relationTargetFieldMetadataId: null,
|
||||
relationTargetObjectMetadataId: null,
|
||||
@@ -54,5 +54,7 @@ export const getDefaultFlatFieldMetadata = ({
|
||||
updatedAt: createdAt,
|
||||
isUIReadOnly: createFieldInput.isUIReadOnly ?? false,
|
||||
morphId: null,
|
||||
viewFilterIds: [],
|
||||
viewGroupIds: [],
|
||||
} as const satisfies FlatFieldMetadata;
|
||||
};
|
||||
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
import { type EnumFieldMetadataType, type FromTo } from 'twenty-shared/types';
|
||||
|
||||
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 { isEnumFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-enum-flat-field-metadata.util';
|
||||
import {
|
||||
type FlatViewFiltersToDeleteAndUpdate,
|
||||
recomputeViewFiltersOnFlatFieldMetadataOptionsUpdate,
|
||||
} from 'src/engine/metadata-modules/flat-field-metadata/utils/recompute-view-filters-on-flat-field-metadata-options-update.util';
|
||||
import { recomputeViewGroupsOnEnumFlatFieldMetadataIsNullableUpdate } from 'src/engine/metadata-modules/flat-field-metadata/utils/recompute-view-groups-on-enum-flat-field-metadata-is-nullable-update.util';
|
||||
import {
|
||||
type FlatViewGroupsToDeleteUpdateAndCreate,
|
||||
recomputeViewGroupsOnFlatFieldMetadataOptionsUpdate,
|
||||
} from 'src/engine/metadata-modules/flat-field-metadata/utils/recompute-view-groups-on-flat-field-metadata-options-update.util';
|
||||
import { type PropertyUpdate } from 'src/engine/workspace-manager/workspace-migration-v2/types/property-update.type';
|
||||
|
||||
type HandleEnumFlatFieldMetadataOptionsUpdateSideEffectsArgs = FromTo<
|
||||
FlatFieldMetadata,
|
||||
'flatFieldMetadata'
|
||||
> &
|
||||
Pick<AllFlatEntityMaps, 'flatViewFilterMaps' | 'flatViewGroupMaps'>;
|
||||
|
||||
type EnumFieldMetadataSideEffectResult = FlatViewGroupsToDeleteUpdateAndCreate &
|
||||
FlatViewFiltersToDeleteAndUpdate;
|
||||
|
||||
const EMPTY_ENUM_FIELD_METADATA_SIDE_EFFECT_RESULT: EnumFieldMetadataSideEffectResult =
|
||||
{
|
||||
flatViewFiltersToDelete: [],
|
||||
flatViewFiltersToUpdate: [],
|
||||
flatViewGroupsToCreate: [],
|
||||
flatViewGroupsToDelete: [],
|
||||
flatViewGroupsToUpdate: [],
|
||||
};
|
||||
|
||||
export const handleEnumFlatFieldMetadataUpdateSideEffects = ({
|
||||
fromFlatFieldMetadata,
|
||||
toFlatFieldMetadata,
|
||||
flatViewFilterMaps,
|
||||
flatViewGroupMaps,
|
||||
}: HandleEnumFlatFieldMetadataOptionsUpdateSideEffectsArgs): EnumFieldMetadataSideEffectResult => {
|
||||
const sideEffectResult = structuredClone(
|
||||
EMPTY_ENUM_FIELD_METADATA_SIDE_EFFECT_RESULT,
|
||||
);
|
||||
|
||||
if (
|
||||
!isEnumFlatFieldMetadata(toFlatFieldMetadata) ||
|
||||
!isEnumFlatFieldMetadata(fromFlatFieldMetadata)
|
||||
) {
|
||||
return sideEffectResult;
|
||||
}
|
||||
|
||||
if (
|
||||
JSON.stringify(fromFlatFieldMetadata.options) !==
|
||||
JSON.stringify(toFlatFieldMetadata.options)
|
||||
) {
|
||||
const optionsPropertyUpdate: PropertyUpdate<
|
||||
FlatFieldMetadata<EnumFieldMetadataType>,
|
||||
'options'
|
||||
> = {
|
||||
from: fromFlatFieldMetadata.options,
|
||||
property: 'options',
|
||||
to: toFlatFieldMetadata.options,
|
||||
};
|
||||
|
||||
const { flatViewFiltersToDelete, flatViewFiltersToUpdate } =
|
||||
recomputeViewFiltersOnFlatFieldMetadataOptionsUpdate({
|
||||
flatViewFilterMaps,
|
||||
fromFlatFieldMetadata,
|
||||
update: optionsPropertyUpdate,
|
||||
});
|
||||
|
||||
sideEffectResult.flatViewFiltersToDelete.push(...flatViewFiltersToDelete);
|
||||
sideEffectResult.flatViewFiltersToUpdate.push(...flatViewFiltersToUpdate);
|
||||
|
||||
const {
|
||||
flatViewGroupsToCreate,
|
||||
flatViewGroupsToDelete,
|
||||
flatViewGroupsToUpdate,
|
||||
} = recomputeViewGroupsOnFlatFieldMetadataOptionsUpdate({
|
||||
flatViewGroupMaps,
|
||||
fromFlatFieldMetadata,
|
||||
update: optionsPropertyUpdate,
|
||||
});
|
||||
|
||||
sideEffectResult.flatViewGroupsToCreate.push(...flatViewGroupsToCreate);
|
||||
sideEffectResult.flatViewGroupsToDelete.push(...flatViewGroupsToDelete);
|
||||
sideEffectResult.flatViewGroupsToUpdate.push(...flatViewGroupsToUpdate);
|
||||
}
|
||||
|
||||
if (fromFlatFieldMetadata.isNullable !== toFlatFieldMetadata.isNullable) {
|
||||
const { flatViewGroupsToCreate, flatViewGroupsToDelete } =
|
||||
recomputeViewGroupsOnEnumFlatFieldMetadataIsNullableUpdate({
|
||||
flatViewGroupMaps,
|
||||
fromFlatFieldMetadata,
|
||||
toFlatFieldMetadata,
|
||||
});
|
||||
|
||||
sideEffectResult.flatViewGroupsToCreate.push(...flatViewGroupsToCreate);
|
||||
sideEffectResult.flatViewGroupsToDelete.push(...flatViewGroupsToDelete);
|
||||
}
|
||||
|
||||
return sideEffectResult;
|
||||
};
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
import { type FromTo } from 'twenty-shared/types';
|
||||
|
||||
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { handleEnumFlatFieldMetadataUpdateSideEffects } from 'src/engine/metadata-modules/flat-field-metadata/utils/handle-enum-flat-field-metadata-update-side-effects.util';
|
||||
import {
|
||||
type FieldMetadataUpdateIndexSideEffect,
|
||||
handleIndexChangesDuringFieldUpdate,
|
||||
} from 'src/engine/metadata-modules/flat-field-metadata/utils/handle-index-changes-during-field-update.util';
|
||||
import { type FlatViewFiltersToDeleteAndUpdate } from 'src/engine/metadata-modules/flat-field-metadata/utils/recompute-view-filters-on-flat-field-metadata-options-update.util';
|
||||
import { type FlatViewGroupsToDeleteUpdateAndCreate } from 'src/engine/metadata-modules/flat-field-metadata/utils/recompute-view-groups-on-flat-field-metadata-options-update.util';
|
||||
|
||||
export type FlatFieldMetadataUpdateSideEffects =
|
||||
FlatViewFiltersToDeleteAndUpdate &
|
||||
FlatViewGroupsToDeleteUpdateAndCreate &
|
||||
FieldMetadataUpdateIndexSideEffect;
|
||||
|
||||
type HandleFlatFieldMetadataUpdateSideEffectArgs = FromTo<
|
||||
FlatFieldMetadata,
|
||||
'flatFieldMetadata'
|
||||
> &
|
||||
Pick<
|
||||
AllFlatEntityMaps,
|
||||
| 'flatIndexMaps'
|
||||
| 'flatObjectMetadataMaps'
|
||||
| 'flatFieldMetadataMaps'
|
||||
| 'flatViewFilterMaps'
|
||||
| 'flatViewGroupMaps'
|
||||
>;
|
||||
|
||||
export const handleFlatFieldMetadataUpdateSideEffect = ({
|
||||
fromFlatFieldMetadata,
|
||||
toFlatFieldMetadata,
|
||||
flatObjectMetadataMaps,
|
||||
flatIndexMaps,
|
||||
flatFieldMetadataMaps,
|
||||
flatViewFilterMaps,
|
||||
flatViewGroupMaps,
|
||||
}: HandleFlatFieldMetadataUpdateSideEffectArgs): FlatFieldMetadataUpdateSideEffects => {
|
||||
const {
|
||||
flatViewFiltersToDelete,
|
||||
flatViewFiltersToUpdate,
|
||||
flatViewGroupsToCreate,
|
||||
flatViewGroupsToDelete,
|
||||
flatViewGroupsToUpdate,
|
||||
} = handleEnumFlatFieldMetadataUpdateSideEffects({
|
||||
flatViewFilterMaps,
|
||||
flatViewGroupMaps,
|
||||
fromFlatFieldMetadata,
|
||||
toFlatFieldMetadata,
|
||||
});
|
||||
|
||||
const {
|
||||
flatIndexMetadatasToUpdate,
|
||||
flatIndexMetadatasToCreate,
|
||||
flatIndexMetadatasToDelete,
|
||||
} = handleIndexChangesDuringFieldUpdate({
|
||||
fromFlatFieldMetadata,
|
||||
toFlatFieldMetadata,
|
||||
flatIndexMaps,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
});
|
||||
|
||||
return {
|
||||
flatIndexMetadatasToUpdate,
|
||||
flatViewFiltersToDelete,
|
||||
flatViewFiltersToUpdate,
|
||||
flatViewGroupsToCreate,
|
||||
flatViewGroupsToDelete,
|
||||
flatIndexMetadatasToDelete,
|
||||
flatIndexMetadatasToCreate,
|
||||
flatViewGroupsToUpdate,
|
||||
};
|
||||
};
|
||||
+55
-52
@@ -1,3 +1,5 @@
|
||||
import { type FromTo } from 'twenty-shared/types';
|
||||
|
||||
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
@@ -8,61 +10,64 @@ import { recomputeIndexOnFlatFieldMetadataNameUpdate } from 'src/engine/metadata
|
||||
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';
|
||||
|
||||
type IndexChanges = {
|
||||
flatIndexMetadataToUpdate: FlatIndexMetadata[];
|
||||
export type FieldMetadataUpdateIndexSideEffect = {
|
||||
flatIndexMetadatasToUpdate: FlatIndexMetadata[];
|
||||
flatIndexMetadatasToDelete: FlatIndexMetadata[];
|
||||
flatIndexMetadatasToCreate: FlatIndexMetadata[];
|
||||
};
|
||||
|
||||
const NO_INDEX_CHANGES: IndexChanges = {
|
||||
flatIndexMetadataToUpdate: [],
|
||||
flatIndexMetadatasToDelete: [],
|
||||
flatIndexMetadatasToCreate: [],
|
||||
};
|
||||
type FromToFlatFieldMetadataAndFlatEntityMaps = FromTo<
|
||||
FlatFieldMetadata,
|
||||
'flatFieldMetadata'
|
||||
> &
|
||||
Pick<
|
||||
AllFlatEntityMaps,
|
||||
'flatObjectMetadataMaps' | 'flatFieldMetadataMaps' | 'flatIndexMaps'
|
||||
>;
|
||||
const FIELD_METADATA_UPDATE_INDEX_SIDE_EFFECT: FieldMetadataUpdateIndexSideEffect =
|
||||
{
|
||||
flatIndexMetadatasToUpdate: [],
|
||||
flatIndexMetadatasToDelete: [],
|
||||
flatIndexMetadatasToCreate: [],
|
||||
};
|
||||
|
||||
export const handleIndexChangesDuringFieldUpdate = ({
|
||||
originalFlatFieldMetadata,
|
||||
updatedFlatFieldMetadata,
|
||||
fromFlatFieldMetadata,
|
||||
toFlatFieldMetadata,
|
||||
flatIndexMaps,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
}: {
|
||||
originalFlatFieldMetadata: FlatFieldMetadata;
|
||||
updatedFlatFieldMetadata: FlatFieldMetadata;
|
||||
flatIndexMaps: FlatEntityMaps<FlatIndexMetadata>;
|
||||
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
|
||||
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
|
||||
}): IndexChanges => {
|
||||
}: FromToFlatFieldMetadataAndFlatEntityMaps): FieldMetadataUpdateIndexSideEffect => {
|
||||
if (
|
||||
!hasIndexRelevantChanges({
|
||||
originalFlatFieldMetadata,
|
||||
updatedFlatFieldMetadata,
|
||||
fromFlatFieldMetadata,
|
||||
toFlatFieldMetadata,
|
||||
})
|
||||
) {
|
||||
return NO_INDEX_CHANGES;
|
||||
return FIELD_METADATA_UPDATE_INDEX_SIDE_EFFECT;
|
||||
}
|
||||
|
||||
const flatObjectMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityMaps: flatObjectMetadataMaps,
|
||||
flatEntityId: originalFlatFieldMetadata.objectMetadataId,
|
||||
flatEntityId: fromFlatFieldMetadata.objectMetadataId,
|
||||
});
|
||||
|
||||
const relatedIndexes = findRelatedIndexes({
|
||||
flatFieldMetadata: originalFlatFieldMetadata,
|
||||
flatFieldMetadata: fromFlatFieldMetadata,
|
||||
flatObjectMetadata,
|
||||
flatIndexMaps,
|
||||
});
|
||||
|
||||
if (relatedIndexes.length === 0) {
|
||||
return handleNoExistingIndexes({
|
||||
updatedFlatFieldMetadata,
|
||||
toFlatFieldMetadata,
|
||||
flatObjectMetadata,
|
||||
});
|
||||
}
|
||||
|
||||
return handleExistingIndexes({
|
||||
updatedFlatFieldMetadata,
|
||||
originalFlatFieldMetadata,
|
||||
toFlatFieldMetadata,
|
||||
fromFlatFieldMetadata,
|
||||
relatedIndexes,
|
||||
flatObjectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
@@ -70,14 +75,11 @@ export const handleIndexChangesDuringFieldUpdate = ({
|
||||
};
|
||||
|
||||
const hasIndexRelevantChanges = ({
|
||||
originalFlatFieldMetadata,
|
||||
updatedFlatFieldMetadata,
|
||||
}: {
|
||||
originalFlatFieldMetadata: FlatFieldMetadata;
|
||||
updatedFlatFieldMetadata: FlatFieldMetadata;
|
||||
}): boolean =>
|
||||
originalFlatFieldMetadata.name !== updatedFlatFieldMetadata.name ||
|
||||
originalFlatFieldMetadata.isUnique !== updatedFlatFieldMetadata.isUnique;
|
||||
fromFlatFieldMetadata,
|
||||
toFlatFieldMetadata,
|
||||
}: FromTo<FlatFieldMetadata, 'flatFieldMetadata'>): boolean =>
|
||||
fromFlatFieldMetadata.name !== toFlatFieldMetadata.name ||
|
||||
fromFlatFieldMetadata.isUnique !== toFlatFieldMetadata.isUnique;
|
||||
|
||||
const findRelatedIndexes = ({
|
||||
flatFieldMetadata,
|
||||
@@ -101,45 +103,46 @@ const findRelatedIndexes = ({
|
||||
};
|
||||
|
||||
const handleNoExistingIndexes = ({
|
||||
updatedFlatFieldMetadata,
|
||||
toFlatFieldMetadata,
|
||||
flatObjectMetadata,
|
||||
}: {
|
||||
updatedFlatFieldMetadata: FlatFieldMetadata;
|
||||
toFlatFieldMetadata: FlatFieldMetadata;
|
||||
flatObjectMetadata: FlatObjectMetadata;
|
||||
}): IndexChanges => {
|
||||
if (!updatedFlatFieldMetadata.isUnique) {
|
||||
return NO_INDEX_CHANGES;
|
||||
}): FieldMetadataUpdateIndexSideEffect => {
|
||||
if (!toFlatFieldMetadata.isUnique) {
|
||||
return FIELD_METADATA_UPDATE_INDEX_SIDE_EFFECT;
|
||||
}
|
||||
|
||||
const newIndex = generateIndexForFlatFieldMetadata({
|
||||
flatFieldMetadata: updatedFlatFieldMetadata,
|
||||
flatFieldMetadata: toFlatFieldMetadata,
|
||||
flatObjectMetadata,
|
||||
workspaceId: flatObjectMetadata.workspaceId,
|
||||
});
|
||||
|
||||
return {
|
||||
...NO_INDEX_CHANGES,
|
||||
...FIELD_METADATA_UPDATE_INDEX_SIDE_EFFECT,
|
||||
flatIndexMetadatasToCreate: [newIndex],
|
||||
};
|
||||
};
|
||||
|
||||
const handleExistingIndexes = ({
|
||||
updatedFlatFieldMetadata,
|
||||
originalFlatFieldMetadata,
|
||||
fromFlatFieldMetadata,
|
||||
toFlatFieldMetadata,
|
||||
relatedIndexes,
|
||||
flatObjectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
}: {
|
||||
updatedFlatFieldMetadata: FlatFieldMetadata;
|
||||
originalFlatFieldMetadata: FlatFieldMetadata;
|
||||
relatedIndexes: FlatIndexMetadata[];
|
||||
flatObjectMetadata: FlatObjectMetadata;
|
||||
flatFieldMetadataMaps: AllFlatEntityMaps['flatFieldMetadataMaps'];
|
||||
}): IndexChanges => {
|
||||
if (updatedFlatFieldMetadata.isUnique === false) {
|
||||
} & FromTo<
|
||||
FlatFieldMetadata,
|
||||
'flatFieldMetadata'
|
||||
>): FieldMetadataUpdateIndexSideEffect => {
|
||||
if (toFlatFieldMetadata.isUnique === false) {
|
||||
const expectedUniqueIndex = generateIndexForFlatFieldMetadata({
|
||||
flatFieldMetadata: {
|
||||
...originalFlatFieldMetadata,
|
||||
...fromFlatFieldMetadata,
|
||||
isUnique: true,
|
||||
},
|
||||
flatObjectMetadata,
|
||||
@@ -151,7 +154,7 @@ const handleExistingIndexes = ({
|
||||
);
|
||||
|
||||
return {
|
||||
...NO_INDEX_CHANGES,
|
||||
...FIELD_METADATA_UPDATE_INDEX_SIDE_EFFECT,
|
||||
flatIndexMetadatasToDelete: uniqueIndexToDelete
|
||||
? [uniqueIndexToDelete]
|
||||
: [],
|
||||
@@ -161,16 +164,16 @@ const handleExistingIndexes = ({
|
||||
const updatedIndexes = recomputeIndexOnFlatFieldMetadataNameUpdate({
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadata,
|
||||
fromFlatFieldMetadata: originalFlatFieldMetadata,
|
||||
fromFlatFieldMetadata,
|
||||
toFlatFieldMetadata: {
|
||||
name: updatedFlatFieldMetadata.name,
|
||||
isUnique: updatedFlatFieldMetadata.isUnique,
|
||||
name: toFlatFieldMetadata.name,
|
||||
isUnique: toFlatFieldMetadata.isUnique,
|
||||
},
|
||||
relatedFlatIndexMetadata: relatedIndexes,
|
||||
});
|
||||
|
||||
return {
|
||||
...NO_INDEX_CHANGES,
|
||||
flatIndexMetadataToUpdate: updatedIndexes,
|
||||
...FIELD_METADATA_UPDATE_INDEX_SIDE_EFFECT,
|
||||
flatIndexMetadatasToUpdate: updatedIndexes,
|
||||
};
|
||||
};
|
||||
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { type EnumFieldMetadataType } from 'twenty-shared/types';
|
||||
import { isDefined, parseJson } from 'twenty-shared/utils';
|
||||
|
||||
import {
|
||||
FieldMetadataException,
|
||||
FieldMetadataExceptionCode,
|
||||
} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
|
||||
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
|
||||
import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { compareTwoFlatFieldMetadataEnumOptions } from 'src/engine/metadata-modules/flat-field-metadata/utils/compare-two-flat-field-metadata-enum-options.util';
|
||||
import { type FlatViewFilter } from 'src/engine/metadata-modules/flat-view-filter/types/flat-view-filter.type';
|
||||
import { type PropertyUpdate } from 'src/engine/workspace-manager/workspace-migration-v2/types/property-update.type';
|
||||
|
||||
type RecomputeViewFiltersOnFlatFieldMetadataOptionsUpdateArgs = {
|
||||
fromFlatFieldMetadata: FlatFieldMetadata<EnumFieldMetadataType>;
|
||||
update: PropertyUpdate<FlatFieldMetadata<EnumFieldMetadataType>, 'options'>;
|
||||
} & Pick<AllFlatEntityMaps, 'flatViewFilterMaps'>;
|
||||
|
||||
export type FlatViewFiltersToDeleteAndUpdate = {
|
||||
flatViewFiltersToDelete: FlatViewFilter[];
|
||||
flatViewFiltersToUpdate: FlatViewFilter[];
|
||||
};
|
||||
export const recomputeViewFiltersOnFlatFieldMetadataOptionsUpdate = ({
|
||||
flatViewFilterMaps,
|
||||
fromFlatFieldMetadata,
|
||||
update,
|
||||
}: RecomputeViewFiltersOnFlatFieldMetadataOptionsUpdateArgs): FlatViewFiltersToDeleteAndUpdate => {
|
||||
const flatViewFiltersToCreateAndUpdate: FlatViewFiltersToDeleteAndUpdate = {
|
||||
flatViewFiltersToDelete: [],
|
||||
flatViewFiltersToUpdate: [],
|
||||
};
|
||||
|
||||
const {
|
||||
deleted: deletedFieldMetadataOptions,
|
||||
updated: updatedFieldMetadataOptions,
|
||||
} = compareTwoFlatFieldMetadataEnumOptions({
|
||||
compareLabel: false,
|
||||
fromOptions: fromFlatFieldMetadata.options,
|
||||
toOptions: update.to,
|
||||
});
|
||||
|
||||
if (
|
||||
deletedFieldMetadataOptions.length === 0 &&
|
||||
updatedFieldMetadataOptions.length === 0
|
||||
) {
|
||||
return flatViewFiltersToCreateAndUpdate;
|
||||
}
|
||||
|
||||
const flatViewFilters = findManyFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityIds: fromFlatFieldMetadata.viewFilterIds,
|
||||
flatEntityMaps: flatViewFilterMaps,
|
||||
});
|
||||
|
||||
for (const viewFilter of flatViewFilters) {
|
||||
const rawViewFilterValue = viewFilter.value;
|
||||
|
||||
if (!isDefined(rawViewFilterValue)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO: all view filter value should be stored as JSON, this is ongoing work (we are missing a command to migrate the data)
|
||||
const viewFilterValue = isNonEmptyString(rawViewFilterValue)
|
||||
? parseJson(rawViewFilterValue)
|
||||
: rawViewFilterValue;
|
||||
|
||||
if (!isDefined(viewFilterValue) || !Array.isArray(viewFilterValue)) {
|
||||
throw new FieldMetadataException(
|
||||
`Unexpected invalid view filter value for filter ${viewFilter.id}`,
|
||||
FieldMetadataExceptionCode.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
const viewFilterOptions = viewFilterValue
|
||||
.flatMap((value) => {
|
||||
if (!isDefined(fromFlatFieldMetadata.options)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return fromFlatFieldMetadata.options.find(
|
||||
(option) => option.value === value,
|
||||
);
|
||||
})
|
||||
.filter(isDefined);
|
||||
|
||||
const afterDeleteViewFilterOptions = viewFilterOptions.filter(
|
||||
(viewFilterOption) =>
|
||||
!deletedFieldMetadataOptions.some(
|
||||
(option) => option.value === viewFilterOption.value,
|
||||
),
|
||||
);
|
||||
|
||||
if (afterDeleteViewFilterOptions.length === 0) {
|
||||
flatViewFiltersToCreateAndUpdate.flatViewFiltersToDelete.push(viewFilter);
|
||||
continue;
|
||||
}
|
||||
|
||||
const afterUpdateAndDeleteViewFilterOptions =
|
||||
afterDeleteViewFilterOptions.map((viewFilterOption) => {
|
||||
const updatedOption = updatedFieldMetadataOptions.find(
|
||||
({ from }) => viewFilterOption.value === from.value,
|
||||
);
|
||||
|
||||
return isDefined(updatedOption) ? updatedOption.to : viewFilterOption;
|
||||
});
|
||||
|
||||
const optionsValues = afterUpdateAndDeleteViewFilterOptions.map(
|
||||
(option) => option.value,
|
||||
);
|
||||
|
||||
flatViewFiltersToCreateAndUpdate.flatViewFiltersToUpdate.push({
|
||||
...viewFilter,
|
||||
value: optionsValues,
|
||||
});
|
||||
}
|
||||
|
||||
return flatViewFiltersToCreateAndUpdate;
|
||||
};
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
import { type FromTo } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
|
||||
import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { type FlatViewGroup } from 'src/engine/metadata-modules/flat-view-group/types/flat-view-group.type';
|
||||
import { reduceFlatViewGroupsByViewId } from 'src/engine/metadata-modules/flat-view-group/utils/reduce-flat-view-groups-by-view-id.util';
|
||||
|
||||
type RecomputeViewGroupsOnEnumFlatFieldMetadataIsNullableUpdateArgs = FromTo<
|
||||
FlatFieldMetadata,
|
||||
'flatFieldMetadata'
|
||||
> &
|
||||
Pick<AllFlatEntityMaps, 'flatViewGroupMaps'>;
|
||||
|
||||
type EnumFieldMetadataIsNullableUpdateSideEffect = {
|
||||
flatViewGroupsToDelete: FlatViewGroup[];
|
||||
flatViewGroupsToCreate: FlatViewGroup[];
|
||||
};
|
||||
const EMPTY_ENUM_FIELD_METADATA_IS_NULLABLE_UPDATE_SIDE_EFFECT_RESULT: EnumFieldMetadataIsNullableUpdateSideEffect =
|
||||
{
|
||||
flatViewGroupsToCreate: [],
|
||||
flatViewGroupsToDelete: [],
|
||||
};
|
||||
|
||||
export const recomputeViewGroupsOnEnumFlatFieldMetadataIsNullableUpdate = ({
|
||||
flatViewGroupMaps: allFlatViewGroups,
|
||||
fromFlatFieldMetadata,
|
||||
toFlatFieldMetadata,
|
||||
}: RecomputeViewGroupsOnEnumFlatFieldMetadataIsNullableUpdateArgs): {
|
||||
flatViewGroupsToDelete: FlatViewGroup[];
|
||||
flatViewGroupsToCreate: FlatViewGroup[];
|
||||
} => {
|
||||
const sideEffectResult = structuredClone(
|
||||
EMPTY_ENUM_FIELD_METADATA_IS_NULLABLE_UPDATE_SIDE_EFFECT_RESULT,
|
||||
);
|
||||
const flatViewGroups = findManyFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityIds: fromFlatFieldMetadata.viewGroupIds,
|
||||
flatEntityMaps: allFlatViewGroups,
|
||||
});
|
||||
const { flatViewGroupRecordByViewId, highestViewGroupPositionByViewId } =
|
||||
reduceFlatViewGroupsByViewId({
|
||||
flatViewGroups,
|
||||
});
|
||||
|
||||
for (const viewId in flatViewGroupRecordByViewId) {
|
||||
const flatViewGroups = Object.values(flatViewGroupRecordByViewId[viewId]);
|
||||
|
||||
const emptyValueFlatViewGroup = flatViewGroups.find(
|
||||
(flatViewGroup) => flatViewGroup.fieldValue === '',
|
||||
);
|
||||
|
||||
if (
|
||||
toFlatFieldMetadata.isNullable === true &&
|
||||
!isDefined(emptyValueFlatViewGroup)
|
||||
) {
|
||||
const highestViewGroupPosition = highestViewGroupPositionByViewId[viewId];
|
||||
const viewGroupId = v4();
|
||||
const createdAt = new Date();
|
||||
|
||||
sideEffectResult.flatViewGroupsToCreate.push({
|
||||
fieldMetadataId: toFlatFieldMetadata.id,
|
||||
id: viewGroupId,
|
||||
universalIdentifier: viewGroupId,
|
||||
fieldValue: '',
|
||||
position: highestViewGroupPosition + 1,
|
||||
isVisible: true,
|
||||
workspaceId: toFlatFieldMetadata.workspaceId,
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
deletedAt: null,
|
||||
viewId,
|
||||
});
|
||||
} else if (isDefined(emptyValueFlatViewGroup)) {
|
||||
sideEffectResult.flatViewGroupsToDelete.push(emptyValueFlatViewGroup);
|
||||
}
|
||||
}
|
||||
|
||||
return sideEffectResult;
|
||||
};
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
import { isDefined } from 'class-validator';
|
||||
import { type EnumFieldMetadataType } from 'twenty-shared/types';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import {
|
||||
FlatEntityMapsException,
|
||||
FlatEntityMapsExceptionCode,
|
||||
} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception';
|
||||
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
|
||||
import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { compareTwoFlatFieldMetadataEnumOptions } from 'src/engine/metadata-modules/flat-field-metadata/utils/compare-two-flat-field-metadata-enum-options.util';
|
||||
import { type FlatViewGroup } from 'src/engine/metadata-modules/flat-view-group/types/flat-view-group.type';
|
||||
import { reduceFlatViewGroupsByViewId } from 'src/engine/metadata-modules/flat-view-group/utils/reduce-flat-view-groups-by-view-id.util';
|
||||
import { type PropertyUpdate } from 'src/engine/workspace-manager/workspace-migration-v2/types/property-update.type';
|
||||
|
||||
type RecomputeViewGroupsOnFlatFieldMetadataOptionsUpdateArgs = {
|
||||
fromFlatFieldMetadata: FlatFieldMetadata<EnumFieldMetadataType>;
|
||||
update: PropertyUpdate<FlatFieldMetadata<EnumFieldMetadataType>, 'options'>;
|
||||
} & Pick<AllFlatEntityMaps, 'flatViewGroupMaps'>;
|
||||
|
||||
export type FlatViewGroupsToDeleteUpdateAndCreate = {
|
||||
flatViewGroupsToDelete: FlatViewGroup[];
|
||||
flatViewGroupsToUpdate: FlatViewGroup[];
|
||||
flatViewGroupsToCreate: FlatViewGroup[];
|
||||
};
|
||||
export const recomputeViewGroupsOnFlatFieldMetadataOptionsUpdate = ({
|
||||
flatViewGroupMaps,
|
||||
fromFlatFieldMetadata,
|
||||
update,
|
||||
}: RecomputeViewGroupsOnFlatFieldMetadataOptionsUpdateArgs): FlatViewGroupsToDeleteUpdateAndCreate => {
|
||||
const {
|
||||
deleted: deletedFieldMetadataOptions,
|
||||
updated: updatedFieldMetadataOptions,
|
||||
created: createdFieldMetadataOptions,
|
||||
} = compareTwoFlatFieldMetadataEnumOptions({
|
||||
compareLabel: false,
|
||||
fromOptions: fromFlatFieldMetadata.options,
|
||||
toOptions: update.to,
|
||||
});
|
||||
|
||||
const flatViewGroups = findManyFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityIds: fromFlatFieldMetadata.viewGroupIds,
|
||||
flatEntityMaps: flatViewGroupMaps,
|
||||
});
|
||||
|
||||
if (flatViewGroups.length === 0) {
|
||||
return {
|
||||
flatViewGroupsToCreate: [],
|
||||
flatViewGroupsToDelete: [],
|
||||
flatViewGroupsToUpdate: [],
|
||||
};
|
||||
}
|
||||
|
||||
const workspaceId = fromFlatFieldMetadata.workspaceId;
|
||||
|
||||
const flatViewGroupsToDelete = deletedFieldMetadataOptions.flatMap((option) =>
|
||||
flatViewGroups.filter(
|
||||
(flatViewGroup) => flatViewGroup.fieldValue === option.value,
|
||||
),
|
||||
);
|
||||
|
||||
const flatViewGroupsToUpdate = updatedFieldMetadataOptions.flatMap(
|
||||
({ from: fromOption, to: toOption }) =>
|
||||
flatViewGroups.flatMap((flatViewGroup) =>
|
||||
flatViewGroup.fieldValue === fromOption.value
|
||||
? { ...flatViewGroup, fieldValue: toOption.value }
|
||||
: [],
|
||||
),
|
||||
);
|
||||
|
||||
const viewGroupsByViewId = reduceFlatViewGroupsByViewId({
|
||||
flatViewGroups: flatViewGroups.filter(
|
||||
(flatViewGroup) =>
|
||||
!flatViewGroupsToDelete.some(
|
||||
(flatViewGroupToDelete) =>
|
||||
flatViewGroupToDelete.id === flatViewGroup.id,
|
||||
),
|
||||
),
|
||||
});
|
||||
|
||||
const viewIds = Object.keys(viewGroupsByViewId.flatViewGroupRecordByViewId);
|
||||
|
||||
const createdAt = new Date();
|
||||
const flatViewGroupsToCreate = createdFieldMetadataOptions.flatMap(
|
||||
(option, createdOptionIndex) =>
|
||||
viewIds.map<FlatViewGroup>((viewId) => {
|
||||
const viewGroupHighestPosition =
|
||||
viewGroupsByViewId.highestViewGroupPositionByViewId[viewId];
|
||||
|
||||
if (!isDefined(viewGroupHighestPosition)) {
|
||||
throw new FlatEntityMapsException(
|
||||
'View id highest position not found, should never occur',
|
||||
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const viewGroupId = v4();
|
||||
|
||||
return {
|
||||
id: viewGroupId,
|
||||
fieldMetadataId: fromFlatFieldMetadata.id,
|
||||
viewId,
|
||||
workspaceId,
|
||||
createdAt: createdAt,
|
||||
updatedAt: createdAt,
|
||||
deletedAt: null,
|
||||
universalIdentifier: viewGroupId,
|
||||
isVisible: true,
|
||||
fieldValue: option.value,
|
||||
position: viewGroupHighestPosition + createdOptionIndex + 1,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
flatViewGroupsToCreate,
|
||||
flatViewGroupsToDelete,
|
||||
flatViewGroupsToUpdate,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user