[Followup] Fix defaultValue with enum options update in migration v2 integration test + polish (#15300)

# Introduction
Followup of https://github.com/twentyhq/twenty/pull/15286

- Polish replacing sorting by conditional filter and push
- Integration test
This commit is contained in:
Paul Rastoin
2025-10-23 17:57:41 +02:00
committed by GitHub
parent 4e767799c6
commit 7edfe4bc7a
4 changed files with 278 additions and 5 deletions
@@ -156,10 +156,12 @@ export class UpdateFieldActionHandlerService extends WorkspaceMigrationRunnerAct
let wasDefaultValueHandledByEnumUpdate = false;
const sortedUpdatesWithDefaultValuesUpdateLast = [...updates].sort(
(a, b) =>
+(a.property === 'defaultValue') - +(b.property === 'defaultValue'),
);
const sortedUpdatesWithDefaultValuesUpdateLast = hasDefaultValueUpdate
? [
...updates.filter((update) => update.property !== 'defaultValue'),
defaultValueUpdate,
]
: updates;
for (const update of sortedUpdatesWithDefaultValuesUpdateLast) {
if (isPropertyUpdate(update, 'name')) {
@@ -0,0 +1,27 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`successful update default value option value and side effect on records should update existing records when default value option value is changed and not mutate when default option is changed 1`] = `
[
{
"color": "green",
"id": Any<String>,
"label": "Option 1",
"position": 0,
"value": "OPTION_1_MODIFIED",
},
{
"color": "blue",
"id": Any<String>,
"label": "Option 2",
"position": 1,
"value": "OPTION_2",
},
{
"color": "red",
"id": Any<String>,
"label": "Option 3",
"position": 2,
"value": "OPTION_3",
},
]
`;
@@ -0,0 +1,237 @@
import { createOneOperation } from 'test/integration/graphql/utils/create-one-operation.util';
import { findOneOperation } from 'test/integration/graphql/utils/find-one-operation.util';
import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util';
import { updateOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/update-one-field-metadata.util';
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any';
import { FieldMetadataType } from 'twenty-shared/types';
const TEST_OBJECT_NAME_SINGULAR = 'testSelectObject';
const TEST_OBJECT_NAME_PLURAL = 'testSelectObjects';
describe('successful update default value option value and side effect on records', () => {
let createdObjectMetadataId: string;
let createdFieldMetadataId: string;
let firstOptionId: string;
let secondOptionId: string;
let thirdOptionId: string;
beforeAll(async () => {
const { data: objectData } = await createOneObjectMetadata({
expectToFail: false,
input: {
nameSingular: TEST_OBJECT_NAME_SINGULAR,
namePlural: TEST_OBJECT_NAME_PLURAL,
labelSingular: 'Test Select Object',
labelPlural: 'Test Select Objects',
icon: 'IconTestPipe',
isLabelSyncedWithName: false,
},
});
createdObjectMetadataId = objectData.createOneObject.id;
const { data: fieldData } = await createOneFieldMetadata({
expectToFail: false,
input: {
objectMetadataId: createdObjectMetadataId,
type: FieldMetadataType.SELECT,
name: 'statusField',
label: 'Status Field',
isLabelSyncedWithName: false,
options: [
{
label: 'Option 1',
value: 'OPTION_1',
color: 'green',
position: 0,
},
{
label: 'Option 2',
value: 'OPTION_2',
color: 'blue',
position: 1,
},
{
label: 'Option 3',
value: 'OPTION_3',
color: 'red',
position: 2,
},
],
defaultValue: "'OPTION_1'",
},
gqlFields: `
id
options
defaultValue
`,
});
createdFieldMetadataId = fieldData.createOneField.id;
const options = fieldData.createOneField.options;
if (!options) {
throw new Error('Options should be defined');
}
const option1 = options.find((opt) => opt.value === 'OPTION_1');
const option2 = options.find((opt) => opt.value === 'OPTION_2');
const option3 = options.find((opt) => opt.value === 'OPTION_3');
if (!option1?.id || !option2?.id || !option3?.id) {
throw new Error('All options and their IDs should be defined');
}
firstOptionId = option1.id;
secondOptionId = option2.id;
thirdOptionId = option3.id;
});
afterAll(async () => {
await updateOneObjectMetadata({
expectToFail: false,
input: {
idToUpdate: createdObjectMetadataId,
updatePayload: {
isActive: false,
},
},
});
await deleteOneObjectMetadata({
expectToFail: false,
input: { idToDelete: createdObjectMetadataId },
});
});
it('should update existing records when default value option value is changed and not mutate when default option is changed', async () => {
const { data: createFirstRecordData } = await createOneOperation({
expectToFail: false,
objectMetadataSingularName: TEST_OBJECT_NAME_SINGULAR,
input: {},
gqlFields: `
id
statusField
`,
});
const firstRecordId = createFirstRecordData.createOneResponse.id;
expect(createFirstRecordData.createOneResponse.statusField).toBe(
'OPTION_1',
);
const { data: updateOptionValueData } = await updateOneFieldMetadata({
expectToFail: false,
input: {
idToUpdate: createdFieldMetadataId,
updatePayload: {
defaultValue: "'OPTION_1_MODIFIED'",
options: [
{
id: firstOptionId,
label: 'Option 1',
value: 'OPTION_1_MODIFIED',
color: 'green',
position: 0,
},
{
id: secondOptionId,
label: 'Option 2',
value: 'OPTION_2',
color: 'blue',
position: 1,
},
{
id: thirdOptionId,
label: 'Option 3',
value: 'OPTION_3',
color: 'red',
position: 2,
},
],
},
},
gqlFields: `
id
options
defaultValue
`,
});
expect(updateOptionValueData.updateOneField.defaultValue).toBe(
"'OPTION_1_MODIFIED'",
);
const { data: queryFirstRecordAfterOptionValueUpdate } =
await findOneOperation({
expectToFail: false,
objectMetadataSingularName: TEST_OBJECT_NAME_SINGULAR,
gqlFields: `
id
statusField
`,
filter: { id: { eq: firstRecordId } },
});
expect(
queryFirstRecordAfterOptionValueUpdate.findResponse.statusField,
).toBe('OPTION_1_MODIFIED');
const { data: updateDefaultOptionData } = await updateOneFieldMetadata({
expectToFail: false,
input: {
idToUpdate: createdFieldMetadataId,
updatePayload: {
defaultValue: "'OPTION_2'",
},
},
gqlFields: `
id
options
defaultValue
`,
});
expect(updateDefaultOptionData.updateOneField.defaultValue).toBe(
"'OPTION_2'",
);
expect(updateDefaultOptionData.updateOneField.options).toMatchSnapshot(
extractRecordIdsAndDatesAsExpectAny(
updateDefaultOptionData.updateOneField.options as any,
),
);
const { data: createSecondRecordData } = await createOneOperation({
expectToFail: false,
objectMetadataSingularName: TEST_OBJECT_NAME_SINGULAR,
input: {},
gqlFields: `
id
statusField
`,
});
expect(createSecondRecordData.createOneResponse.statusField).toBe(
'OPTION_2',
);
const { data: queryFirstRecordAfterDefaultChange } = await findOneOperation(
{
expectToFail: false,
objectMetadataSingularName: TEST_OBJECT_NAME_SINGULAR,
gqlFields: `
id
statusField
`,
filter: { id: { eq: firstRecordId } },
},
);
expect(queryFirstRecordAfterDefaultChange.findResponse.statusField).toBe(
'OPTION_1_MODIFIED',
);
});
});
@@ -1,5 +1,5 @@
import { isDefined } from 'class-validator';
import { type Response } from 'supertest';
import { isDefined } from 'twenty-shared/utils';
type WarnIfErrorButNotExpectedToFailInput = {
response: Response;
@@ -11,6 +11,13 @@ export const warnIfErrorButNotExpectedToFail = ({
errorMessage,
}: WarnIfErrorButNotExpectedToFailInput) => {
if (isDefined(response.body.errors) && response.body.errors.length > 0) {
if (
isDefined(process.env.LOG_LEVELS) &&
process.env.LOG_LEVELS.includes('debug')
) {
// eslint-disable-next-line no-console
console.log(JSON.stringify(response.body.errors, null, 2));
}
expect(response.body.errors).toEqual(errorMessage);
}
expect(response.body.data).toBeDefined();