fix(api): Allow deactivation of relation fields (#13202)
_(AI generated)_ ### Summary This PR fixes a validation bug in the GraphQL API that prevented relation fields from being programmatically deactivated. The validation was incorrectly triggering a "name cannot be changed" error even when the update payload did not include a name, making it impossible to disable the field. Issue #13200 ### Problem - The [updateOneField] mutation failed when trying to set `isActive: false` on a `RELATION` field. - The root cause was a validation check in [FieldMetadataValidationService] that compared the incoming `name` with the existing one. If the input `name` was `undefined`, the check `undefined !== existingName` would incorrectly fail. - This created a catch-22 where a field could not be deleted (because it had to be deactivated first) and could not be deactivated (due to this validation error). ### Solution - The validation logic in [field-metadata-validation.service.ts] has been updated to only check for a name change if a new name is **explicitly provided** in the input (`isDefined(fieldMetadataInput.name)`). - This change correctly enforces the rule that relation field names cannot be changed, while allowing other properties like `isActive` to be updated without issue. ### How to Test 1. Create a custom field of type `RELATION`. 2. Using the GraphQL API, call the [updateOneField] mutation with the field's ID and the payload `{ "isActive": false }`. 3. Verify that the mutation succeeds and the field is now inactive. 4. Call the [deleteOneField] mutation to delete the field. 5. Verify that the deletion is successful. ### Additional Changes _to be deleted if not necessary_ - Added a new integration test [successful-field-metadata-relation-update.integration-spec.ts] to cover this specific use case and prevent future regressions. The existing test for failing updates remains untouched and continues to pass.
This commit is contained in:
committed by
GitHub
parent
0cb8533e50
commit
ff3f3d4661
@ -195,6 +195,7 @@ export class FieldMetadataValidationService {
|
|||||||
if (
|
if (
|
||||||
isRelationField &&
|
isRelationField &&
|
||||||
isDefined(existingFieldMetadata) &&
|
isDefined(existingFieldMetadata) &&
|
||||||
|
isDefined(fieldMetadataInput.name) &&
|
||||||
fieldMetadataInput.name !== existingFieldMetadata.name
|
fieldMetadataInput.name !== existingFieldMetadata.name
|
||||||
) {
|
) {
|
||||||
throw new FieldMetadataException(
|
throw new FieldMetadataException(
|
||||||
|
|||||||
@ -0,0 +1,95 @@
|
|||||||
|
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 { FieldMetadataType } from 'twenty-shared/types';
|
||||||
|
|
||||||
|
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||||
|
|
||||||
|
const globalTestContext = {
|
||||||
|
employeeObjectId: '',
|
||||||
|
enterpriseObjectId: '',
|
||||||
|
employerFieldMetadataId: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Field metadata relation update should succeed', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
const {
|
||||||
|
data: {
|
||||||
|
createOneObject: { id: employeeObjectId },
|
||||||
|
},
|
||||||
|
} = await createOneObjectMetadata({
|
||||||
|
input: getMockCreateObjectInput({
|
||||||
|
namePlural: 'employees',
|
||||||
|
nameSingular: 'employee',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: {
|
||||||
|
createOneObject: { id: enterpriseObjectId },
|
||||||
|
},
|
||||||
|
} = await createOneObjectMetadata({
|
||||||
|
input: getMockCreateObjectInput({
|
||||||
|
namePlural: 'enterprises',
|
||||||
|
nameSingular: 'enterprise',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data } = await createOneFieldMetadata({
|
||||||
|
input: {
|
||||||
|
objectMetadataId: employeeObjectId,
|
||||||
|
name: 'employer',
|
||||||
|
label: 'Employer',
|
||||||
|
isLabelSyncedWithName: false,
|
||||||
|
type: FieldMetadataType.RELATION,
|
||||||
|
relationCreationPayload: {
|
||||||
|
targetFieldLabel: 'employees',
|
||||||
|
type: RelationType.MANY_TO_ONE,
|
||||||
|
targetObjectMetadataId: enterpriseObjectId,
|
||||||
|
targetFieldIcon: 'IconBuildingSkyscraper',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
globalTestContext.employeeObjectId = employeeObjectId;
|
||||||
|
globalTestContext.enterpriseObjectId = enterpriseObjectId;
|
||||||
|
globalTestContext.employerFieldMetadataId = data.createOneField.id;
|
||||||
|
|
||||||
|
expect(data).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
for (const objectMetadataId of [
|
||||||
|
globalTestContext.employeeObjectId,
|
||||||
|
globalTestContext.enterpriseObjectId,
|
||||||
|
]) {
|
||||||
|
await deleteOneObjectMetadata({
|
||||||
|
input: {
|
||||||
|
idToDelete: objectMetadataId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('when isActive is updated to false', async () => {
|
||||||
|
const { data, errors } = await updateOneFieldMetadata({
|
||||||
|
expectToFail: false,
|
||||||
|
input: {
|
||||||
|
idToUpdate: globalTestContext.employerFieldMetadataId,
|
||||||
|
updatePayload: {
|
||||||
|
isActive: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
gqlFields: `
|
||||||
|
id
|
||||||
|
isActive
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(errors).toBeUndefined();
|
||||||
|
expect(data).toBeDefined();
|
||||||
|
expect(data.updateOneField.isActive).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user