Fix view filter update and deletion propagation (#12082)

# Introduction

Diff description: ~500 tests and +500 additions

close https://github.com/twentyhq/core-team-issues/issues/731

## What has been done here
In a nutshell on a field metadata type ( `SELECT MULTI_SELECT` ) update,
we will be browsing all `ViewFilters` in a post hook searching for some
referencing related updated `fieldMetadata` select. In order to update
or delete the `viewFilter` depending on the associated mutations.

## How to test:
- Add FieldMetadata `SELECT | MULTI_SELECT` to an existing or a new
`objectMetadata`
- Create a filtered view on created `fieldMetadata` with any options you
would like
- Remove some options ( in the best of the world some that are selected
by the filter ) from the `fieldMetadata` settings page
- Go back to the filtered view, removed or updated options should have
been hydrated in the `displayValue` and the filtered data should make
sense

## All filtered options are deleted edge case
If an update implies that a viewFilter does not have any existing
related options anymore, then we remove the viewFilter

## Testing
```sh 
PASS  test/integration/metadata/suites/field-metadata/update-one-field-metadata-related-record.integration-spec.ts (27 s)
  update-one-field-metadata-related-record
    SELECT
      ✓ should delete related view filter if all select field options got deleted (2799 ms)
      ✓ should update related multi selected options view filter (1244 ms)
      ✓ should update related solo selected option view filter (1235 ms)
      ✓ should handle partial deletion of selected options in view filter (1210 ms)
      ✓ should handle reordering of options while maintaining view filter values (1487 ms)
      ✓ should handle no changes update of options while maintaining existing view filter values (1174 ms)
      ✓ should handle adding new options while maintaining existing view filter (1174 ms)
      ✓ should update display value with options label if less than 3 options are selected (1249 ms)
      ✓ should throw error if view filter value is not a stringified JSON array (1300 ms)
    MULTI_SELECT
      ✓ should delete related view filter if all select field options got deleted (1127 ms)
      ✓ should update related multi selected options view filter (1215 ms)
      ✓ should update related solo selected option view filter (1404 ms)
      ✓ should handle partial deletion of selected options in view filter (1936 ms)
      ✓ should handle reordering of options while maintaining view filter values (1261 ms)
      ✓ should handle no changes update of options while maintaining existing view filter values (1831 ms)
      ✓ should handle adding new options while maintaining existing view filter (1610 ms)
      ✓ should update display value with options label if less than 3 options are selected (1889 ms)
      ✓ should throw error if view filter value is not a stringified JSON array (1365 ms)

Test Suites: 1 passed, 1 total
Tests:       18 passed, 18 total
Snapshots:   18 passed, 18 total
Time:        27.039 s
```
## Out of scope
- We should handle ViewFilter validation when extracting its definition
from the metadata
https://github.com/twentyhq/core-team-issues/issues/1009

## Concerns
- Are we able through the api to update an RATING fieldMetadata ? ( if
yes than that's an issue and we should handle RATING the same way than
for SELECT and MULTI_SELECT )
- It's not possible to group a view from a MULTI_SELECT field

The above points create a double nor a triple "lecture" to the post hook
effect:
- ViewGroup -> only SELECT
- VIewFilter -> only SELECT || MULTI_SELECT
- Rating nothing
I think we should determine the scope of all of that

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Paul Rastoin
2025-05-28 12:22:28 +02:00
committed by GitHub
parent e63cd39a5b
commit 97d4ec96af
31 changed files with 1250 additions and 93 deletions

View File

@ -13,13 +13,13 @@ export const createOneOperationFactory = ({
data = {},
}: CreateOneOperationFactoryParams) => ({
query: gql`
mutation Create${capitalize(objectMetadataSingularName)}($data: ${capitalize(objectMetadataSingularName)}CreateInput) {
create${capitalize(objectMetadataSingularName)}(data: $data) {
mutation CreateOne${capitalize(objectMetadataSingularName)}($input: ${capitalize(objectMetadataSingularName)}CreateInput!) {
create${capitalize(objectMetadataSingularName)}(data: $input) {
${gqlFields}
}
}
`,
variables: {
data,
input: data,
},
});

View File

@ -0,0 +1,43 @@
import { createOneOperationFactory } from 'test/integration/graphql/utils/create-one-operation-factory.util';
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
import { CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
import { PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
import { capitalize } from 'twenty-shared/utils';
import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
type CreateOneOperationArgs<T> = PerformMetadataQueryParams<T> & {
objectMetadataSingularName: string;
};
export const createOneOperation = async <T = object>({
input,
gqlFields = 'id',
objectMetadataSingularName,
expectToFail = false,
}: CreateOneOperationArgs<T>): CommonResponseBody<{
createOneResponse: ObjectRecord;
}> => {
const graphqlOperation = createOneOperationFactory({
data: input as object, // TODO default generic does not work
objectMetadataSingularName,
gqlFields,
});
const response = await makeGraphqlAPIRequest(graphqlOperation);
if (expectToFail) {
warnIfNoErrorButExpectedToFail({
response,
errorMessage: 'Create one operation should have failed but did not',
});
}
return {
data: {
createOneResponse:
response.body.data[`create${capitalize(objectMetadataSingularName)}`],
},
errors: response.body.errors,
};
};

View File

@ -4,7 +4,7 @@ import { capitalize } from 'twenty-shared/utils';
type FindOneOperationFactoryParams = {
objectMetadataSingularName: string;
gqlFields: string;
filter?: object;
filter?: unknown;
};
export const findOneOperationFactory = ({
@ -13,7 +13,7 @@ export const findOneOperationFactory = ({
filter = {},
}: FindOneOperationFactoryParams) => ({
query: gql`
query ${capitalize(objectMetadataSingularName)}($filter: ${capitalize(objectMetadataSingularName)}FilterInput) {
query FindOne${capitalize(objectMetadataSingularName)}($filter: ${capitalize(objectMetadataSingularName)}FilterInput!) {
${objectMetadataSingularName}(filter: $filter) {
${gqlFields}
}

View File

@ -0,0 +1,40 @@
import { findOneOperationFactory } from 'test/integration/graphql/utils/find-one-operation-factory.util';
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
import { CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
type FindOneOperationArgs = Parameters<typeof findOneOperationFactory>[0] & {
expectToFail?: boolean;
};
export const findOneOperation = async ({
gqlFields = 'id',
objectMetadataSingularName,
expectToFail = false,
filter,
}: FindOneOperationArgs): CommonResponseBody<{
findResponse: ObjectRecord;
}> => {
const graphqlOperation = findOneOperationFactory({
objectMetadataSingularName,
gqlFields,
filter,
});
const response = await makeGraphqlAPIRequest(graphqlOperation);
if (expectToFail) {
warnIfNoErrorButExpectedToFail({
response,
errorMessage: 'Find one operation should have failed but did not',
});
}
return {
data: {
findResponse: response.body.data[objectMetadataSingularName],
},
errors: response.body.errors,
};
};

View File

@ -0,0 +1,175 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`update-one-field-metadata-related-record MULTI_SELECT should delete related view filter if all select field options got deleted 1`] = `
[
{
"extensions": {
"code": "NOT_FOUND",
},
"message": "Record not found",
},
]
`;
exports[`update-one-field-metadata-related-record MULTI_SELECT should handle adding new options while maintaining existing view filter 1`] = `
{
"displayValue": "2 options",
"id": Any<String>,
"value": "["OPTION_0","OPTION_1"]",
}
`;
exports[`update-one-field-metadata-related-record MULTI_SELECT should handle no changes update of options while maintaining existing view filter values 1`] = `
{
"displayValue": "10 options",
"id": Any<String>,
"value": "["OPTION_0","OPTION_1","OPTION_2","OPTION_3","OPTION_4","OPTION_5","OPTION_6","OPTION_7","OPTION_8","OPTION_9"]",
}
`;
exports[`update-one-field-metadata-related-record MULTI_SELECT should handle partial deletion of selected options in view filter 1`] = `
{
"displayValue": "6 options",
"id": Any<String>,
"value": "["OPTION_4","OPTION_5","OPTION_6","OPTION_7","OPTION_8","OPTION_9"]",
}
`;
exports[`update-one-field-metadata-related-record MULTI_SELECT should handle reordering of options while maintaining view filter values 1`] = `
{
"displayValue": "2 options",
"id": Any<String>,
"value": "["OPTION_0","OPTION_1"]",
}
`;
exports[`update-one-field-metadata-related-record MULTI_SELECT should throw error if view filter value is not a stringified JSON array 1`] = `
[
{
"extensions": {
"code": "INTERNAL_SERVER_ERROR",
"exceptionEventId": "mocked-exception-id",
},
"message": "Unexpected invalid view filter value for filter 20202020-e3b5-4fa7-85aa-9b1950fc7bf5",
},
]
`;
exports[`update-one-field-metadata-related-record MULTI_SELECT should update display value with options label if less than 3 options are selected 1`] = `
{
"displayValue": "Option 8, Option 9",
"id": Any<String>,
"value": "["OPTION_8","OPTION_9"]",
}
`;
exports[`update-one-field-metadata-related-record MULTI_SELECT should update related multi selected options view filter 1`] = `
{
"displayValue": "10 options",
"id": Any<String>,
"value": "["OPTION_0_UPDATED","OPTION_1","OPTION_2_UPDATED","OPTION_3","OPTION_4_UPDATED","OPTION_5","OPTION_6_UPDATED","OPTION_7","OPTION_8_UPDATED","OPTION_9"]",
}
`;
exports[`update-one-field-metadata-related-record MULTI_SELECT should update related solo selected option view filter 1`] = `
{
"displayValue": "Option 5 updated",
"id": Any<String>,
"value": "["OPTION_5_UPDATED"]",
}
`;
exports[`update-one-field-metadata-related-record MULTI_SELECT should update the display value on an option label change only 1`] = `
{
"displayValue": "Option 0 updated, Option 1 updated, Option 2 updated",
"id": Any<String>,
"value": "["OPTION_0","OPTION_1","OPTION_2"]",
}
`;
exports[`update-one-field-metadata-related-record SELECT should delete related view filter if all select field options got deleted 1`] = `
[
{
"extensions": {
"code": "NOT_FOUND",
},
"message": "Record not found",
},
]
`;
exports[`update-one-field-metadata-related-record SELECT should handle adding new options while maintaining existing view filter 1`] = `
{
"displayValue": "2 options",
"id": Any<String>,
"value": "["OPTION_0","OPTION_1"]",
}
`;
exports[`update-one-field-metadata-related-record SELECT should handle no changes update of options while maintaining existing view filter values 1`] = `
{
"displayValue": "10 options",
"id": Any<String>,
"value": "["OPTION_0","OPTION_1","OPTION_2","OPTION_3","OPTION_4","OPTION_5","OPTION_6","OPTION_7","OPTION_8","OPTION_9"]",
}
`;
exports[`update-one-field-metadata-related-record SELECT should handle partial deletion of selected options in view filter 1`] = `
{
"displayValue": "6 options",
"id": Any<String>,
"value": "["OPTION_4","OPTION_5","OPTION_6","OPTION_7","OPTION_8","OPTION_9"]",
}
`;
exports[`update-one-field-metadata-related-record SELECT should handle reordering of options while maintaining view filter values 1`] = `
{
"displayValue": "2 options",
"id": Any<String>,
"value": "["OPTION_0","OPTION_1"]",
}
`;
exports[`update-one-field-metadata-related-record SELECT should throw error if view filter value is not a stringified JSON array 1`] = `
[
{
"extensions": {
"code": "INTERNAL_SERVER_ERROR",
"exceptionEventId": "mocked-exception-id",
},
"message": "Unexpected invalid view filter value for filter 20202020-e3b5-4fa7-85aa-9b1950fc7bf5",
},
]
`;
exports[`update-one-field-metadata-related-record SELECT should update display value with options label if less than 3 options are selected 1`] = `
{
"displayValue": "Option 8, Option 9",
"id": Any<String>,
"value": "["OPTION_8","OPTION_9"]",
}
`;
exports[`update-one-field-metadata-related-record SELECT should update related multi selected options view filter 1`] = `
{
"displayValue": "10 options",
"id": Any<String>,
"value": "["OPTION_0_UPDATED","OPTION_1","OPTION_2_UPDATED","OPTION_3","OPTION_4_UPDATED","OPTION_5","OPTION_6_UPDATED","OPTION_7","OPTION_8_UPDATED","OPTION_9"]",
}
`;
exports[`update-one-field-metadata-related-record SELECT should update related solo selected option view filter 1`] = `
{
"displayValue": "Option 5 updated",
"id": Any<String>,
"value": "["OPTION_5_UPDATED"]",
}
`;
exports[`update-one-field-metadata-related-record SELECT should update the display value on an option label change only 1`] = `
{
"displayValue": "Option 0 updated, Option 1 updated, Option 2 updated",
"id": Any<String>,
"value": "["OPTION_0","OPTION_1","OPTION_2"]",
}
`;

View File

@ -7,12 +7,15 @@ import {
LISTING_NAME_PLURAL,
LISTING_NAME_SINGULAR,
} from 'test/integration/metadata/suites/object-metadata/constants/test-object-names.constant';
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 { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
import { FieldMetadataComplexOption } from 'src/engine/metadata-modules/field-metadata/dtos/options.input';
import {
FieldMetadataComplexOption,
FieldMetadataDefaultOption,
} from 'src/engine/metadata-modules/field-metadata/dtos/options.input';
const { failingTestCases, successfulTestCases } =
UPDATE_CREATE_ONE_FIELD_METADATA_SELECT_TEST_CASES;
@ -62,8 +65,9 @@ describe('Field metadata select creation tests group', () => {
expect(data).not.toBeNull();
expect(data.createOneField).toBeDefined();
const createdOptions: FieldMetadataComplexOption[] =
data.createOneField.options;
const createdOptions:
| FieldMetadataDefaultOption[]
| FieldMetadataComplexOption[] = data.createOneField.options;
const optionsToCompare = expectedOptions ?? input.options;

View File

@ -0,0 +1,381 @@
import { faker } from '@faker-js/faker';
import { isDefined } from 'class-validator';
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 { getMockCreateObjectInput } from 'test/integration/metadata/suites/object-metadata/utils/generate-mock-create-object-metadata-input';
import { EachTestingContext } from 'twenty-shared/testing';
import { FieldMetadataType } from 'twenty-shared/types';
import { parseJson } from 'twenty-shared/utils';
import {
FieldMetadataComplexOption,
FieldMetadataDefaultOption,
} from 'src/engine/metadata-modules/field-metadata/dtos/options.input';
import { EnumFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/enum-column-action.factory';
type Option = FieldMetadataDefaultOption | FieldMetadataComplexOption;
const generateOption = (index: number): Option => ({
label: `Option ${index}`,
value: `OPTION_${index}`,
color: 'green',
position: index,
});
const generateOptions = (length: number) =>
Array.from({ length }, (_value, index) => generateOption(index));
const updateOption = ({ value, label, ...option }: Option) => ({
...option,
value: `${value}_UPDATED`,
label: `${label} updated`,
});
const ALL_OPTIONS = generateOptions(10);
const isEven = (_value: unknown, index: number) => index % 2 === 0;
type ViewFilterUpdate = {
displayValue: string;
value: string[];
};
type FieldMetadataOptionsAndType = {
options: Option[];
type: EnumFieldMetadataType;
};
type TestCase = EachTestingContext<{
fieldMetadata?: FieldMetadataOptionsAndType;
createViewFilter?: ViewFilterUpdate;
updateOptions: (
options: FieldMetadataDefaultOption[] | FieldMetadataComplexOption[],
) => FieldMetadataDefaultOption[] | FieldMetadataComplexOption[];
expected?: null;
}>;
const testFieldMetadataType: EnumFieldMetadataType[] = [
FieldMetadataType.SELECT,
FieldMetadataType.MULTI_SELECT,
];
describe('update-one-field-metadata-related-record', () => {
let idToDelete: string;
const createObjectSelectFieldAndView = async ({
options,
type: fieldMetadataType,
}: FieldMetadataOptionsAndType) => {
const singular = faker.lorem.words();
const plural = singular + faker.lorem.word();
const {
data: { createOneObject },
} = await createOneObjectMetadata({
input: getMockCreateObjectInput({
labelSingular: singular,
labelPlural: plural,
nameSingular: singular.split(' ').join(''),
namePlural: plural.split(' ').join(''),
isLabelSyncedWithName: false,
}),
});
idToDelete = createOneObject.id;
const {
data: { createOneField },
} = await createOneFieldMetadata({
input: {
objectMetadataId: createOneObject.id,
type: fieldMetadataType,
name: 'testName',
label: 'Test name',
isLabelSyncedWithName: true,
options,
},
gqlFields: `
id
options
`,
});
const {
data: { createOneResponse: createOneView },
} = await createOneOperation<{
id: string;
objectMetadataId: string;
type: string;
}>({
objectMetadataSingularName: 'view',
input: {
id: faker.string.uuid(),
objectMetadataId: createOneObject.id,
type: 'table',
},
});
return { createOneObject, createOneField, createOneView };
};
afterEach(async () => {
if (isDefined(idToDelete)) {
await deleteOneObjectMetadata({
input: { idToDelete: idToDelete },
});
}
});
describe.each(testFieldMetadataType)('%s', (fieldType) => {
const testCases: TestCase[] = [
{
title:
'should delete related view filter if all select field options got deleted',
context: {
updateOptions: () => generateOptions(3),
expected: null,
},
},
{
title: 'should update related multi selected options view filter',
context: {
updateOptions: (options) =>
options.map((option, index) =>
isEven(option, index) ? updateOption(option) : option,
),
},
},
{
title: 'should update related solo selected option view filter',
context: {
createViewFilter: {
displayValue: ALL_OPTIONS[5].label,
value: [ALL_OPTIONS[5].value],
},
updateOptions: (options) => [updateOption(options[5])],
},
},
{
title:
'should handle partial deletion of selected options in view filter',
context: {
updateOptions: (options) => options.slice(4),
},
},
{
title:
'should handle reordering of options while maintaining view filter values',
context: {
createViewFilter: {
displayValue: '2 options',
value: ALL_OPTIONS.slice(0, 2).map((option) => option.value),
},
updateOptions: (options) => [...options].reverse(),
},
},
{
title:
'should handle no changes update of options while maintaining existing view filter values',
context: {
updateOptions: (options) => options,
},
},
{
title:
'should handle adding new options while maintaining existing view filter',
context: {
fieldMetadata: {
options: ALL_OPTIONS.slice(0, 5),
type: fieldType,
},
createViewFilter: {
displayValue: '2 options',
value: ALL_OPTIONS.slice(0, 2).map((option) => option.value),
},
updateOptions: (options) => [
...options,
...generateOptions(6).slice(5),
],
},
},
{
title:
'should update display value with options label if less than 3 options are selected',
context: {
updateOptions: (options) => options.slice(8),
},
},
{
title: 'should update the display value on an option label change only',
context: {
createViewFilter: {
displayValue: 'Option 3',
value: ALL_OPTIONS.slice(0, 3).map((option) => option.value),
},
updateOptions: (options) =>
options.map((option) => ({
...option,
label: `${option.label} updated`,
})),
},
},
];
test.each(testCases)(
'$title',
async ({
context: {
expected,
createViewFilter = {
displayValue: '10 options',
value: ALL_OPTIONS.map((option) => option.value),
},
fieldMetadata = { options: ALL_OPTIONS, type: fieldType },
updateOptions,
},
}) => {
const { createOneField, createOneView } =
await createObjectSelectFieldAndView(fieldMetadata);
const {
data: { createOneResponse: createOneViewFilter },
} = await createOneOperation<{
id: string;
viewId: string;
fieldMetadataId: string;
operand: string;
value: string;
displayValue: string;
}>({
objectMetadataSingularName: 'viewFilter',
input: {
id: faker.string.uuid(),
viewId: createOneView.id,
fieldMetadataId: createOneField.id,
operand: 'is',
value: JSON.stringify(createViewFilter.value),
displayValue: createViewFilter.displayValue,
},
});
const optionsWithIds = createOneField.options;
const updatedOptions = updateOptions(optionsWithIds);
await updateOneFieldMetadata({
input: {
idToUpdate: createOneField.id,
updatePayload: {
options: updatedOptions,
},
},
gqlFields: `
id
options
`,
});
const {
data: { findResponse },
errors,
} = await findOneOperation({
gqlFields: `
id
displayValue
value
`,
objectMetadataSingularName: 'viewFilter',
filter: {
id: { eq: createOneViewFilter.id },
},
});
if (expected !== undefined) {
expect(findResponse).toBe(expected);
expect(errors).toMatchSnapshot();
return;
}
const parsedViewFilterValues = parseJson<string[]>(findResponse.value);
expect(parsedViewFilterValues).not.toBeNull();
if (parsedViewFilterValues === null) {
throw new Error('Invariant parsedValue should not be null');
}
expect(updatedOptions.map((option) => option.value)).toEqual(
expect.arrayContaining(parsedViewFilterValues),
);
expect(findResponse).toMatchSnapshot({
id: expect.any(String),
});
},
);
// Note these test exists only because we do not validate the view filter value on creation/update
// Should be removed after https://github.com/twentyhq/core-team-issues/issues/1009 completion
const failingTestCases: EachTestingContext<{
createViewFilterValue: unknown;
}>[] = [
{
title:
'should throw error if view filter value is not a stringified JSON array',
context: {
createViewFilterValue: JSON.stringify(
'not an array stringified json',
),
},
},
];
test.each(failingTestCases)(
'$title',
async ({ context: { createViewFilterValue } }) => {
const { createOneField, createOneView } =
await createObjectSelectFieldAndView({
options: ALL_OPTIONS,
type: fieldType,
});
const viewFilterId = '20202020-e3b5-4fa7-85aa-9b1950fc7bf5';
await createOneOperation<{
id: string;
viewId: string;
fieldMetadataId: string;
operand: string;
value: string;
displayValue: string;
}>({
objectMetadataSingularName: 'viewFilter',
input: {
id: viewFilterId,
viewId: createOneView.id,
fieldMetadataId: createOneField.id,
operand: 'is',
value: createViewFilterValue as unknown as string,
displayValue: '10 options',
},
});
const optionsWithIds = createOneField.options;
const updatePayload = {
options: optionsWithIds.map((option) => updateOption(option)),
};
const { errors, data } = await updateOneFieldMetadata({
input: {
idToUpdate: createOneField.id,
updatePayload,
},
gqlFields: `
id
options
`,
});
expect(data).toBeNull();
expect(errors).toBeDefined();
expect(errors).toMatchSnapshot();
},
);
});
});

View File

@ -9,12 +9,15 @@ import {
LISTING_NAME_PLURAL,
LISTING_NAME_SINGULAR,
} from 'test/integration/metadata/suites/object-metadata/constants/test-object-names.constant';
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 { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
import { FieldMetadataComplexOption } from 'src/engine/metadata-modules/field-metadata/dtos/options.input';
import {
FieldMetadataComplexOption,
FieldMetadataDefaultOption,
} from 'src/engine/metadata-modules/field-metadata/dtos/options.input';
const { failingTestCases, successfulTestCases } =
UPDATE_CREATE_ONE_FIELD_METADATA_SELECT_TEST_CASES;
@ -146,8 +149,9 @@ describe('Field metadata select update tests group', () => {
});
expect(data.updateOneField).toBeDefined();
const updatedOptions: FieldMetadataComplexOption[] =
data.updateOneField.options;
const updatedOptions:
| FieldMetadataComplexOption[]
| FieldMetadataDefaultOption[] = data.updateOneField.options;
expect(errors).toBeUndefined();
updatedOptions.forEach((option) => expect(option.id).toBeDefined());

View File

@ -3,14 +3,19 @@ import {
createOneFieldMetadataQueryFactory,
} from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata-query-factory.util';
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
import { CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
import { PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
export const createOneFieldMetadata = async ({
input,
gqlFields,
expectToFail = false,
}: PerformMetadataQueryParams<CreateOneFieldFactoryInput>) => {
}: PerformMetadataQueryParams<CreateOneFieldFactoryInput>): CommonResponseBody<{
createOneField: FieldMetadataEntity;
}> => {
const graphqlOperation = createOneFieldMetadataQueryFactory({
input,
gqlFields,

View File

@ -3,14 +3,19 @@ import {
UpdateOneFieldFactoryInput,
updateOneFieldMetadataQueryFactory,
} from 'test/integration/metadata/suites/field-metadata/utils/update-one-field-metadata-query-factory.util';
import { CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
import { PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
export const updateOneFieldMetadata = async ({
input,
gqlFields,
expectToFail = false,
}: PerformMetadataQueryParams<UpdateOneFieldFactoryInput>) => {
}: PerformMetadataQueryParams<UpdateOneFieldFactoryInput>): CommonResponseBody<{
updateOneField: FieldMetadataEntity;
}> => {
const graphqlOperation = updateOneFieldMetadataQueryFactory({
input,
gqlFields,

View File

@ -3,14 +3,19 @@ import {
createOneObjectMetadataQueryFactory,
} from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata-query-factory.util';
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
import { CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
import { PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
export const createOneObjectMetadata = async ({
input,
gqlFields,
expectToFail = false,
}: PerformMetadataQueryParams<CreateOneObjectFactoryInput>) => {
}: PerformMetadataQueryParams<CreateOneObjectFactoryInput>): CommonResponseBody<{
createOneObject: ObjectMetadataEntity; // not accurate
}> => {
const graphqlOperation = createOneObjectMetadataQueryFactory({
input,
gqlFields,

View File

@ -0,0 +1,6 @@
import { BaseGraphQLError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
export type CommonResponseBody<T> = Promise<{
data: T;
errors: BaseGraphQLError[];
}>;