Refactor workspace migration update action (#17701)

# Introduction
Removing:
- `from` property from actions definition, as it's a legitimate source
of truth. The stored comparison might have been compromised since action
generation. If from is needed it should be computed from the optimistic
cache at runner lvl
- Removed the `FlatEntityPropertyUpdates` Array complexity in favor of

From
```ts
export type PropertyUpdate<T, P extends keyof T> = {
  property: P;
} & FromTo<T[P]>;
```

To
```ts
export type FlatEntityUpdate<T extends AllMetadataName> = Partial<
  Pick<
    MetadataFlatEntity<T>,
    Extract<FlatEntityPropertiesToCompare<T>, keyof MetadataFlatEntity<T>>
  >
>;
```

## New interactions
From
```ts
    const positionUpdate = findFlatEntityPropertyUpdate({
      flatEntityUpdates,
      property: 'position',
    });

    if (
      isDefined(positionUpdate) &&
      (!Number.isInteger(positionUpdate.to) || positionUpdate.to < 0)
    ) {


   const toFlatNavigationMenuItem = {
      ...fromFlatNavigationMenuItem,
      ...fromFlatEntityPropertiesUpdatesToPartialFlatEntity({
        updates: flatEntityUpdates,
      }),
    };
```

To
```ts
    const positionUpdate = flatEntityUpdate.position;
    if (
      isDefined(positionUpdate) &&
      (!Number.isInteger(positionUpdate) || positionUpdate < 0)
    ) {

    const toFlatNavigationMenuItem = {
      ...fromFlatNavigationMenuItem,
      ...flatEntityUpdate,
    };
```

## `SanitizeFlatEntityUpdate`
Enforcing the `flatEntityUpdate` to only contains comparable properties
per flat entity by striping out all unexpected keys
In the future we will also move the whole validation at runner lvl at
some point

```ts
export const sanitizeFlatEntityUpdate = <T extends AllMetadataName>({
  flatEntityUpdate,
  metadataName,
}: {
  flatEntityUpdate: FlatEntityUpdate<T>;
  metadataName: T;
}): FlatEntityUpdate<T> => {
  const { propertiesToCompare } =
    ALL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY[metadataName];

  const initialAccumulator: FlatEntityUpdate<T> = {};

  return propertiesToCompare.reduce((accumulator, property) => {
    const updatedValue =
      flatEntityUpdate[property as MetadataFlatEntityComparableProperties<T>];

    if (updatedValue === undefined) {
      return accumulator;
    }

    return {
      ...accumulator,
      [property]: updatedValue,
    };
  }, initialAccumulator);
};
```
This commit is contained in:
Paul Rastoin
2026-02-04 14:50:46 +01:00
committed by GitHub
parent b03e3772e6
commit 48c8fa6809
89 changed files with 813 additions and 800 deletions
@@ -1,4 +1,4 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`flatEntityDeletedCreatedUpdatedMatrixDispatcher It should detect a created entity 1`] = `
{
@@ -152,13 +152,9 @@ exports[`flatEntityDeletedCreatedUpdatedMatrixDispatcher It should detect an upd
"byUniversalIdentifier": {
"universal-identifier-1": {
"id": "field-id-1",
"updates": [
{
"from": false,
"property": "isActive",
"to": true,
},
],
"update": {
"isActive": true,
},
},
},
},
@@ -281,13 +277,9 @@ exports[`flatEntityDeletedCreatedUpdatedMatrixDispatcher It should detect create
"byUniversalIdentifier": {
"universal-identifier-1": {
"id": "field-id-1",
"updates": [
{
"from": true,
"property": "isActive",
"to": false,
},
],
"update": {
"isActive": false,
},
},
},
},
@@ -21,37 +21,25 @@ describe('aggregateOrchestratorActionsReportDeprioritizeSearchVectorUpdateFieldA
type: 'update',
metadataName: 'fieldMetadata',
universalIdentifier: 'search-vector-field-1',
updates: [
{
property: 'label',
from: 'Search Vector',
to: 'Updated Search Vector',
},
],
update: {
label: 'Updated Search Vector',
},
} satisfies UniversalUpdateFieldAction,
{
type: 'update',
metadataName: 'fieldMetadata',
universalIdentifier: 'regular-field-1',
updates: [
{
property: 'label',
from: 'First Name',
to: 'Updated First Name',
},
],
update: {
label: 'Updated First Name',
},
} satisfies UniversalUpdateFieldAction,
{
type: 'update',
metadataName: 'fieldMetadata',
universalIdentifier: 'regular-field-2',
updates: [
{
property: 'label',
from: 'Last Name',
to: 'Updated Last Name',
},
],
update: {
label: 'Updated Last Name',
},
} satisfies UniversalUpdateFieldAction,
],
delete: [],
@@ -1,24 +0,0 @@
import { type AllMetadataName } from 'twenty-shared/metadata';
import { type FlatEntityPropertiesToCompare } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-properties-to-compare.type';
import { type FlatEntityPropertiesUpdates } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-properties-updates.type';
import { type FlatEntityPropertyUpdate } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-property-update.type';
import { type MetadataFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity.type';
import { isPropertyUpdate } from 'src/engine/workspace-manager/workspace-migration/utils/is-property-update.util';
export const findFlatEntityPropertyUpdate = <
T extends AllMetadataName,
P extends Extract<
FlatEntityPropertiesToCompare<T>,
keyof MetadataFlatEntity<T>
>,
>({
property,
flatEntityUpdates,
}: {
flatEntityUpdates: FlatEntityPropertiesUpdates<T>;
property: P;
}) =>
flatEntityUpdates.find((update): update is FlatEntityPropertyUpdate<T, P> =>
isPropertyUpdate(update, property),
);
@@ -6,7 +6,7 @@ import { ALL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY } from 'src/engine/
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
import { type FlatEntityPropertiesToCompare } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-properties-to-compare.type';
import { type FlatEntityPropertiesToStringify } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-properties-to-stringify.type';
import { type FlatEntityPropertiesUpdates } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-properties-updates.type';
import { type FlatEntityUpdate } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-properties-updates.type';
import { type MetadataFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity-maps.type';
import { type MetadataFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity.type';
import { compareTwoFlatEntity } from 'src/engine/metadata-modules/flat-entity/utils/compare-two-flat-entity.util';
@@ -21,7 +21,7 @@ export type DeletedCreatedUpdatedMatrix<T extends AllMetadataName> = {
byUniversalIdentifier: Record<
string,
{
updates: FlatEntityPropertiesUpdates<T>;
update: FlatEntityUpdate<T>;
// TMP remove when maps is universal based
id: string;
}
@@ -88,7 +88,7 @@ export const flatEntityDeletedCreatedUpdatedMatrixDispatcher = <
if (!isDefined(toFlatEntity)) {
continue;
}
const updates = compareTwoFlatEntity({
const update = compareTwoFlatEntity({
fromFlatEntity,
toFlatEntity,
propertiesToCompare: propertiesToCompare as unknown as Extract<
@@ -101,7 +101,7 @@ export const flatEntityDeletedCreatedUpdatedMatrixDispatcher = <
>[],
});
if (updates.length === 0) {
if (!isDefined(update)) {
continue;
}
@@ -109,7 +109,7 @@ export const flatEntityDeletedCreatedUpdatedMatrixDispatcher = <
fromFlatEntity.universalIdentifier
] = {
id: toFlatEntity.id,
updates,
update,
};
}
@@ -1,8 +0,0 @@
import { type PropertyUpdate } from 'src/engine/workspace-manager/workspace-migration/types/property-update.type';
export const isPropertyUpdate = <T, P extends keyof T>(
update: PropertyUpdate<T, keyof T>,
property: P,
): update is PropertyUpdate<T, P> => {
return update.property === property;
};