Workspace migration v2 builder INDEX (#13100)

# Introduction
- Added `INDEX` action generation
- Refactored the naming, mainly `WorkspaceMigrationV2ObjectInput` ->
`FlattenObjectMetadata` and same for field. The transpilation will be
done above, agnostically of the workspace migration

Still need to:
- Create testing toolbox and follow each testing pattern for each
actions and make a complex one ( remove static current tests )
- Handle standard and custom edges cases

Notes:
`workspace-migration-v2/types` and `workspace-migration-v2/utils` could
be located outside of this folder, my hunch is that we will move them
once we work on flatten tranpilers
This commit is contained in:
Paul Rastoin
2025-07-09 16:58:17 +02:00
committed by GitHub
parent 867619247f
commit 18792f9f74
30 changed files with 1086 additions and 371 deletions
@@ -0,0 +1,56 @@
import { FromTo } from 'src/engine/workspace-manager/workspace-migration-v2/types/from-to.type';
export type DeletedCreatedUpdatedMatrix<T> = {
created: T[];
deleted: T[];
updated: FromTo<T>[];
};
export type CustomDeletedCreatedUpdatedMatrix<TLabel extends string, TInput> = {
[P in keyof DeletedCreatedUpdatedMatrix<TInput> as `${P}${Capitalize<TLabel>}`]: DeletedCreatedUpdatedMatrix<TInput>[P];
};
export type UniqueIdentifierItem = {
uniqueIdentifier: string;
};
export const deletedCreatedUpdatedMatrixDispatcher = <
T extends UniqueIdentifierItem,
>({
from,
to,
}: FromTo<T[]>): DeletedCreatedUpdatedMatrix<T> => {
const initialDispatcher: DeletedCreatedUpdatedMatrix<T> = {
created: [],
updated: [],
deleted: [],
};
const fromMap = new Map(from.map((obj) => [obj.uniqueIdentifier, obj]));
const toMap = new Map(to.map((obj) => [obj.uniqueIdentifier, obj]));
for (const [identifier, fromObj] of fromMap) {
if (!toMap.has(identifier)) {
initialDispatcher.deleted.push(fromObj);
}
}
for (const [identifier, toObj] of toMap) {
if (!fromMap.has(identifier)) {
initialDispatcher.created.push(toObj);
}
}
for (const [identifier, fromObj] of fromMap) {
const toObj = toMap.get(identifier);
if (toObj) {
initialDispatcher.updated.push({
from: fromObj,
to: toObj,
});
}
}
return initialDispatcher;
};
@@ -0,0 +1,104 @@
import diff from 'microdiff';
import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { FlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration-v2/types/flat-field-metadata';
import { FromTo } from 'src/engine/workspace-manager/workspace-migration-v2/types/from-to.type';
import { UpdateFieldAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-field-action-v2';
import { transformMetadataForComparison } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/utils/transform-metadata-for-comparison.util';
const flatFieldMetadataPropertiesToCompare = [
'defaultValue',
'description',
'icon',
'isActive',
'isLabelSyncedWithName',
'isUnique',
'label',
'name',
'options',
'standardOverrides',
] as const satisfies (keyof FlatFieldMetadata)[];
export type FlatFieldMetadataPropertiesToCompare =
(typeof flatFieldMetadataPropertiesToCompare)[number];
const fieldMetadataPropertiesToStringify = [
'defaultValue',
'standardOverrides',
] as const satisfies FlatFieldMetadataPropertiesToCompare[];
const shouldNotOverrideDefaultValue = (type: FieldMetadataType) => {
return [
FieldMetadataType.BOOLEAN,
FieldMetadataType.SELECT,
FieldMetadataType.MULTI_SELECT,
FieldMetadataType.CURRENCY,
FieldMetadataType.PHONES,
FieldMetadataType.ADDRESS,
].includes(type);
};
type GetWorkspaceMigrationUpdateFieldActionArgs = FromTo<FlatFieldMetadata>;
export const compareTwoFlatFieldMetadata = ({
from,
to,
}: GetWorkspaceMigrationUpdateFieldActionArgs) => {
const compareFieldMetadataOptions = {
shouldIgnoreProperty: (
property: string,
fieldMetadata: FlatFieldMetadata,
) => {
if (
!flatFieldMetadataPropertiesToCompare.includes(
property as FlatFieldMetadataPropertiesToCompare,
)
) {
return true;
}
if (
property === 'defaultValue' &&
isDefined(fieldMetadata.type) &&
shouldNotOverrideDefaultValue(fieldMetadata.type)
) {
return true;
}
return false;
},
propertiesToStringify: fieldMetadataPropertiesToStringify,
};
const fromCompare = transformMetadataForComparison(
from,
compareFieldMetadataOptions,
);
const toCompare = transformMetadataForComparison(
to,
compareFieldMetadataOptions,
);
const flatFieldMetadataDifferences = diff(fromCompare, toCompare);
return flatFieldMetadataDifferences.flatMap<
UpdateFieldAction['updates'][number]
>((difference) => {
switch (difference.type) {
case 'CHANGE': {
const { oldValue, path, value } = difference;
return {
from: oldValue,
to: value,
property: path[0] as FlatFieldMetadataPropertiesToCompare,
};
}
case 'CREATE':
case 'REMOVE':
default: {
// Should never occurs, we should only provide null never undefined and so on
return [];
}
}
});
};
@@ -0,0 +1,60 @@
import diff from 'microdiff';
import { FlatIndexMetadata } from 'src/engine/workspace-manager/workspace-migration-v2/types/flat-index-metadata';
import { FromTo } from 'src/engine/workspace-manager/workspace-migration-v2/types/from-to.type';
import { transformMetadataForComparison } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/utils/transform-metadata-for-comparison.util';
const flatIndexMetadataPropertiesToCompare = [
'flatIndexFieldMetadatas', // Comparing this as whole ? should iterate on each keys ? => TBD should only map over cols as before ?
'indexType',
'indexWhereClause',
'isUnique',
'name',
] as const satisfies (keyof FlatIndexMetadata)[];
type FlatIndexMetadataPropertiesToCompare =
(typeof flatIndexMetadataPropertiesToCompare)[number];
// Should also handle indexFieldMetadata comparison ?
export const compareTwoFlatIndexMetadata = ({
from,
to,
}: FromTo<FlatIndexMetadata>) => {
const transformOptions = {
shouldIgnoreProperty: (property: string) =>
!flatIndexMetadataPropertiesToCompare.includes(
property as FlatIndexMetadataPropertiesToCompare,
),
};
const fromCompare = transformMetadataForComparison(from, transformOptions);
const toCompare = transformMetadataForComparison(to, transformOptions);
const flatIndexeDifferences = diff(fromCompare, toCompare);
return flatIndexeDifferences.flatMap<{ property: string } & FromTo<unknown>>(
(difference) => {
switch (difference.type) {
case 'CHANGE': {
const { oldValue, path, value } = difference;
const property = path[0];
if (typeof property === 'number') {
return [];
}
return {
from: oldValue,
property,
to: value,
};
}
case 'CREATE':
case 'REMOVE':
default:
return [];
}
},
);
};
@@ -0,0 +1,79 @@
import omit from 'lodash.omit';
import diff from 'microdiff';
import { assertUnreachable } from 'twenty-shared/utils';
import { FlatObjectMetadata } from 'src/engine/workspace-manager/workspace-migration-v2/types/flat-object-metadata';
import { FromTo } from 'src/engine/workspace-manager/workspace-migration-v2/types/from-to.type';
import { UpdateObjectAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-object-action-v2';
import { transformMetadataForComparison } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/utils/transform-metadata-for-comparison.util';
const flatObjectMetadataPropertiesToCompare = [
'description',
'icon',
'isActive',
'isLabelSyncedWithName',
'labelPlural',
'labelSingular',
'namePlural',
'nameSingular',
'standardOverrides', // Only if standard
] as const satisfies (keyof FlatObjectMetadata)[];
export type FlatObjectMetadataPropertiesToCompare =
(typeof flatObjectMetadataPropertiesToCompare)[number];
export const compareTwoFlatObjectMetadata = ({
from,
to,
}: FromTo<FlatObjectMetadata>) => {
const fromCompare = transformMetadataForComparison(from, {});
const toCompare = transformMetadataForComparison(to, {});
const objectMetadataDifference = diff(fromCompare, omit(toCompare, 'fields'));
return objectMetadataDifference.flatMap<
UpdateObjectAction['updates'][number]
>((difference) => {
switch (difference.type) {
case 'CHANGE': {
if (
difference.oldValue === null &&
(difference.value === null || difference.value === undefined)
) {
return [];
}
const property = difference.path[0];
// TODO investigate why it would be a number, in case of array I guess ?
if (typeof property === 'number') {
return [];
}
// Could be handled directly from the diff we do above
if (
!flatObjectMetadataPropertiesToCompare.includes(
property as FlatObjectMetadataPropertiesToCompare,
)
) {
return [];
}
return {
property: property as FlatObjectMetadataPropertiesToCompare,
from: difference.oldValue,
to: difference.value,
};
}
case 'CREATE':
case 'REMOVE': {
// Should never occurs ? should throw ?
return [];
}
default: {
assertUnreachable(
difference,
`Unexpected difference type: ${difference['type']}`,
);
}
}
});
};