Integration testing v2 enum field types fail and success path (#14010)

# Introduction
Migrating and improving performances of field enum integrations tests
success and failing tests cases to be using the new v2 api

## Discovered issue
When deleting an object in v1 it will leave related enums until the
object is re-created
Something not done anymore within the create in v2 but in the delete
operation
We should implem an upgrade command to remove such relicas

## Bugs
- Update/create default value multi select runner wrong sql query -> FIX
- Update default value multi select regression, we should allow option
without an id to be inserted -> FIX
- default value compare dynamic json stringify convertion or not in
compare tools for object and fields
This commit is contained in:
Paul Rastoin
2025-08-28 12:23:04 +02:00
committed by GitHub
parent 533d7fe49a
commit 59d90bedaa
72 changed files with 8236 additions and 1616 deletions
@@ -36,7 +36,6 @@ import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/
import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module';
import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-v2.module';
import { ViewModule } from 'src/modules/view/view.module';
import { WorkspaceMigrationBuilderExceptionV2Interceptor } from 'src/engine/workspace-manager/workspace-migration-v2/interceptors/workspace-migration-builder-exception-v2.interceptor';
import { FieldMetadataEntity } from './field-metadata.entity';
@@ -101,10 +100,7 @@ import { FieldMetadataService } from './services/field-metadata.service';
},
delete: { disabled: true },
guards: [WorkspaceAuthGuard],
interceptors: [
WorkspaceMigrationBuilderExceptionV2Interceptor,
FieldMetadataGraphqlApiExceptionInterceptor,
],
interceptors: [FieldMetadataGraphqlApiExceptionInterceptor],
},
],
}),
@@ -93,18 +93,14 @@ export class FieldMetadataResolver {
workspaceId,
);
if (isWorkspaceMigrationV2Enabled) {
try {
try {
if (isWorkspaceMigrationV2Enabled) {
return await this.fieldMetadataServiceV2.updateOne({
updateFieldInput: { ...input.update, id: input.id },
workspaceId,
});
} catch (error) {
fieldMetadataGraphqlApiExceptionHandler(error);
}
}
try {
const updatedInput = (await this.beforeUpdateOneField.run(input, {
workspaceId,
locale: context.req.locale,
@@ -61,7 +61,5 @@ export function computeCompositeColumnName<T extends FieldMetadataType>(
);
}
return `${fieldMetadataOrFieldName.name}${pascalCase(
compositeProperty.name,
)}`;
return generateName(fieldMetadataOrFieldName.name);
}
@@ -11,8 +11,14 @@ import {
FieldMetadataExceptionCode,
} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
import { InvalidMetadataException } from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception';
import { WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2';
import { workspaceMigrationBuilderExceptionV2Formatter } from 'src/engine/workspace-manager/workspace-migration-v2/interceptors/workspace-migration-builder-exception-v2-formatter';
export const fieldMetadataGraphqlApiExceptionHandler = (error: Error) => {
if (error instanceof WorkspaceMigrationBuilderExceptionV2) {
workspaceMigrationBuilderExceptionV2Formatter(error);
}
if (error instanceof InvalidMetadataException) {
throw new UserInputError(error);
}
@@ -1,8 +1,8 @@
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
export const FLAT_FIELD_METADATA_JSONB_PROPERTIES = [
'defaultValue',
'options',
'settings',
'standardOverrides',
'defaultValue',
] as const satisfies (keyof FlatFieldMetadata)[];
@@ -92,8 +92,8 @@ export class FlatFieldMetadataValidatorService {
}
const updates = compareTwoFlatFieldMetadata({
from: existingFlatFieldMetadataToUpdate,
to: updatedFlatFieldMetadata,
fromFlatFieldMetadata: existingFlatFieldMetadataToUpdate,
toFlatFieldMetadata: updatedFlatFieldMetadata,
});
if (
@@ -1,36 +1,28 @@
import diff from 'microdiff';
import { FieldMetadataType, type FromTo } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { isDefined, parseJson } from 'twenty-shared/utils';
import { FLAT_FIELD_METADATA_JSONB_PROPERTIES } from 'src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-jsonb-properties.constant';
import { FLAT_FIELD_METADATA_PROPERTIES_TO_COMPARE } from 'src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-properties-to-compare.constant';
import { FLAT_FIELD_METADATA_RELATION_PROPERTIES_TO_COMPARE } from 'src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-relation-properties-to-compare.constant';
import { type FlatFieldMetadataEntityJsonbProperties } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-jsonb-properties.type';
import { type FlatFieldMetadataPropertiesToCompare } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-properties-to-compare.type';
import { type FlatFieldMetadataRelationPropertiesToCompare } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-relation-properties-to-compare.type';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { isFlatFieldMetadataJsonbProperty } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-jsonb-property.util';
import { isStandardMetadata } from 'src/engine/metadata-modules/utils/is-standard-metadata.util';
import { type 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 shouldNotOverrideDefaultValue = (type: FieldMetadataType) => {
return [
FieldMetadataType.BOOLEAN,
FieldMetadataType.SELECT,
FieldMetadataType.MULTI_SELECT,
FieldMetadataType.CURRENCY,
FieldMetadataType.PHONES,
FieldMetadataType.ADDRESS,
].includes(type);
};
type GetWorkspaceMigrationUpdateFieldActionArgs = FromTo<FlatFieldMetadata>;
type GetWorkspaceMigrationUpdateFieldActionArgs = FromTo<
FlatFieldMetadata,
'FlatFieldMetadata'
>;
/**
* This comparator handles update on colliding uniqueIdentifier flatFieldMetadata
*/
export const compareTwoFlatFieldMetadata = ({
from,
to,
fromFlatFieldMetadata,
toFlatFieldMetadata,
}: GetWorkspaceMigrationUpdateFieldActionArgs) => {
const transformMetadataForComparisonParameters = {
shouldIgnoreProperty: (
@@ -45,14 +37,6 @@ export const compareTwoFlatFieldMetadata = ({
return true;
}
if (
property === 'defaultValue' &&
isDefined(fieldMetadata.type) &&
shouldNotOverrideDefaultValue(fieldMetadata.type)
) {
return true;
}
// Remove below assertion when we authorize relation edition, see https://github.com/twentyhq/twenty/commit/39f6f3c4bb101272a9014e142a842d0801a3c33b
const isRelationFieldType =
isDefined(fieldMetadata.type) &&
@@ -80,11 +64,11 @@ export const compareTwoFlatFieldMetadata = ({
propertiesToStringify: FLAT_FIELD_METADATA_JSONB_PROPERTIES,
};
const fromCompare = transformMetadataForComparison(
from,
fromFlatFieldMetadata,
transformMetadataForComparisonParameters,
);
const toCompare = transformMetadataForComparison(
to,
toFlatFieldMetadata,
transformMetadataForComparisonParameters,
);
@@ -97,14 +81,15 @@ export const compareTwoFlatFieldMetadata = ({
case 'CHANGE': {
const { oldValue, path, value } = difference;
const property = path[0] as FlatFieldMetadataPropertiesToCompare;
const isJsonb = FLAT_FIELD_METADATA_JSONB_PROPERTIES.includes(
property as FlatFieldMetadataEntityJsonbProperties,
);
const isJsonb = isFlatFieldMetadataJsonbProperty({
flatFieldMetadata: toFlatFieldMetadata,
property,
});
if (isJsonb) {
return {
from: isDefined(oldValue) ? JSON.parse(oldValue) : oldValue,
to: isDefined(value) ? JSON.parse(value) : value,
from: parseJson(oldValue),
to: parseJson(value),
property,
};
}
@@ -100,6 +100,7 @@ export const fromCreateFieldInputToFlatFieldMetadatasToCreate = async ({
const options = (createFieldInput?.options ?? []).map<
FieldMetadataOptions<typeof createFieldInput.type>[number]
>((option) => ({
id: v4(),
...trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties(
option as FieldMetadataOptions<typeof createFieldInput.type>[number],
['label', 'value', 'id', 'color'],
@@ -3,6 +3,7 @@ import {
extractAndSanitizeObjectStringFields,
isDefined,
} 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';
@@ -132,14 +133,18 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({
const updatedFlatFieldMetadata = fieldMetadataEditableProperties.reduce(
(acc, property) => {
const isPropertyUpdated =
updatedEditableFieldProperties[property] !== undefined;
let newValue = updatedEditableFieldProperties[property];
if (property === 'options' && isDefined(newValue)) {
newValue = updatedEditableFieldProperties[property]?.map((option) => ({
id: v4(),
...option,
}));
}
return {
...acc,
...(isPropertyUpdated
? { [property]: updatedEditableFieldProperties[property] }
: {}),
...(newValue !== undefined ? { [property]: newValue } : {}),
};
},
relatedFlatFieldMetadata,
@@ -0,0 +1,34 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { FLAT_FIELD_METADATA_JSONB_PROPERTIES } from 'src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-jsonb-properties.constant';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { isFlatFieldMetadataEntityOfTypes } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-types.util';
export const isFlatFieldMetadataJsonbProperty = ({
flatFieldMetadata,
property,
}: {
flatFieldMetadata: FlatFieldMetadata;
property: keyof FlatFieldMetadata;
}): boolean => {
if (property === 'defaultValue') {
return isFlatFieldMetadataEntityOfTypes(flatFieldMetadata, [
FieldMetadataType.LINKS,
FieldMetadataType.PHONES,
FieldMetadataType.EMAILS,
FieldMetadataType.CURRENCY,
FieldMetadataType.FULL_NAME,
FieldMetadataType.ADDRESS,
FieldMetadataType.MULTI_SELECT,
FieldMetadataType.RAW_JSON,
FieldMetadataType.ACTOR,
FieldMetadataType.ARRAY,
]);
}
const isJsonB = FLAT_FIELD_METADATA_JSONB_PROPERTIES.includes(
property as (typeof FLAT_FIELD_METADATA_JSONB_PROPERTIES)[number],
);
return isJsonB;
};
@@ -45,12 +45,31 @@ const validateMetadataOptionId = (sanitizedId?: string) => {
});
};
const validateMetadataOptionLabel = (sanitizedLabel: string) => {
const validateMetadataOptionLabel = (
sanitizedLabel: string,
): FlatFieldMetadataValidationError[] => {
if (!isDefined(sanitizedLabel)) {
return [
{
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
message: t`Option label is required`,
userFriendlyMessage: t`Option label is required`,
},
];
}
if (!isNonEmptyString(sanitizedLabel)) {
return [
{
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
message: t`Option label must be a string of at least one character`,
userFriendlyMessage: t`Option label format not supported`,
value: sanitizedLabel,
},
];
}
const validators: FlatMetadataValidator<string>[] = [
{
validator: (label) => !isDefined(label),
message: t`Option label is required`,
},
{
validator: exceedsDatabaseIdentifierMaximumLength,
message: t`Option label exceeds 63 characters`,
@@ -75,12 +94,30 @@ const validateMetadataOptionLabel = (sanitizedLabel: string) => {
});
};
const validateMetadataOptionValue = (sanitizedValue: string) => {
const validateMetadataOptionValue = (
sanitizedValue: string,
): FlatFieldMetadataValidationError[] => {
if (!isDefined(sanitizedValue)) {
return [
{
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
message: t`Option value is required`,
userFriendlyMessage: t`Option value is required`,
},
];
}
if (!isNonEmptyString(sanitizedValue)) {
return [
{
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
message: t`Option value must be a string of at least one character`,
userFriendlyMessage: t`Option value format not supported`,
},
];
}
const validators: FlatMetadataValidator<string>[] = [
{
validator: (value) => !isDefined(value),
message: t`Option value is required`,
},
{
validator: exceedsDatabaseIdentifierMaximumLength,
message: t`Option value exceeds 63 characters`,
@@ -1,7 +1,7 @@
import omit from 'lodash.omit';
import diff from 'microdiff';
import { type FromTo } from 'twenty-shared/types';
import { assertUnreachable, isDefined } from 'twenty-shared/utils';
import { assertUnreachable, parseJson } from 'twenty-shared/utils';
import { FLAT_OBJECT_METADATA_JSONB_PROPERTIES } from 'src/engine/metadata-modules/flat-object-metadata/constants/flat-object-metadata-jsonb-properties.constant';
import { FLAT_OBJECT_METADATA_PROPERTIES_TO_COMPARE } from 'src/engine/metadata-modules/flat-object-metadata/constants/flat-object-metadata-properties-to-compare.constant';
@@ -63,8 +63,8 @@ export const compareTwoFlatObjectMetadata = ({
if (isJsonb) {
return {
from: isDefined(oldValue) ? JSON.parse(oldValue) : oldValue,
to: isDefined(value) ? JSON.parse(value) : value,
from: parseJson(oldValue),
to: parseJson(value),
property,
};
}
@@ -44,7 +44,6 @@ import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/wor
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module';
import { WorkspaceMigrationBuilderExceptionV2Interceptor } from 'src/engine/workspace-manager/workspace-migration-v2/interceptors/workspace-migration-builder-exception-v2.interceptor';
import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-v2.module';
@Module({
@@ -100,10 +99,7 @@ import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspa
update: { disabled: true },
delete: { disabled: true },
guards: [WorkspaceAuthGuard],
interceptors: [
WorkspaceMigrationBuilderExceptionV2Interceptor,
ObjectMetadataGraphqlApiExceptionInterceptor,
],
interceptors: [ObjectMetadataGraphqlApiExceptionInterceptor],
filters: [PermissionsGraphqlApiExceptionFilter],
},
],
@@ -12,8 +12,14 @@ import {
ObjectMetadataExceptionCode,
} from 'src/engine/metadata-modules/object-metadata/object-metadata.exception';
import { InvalidMetadataException } from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception';
import { WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2';
import { workspaceMigrationBuilderExceptionV2Formatter } from 'src/engine/workspace-manager/workspace-migration-v2/interceptors/workspace-migration-builder-exception-v2-formatter';
export const objectMetadataGraphqlApiExceptionHandler = (error: Error) => {
if (error instanceof WorkspaceMigrationBuilderExceptionV2) {
workspaceMigrationBuilderExceptionV2Formatter(error);
}
if (error instanceof InvalidMetadataException) {
throw new UserInputError(error);
}
@@ -8,6 +8,7 @@ import {
export const fieldMetadataTypeToColumnType = <Type extends FieldMetadataType>(
fieldMetadataType: Type,
// Should be columnType
): string => {
/**
* Composite types are not implemented here, as they are flattened by their composite definitions.