From 316aec3c40fecb7b6f3765c2d45639380a9caa1b Mon Sep 17 00:00:00 2001 From: neo773 <62795688+neo773@users.noreply.github.com> Date: Tue, 25 Nov 2025 14:21:48 +0530 Subject: [PATCH] fix: prevent NUMERIC field type creation via API (#16038) Block users from creating NUMERIC, POSITION, and TS_VECTOR fields via the API as these are system-only types. Users should use NUMBER instead of NUMERIC. /closes #16023 --- .../SettingsNonCompositeFieldTypeConfigs.ts | 6 - .../types/SettingsExcludedFieldType.ts | 2 +- ...t-field-metadata-type-validator.service.ts | 45 ++++- ...ne-field-metadata.integration-spec.ts.snap | 171 ++++++++++++++++++ ...ate-one-field-metadata.integration-spec.ts | 89 +++++++++ 5 files changed, 303 insertions(+), 10 deletions(-) create mode 100644 packages/twenty-server/test/integration/metadata/suites/field-metadata/__snapshots__/failing-create-one-field-metadata.integration-spec.ts.snap create mode 100644 packages/twenty-server/test/integration/metadata/suites/field-metadata/failing-create-one-field-metadata.integration-spec.ts diff --git a/packages/twenty-front/src/modules/settings/data-model/constants/SettingsNonCompositeFieldTypeConfigs.ts b/packages/twenty-front/src/modules/settings/data-model/constants/SettingsNonCompositeFieldTypeConfigs.ts index 40a84ca08b..4b889a1853 100644 --- a/packages/twenty-front/src/modules/settings/data-model/constants/SettingsNonCompositeFieldTypeConfigs.ts +++ b/packages/twenty-front/src/modules/settings/data-model/constants/SettingsNonCompositeFieldTypeConfigs.ts @@ -69,12 +69,6 @@ export const SETTINGS_NON_COMPOSITE_FIELD_TYPE_CONFIGS: SettingsNonCompositeFiel ], category: 'Basic', } as const satisfies SettingsFieldTypeConfig, - [FieldMetadataType.NUMERIC]: { - label: 'Numeric', - Icon: IllustrationIconNumbers, - exampleValues: [2000, 3000, 4000], - category: 'Basic', - } as const satisfies SettingsFieldTypeConfig, [FieldMetadataType.NUMBER]: { label: 'Number', Icon: IllustrationIconNumbers, diff --git a/packages/twenty-front/src/modules/settings/data-model/types/SettingsExcludedFieldType.ts b/packages/twenty-front/src/modules/settings/data-model/types/SettingsExcludedFieldType.ts index 05b324ede1..eed8f20ae9 100644 --- a/packages/twenty-front/src/modules/settings/data-model/types/SettingsExcludedFieldType.ts +++ b/packages/twenty-front/src/modules/settings/data-model/types/SettingsExcludedFieldType.ts @@ -3,5 +3,5 @@ import { type PickLiteral } from '~/types/PickLiteral'; export type SettingsExcludedFieldType = PickLiteral< FieldType, - 'POSITION' | 'TS_VECTOR' | 'RICH_TEXT' | 'RICH_TEXT_V2' + 'POSITION' | 'TS_VECTOR' | 'RICH_TEXT' | 'RICH_TEXT_V2' | 'NUMERIC' >; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-type-validator.service.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-type-validator.service.ts index 33128db329..8fc60b1313 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-type-validator.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-type-validator.service.ts @@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common'; import { msg } from '@lingui/core/macro'; import { isDefined } from 'class-validator'; +import { FieldMetadataType } from 'twenty-shared/types'; import { FieldMetadataExceptionCode } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; import { FlatEntityPropertiesUpdates } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-properties-updates.type'; @@ -20,6 +21,32 @@ export type GenericValidateFlatFieldMetadataTypeSpecificitiesArgs = updates?: FlatEntityPropertiesUpdates<'fieldMetadata'>; }; +const rejectUserCreation = ( + fieldType: FieldMetadataType, + message: string, + userFriendlyMessage: ReturnType, +) => { + return async ( + args: GenericValidateFlatFieldMetadataTypeSpecificitiesArgs, + ): Promise => { + const isCreation = !isDefined(args.updates); + const isCustomField = args.flatEntityToValidate.isCustom; + + if (isCreation && isCustomField) { + return [ + { + code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT, + message, + value: fieldType, + userFriendlyMessage, + }, + ]; + } + + return []; + }; +}; + @Injectable() export class FlatFieldMetadataTypeValidatorService { constructor() {} @@ -37,14 +64,26 @@ export class FlatFieldMetadataTypeValidatorService { FULL_NAME: DEFAULT_NO_VALIDATION, LINKS: DEFAULT_NO_VALIDATION, NUMBER: DEFAULT_NO_VALIDATION, - NUMERIC: DEFAULT_NO_VALIDATION, + NUMERIC: rejectUserCreation( + FieldMetadataType.NUMERIC, + 'Field type NUMERIC is not supported for field creation. Use NUMBER instead.', + msg`Field type NUMERIC is not supported. Use Number instead.`, + ), PHONES: DEFAULT_NO_VALIDATION, - POSITION: DEFAULT_NO_VALIDATION, + POSITION: rejectUserCreation( + FieldMetadataType.POSITION, + 'Field type POSITION is a system type and cannot be created manually.', + msg`Field type POSITION is a system type and cannot be created manually.`, + ), RAW_JSON: DEFAULT_NO_VALIDATION, RICH_TEXT: DEFAULT_NO_VALIDATION, RICH_TEXT_V2: DEFAULT_NO_VALIDATION, TEXT: DEFAULT_NO_VALIDATION, - TS_VECTOR: DEFAULT_NO_VALIDATION, + TS_VECTOR: rejectUserCreation( + FieldMetadataType.TS_VECTOR, + 'Field type TS_VECTOR is a system type and cannot be created manually.', + msg`Field type TS_VECTOR is a system type and cannot be created manually.`, + ), UUID: DEFAULT_NO_VALIDATION, MORPH_RELATION: async (args) => { diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/__snapshots__/failing-create-one-field-metadata.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/field-metadata/__snapshots__/failing-create-one-field-metadata.integration-spec.ts.snap new file mode 100644 index 0000000000..f3f62455dd --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/__snapshots__/failing-create-one-field-metadata.integration-spec.ts.snap @@ -0,0 +1,171 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Failing create field metadata tests suite should fail to create NUMERIC field type via API 1`] = ` +[ + { + "extensions": { + "code": "METADATA_VALIDATION_FAILED", + "errors": { + "cronTrigger": [], + "databaseEventTrigger": [], + "fieldMetadata": [ + { + "errors": [ + { + "code": "INVALID_FIELD_INPUT", + "message": "Field type NUMERIC is not supported for field creation. Use NUMBER instead.", + "userFriendlyMessage": "Field type NUMERIC is not supported. Use Number instead.", + "value": "NUMERIC", + }, + ], + "flatEntityMinimalInformation": { + "id": Any, + "name": "numericField", + "objectMetadataId": Any, + }, + "status": "fail", + "type": "create_field", + }, + ], + "index": [], + "objectMetadata": [], + "routeTrigger": [], + "serverlessFunction": [], + "view": [], + "viewField": [], + "viewFilter": [], + "viewGroup": [], + }, + "message": "Validation failed for 0 object(s) and 0 field(s)", + "summary": { + "invalidCronTrigger": 0, + "invalidDatabaseEventTrigger": 0, + "invalidFieldMetadata": 0, + "invalidIndex": 0, + "invalidObjectMetadata": 0, + "invalidRouteTrigger": 0, + "invalidServerlessFunction": 0, + "invalidView": 0, + "invalidViewField": 0, + "invalidViewFilter": 0, + "invalidViewGroup": 0, + "totalErrors": 0, + }, + "userFriendlyMessage": "Validation failed for 0 object(s) and 0 field(s)", + }, + "message": "Multiple validation errors occurred while creating fields", + "name": "GraphQLError", + }, +] +`; + +exports[`Failing create field metadata tests suite should fail to create POSITION field type via API 1`] = ` +[ + { + "extensions": { + "code": "METADATA_VALIDATION_FAILED", + "errors": { + "cronTrigger": [], + "databaseEventTrigger": [], + "fieldMetadata": [ + { + "errors": [ + { + "code": "INVALID_FIELD_INPUT", + "message": "Field type POSITION is a system type and cannot be created manually.", + "userFriendlyMessage": "Field type POSITION is a system type and cannot be created manually.", + "value": "POSITION", + }, + ], + "flatEntityMinimalInformation": { + "id": Any, + "name": "positionField", + "objectMetadataId": Any, + }, + "status": "fail", + "type": "create_field", + }, + ], + "index": [], + "objectMetadata": [], + "routeTrigger": [], + "serverlessFunction": [], + "view": [], + "viewField": [], + "viewFilter": [], + "viewGroup": [], + }, + "message": "Validation failed for 0 object(s) and 0 field(s)", + "summary": { + "invalidCronTrigger": 0, + "invalidDatabaseEventTrigger": 0, + "invalidFieldMetadata": 0, + "invalidIndex": 0, + "invalidObjectMetadata": 0, + "invalidRouteTrigger": 0, + "invalidServerlessFunction": 0, + "invalidView": 0, + "invalidViewField": 0, + "invalidViewFilter": 0, + "invalidViewGroup": 0, + "totalErrors": 0, + }, + "userFriendlyMessage": "Validation failed for 0 object(s) and 0 field(s)", + }, + "message": "Multiple validation errors occurred while creating fields", + "name": "GraphQLError", + }, +] +`; + +exports[`Failing create field metadata tests suite should fail to create TS_VECTOR field type via API 1`] = ` +[ + { + "extensions": { + "code": "METADATA_VALIDATION_FAILED", + "errors": { + "cronTrigger": [], + "databaseEventTrigger": [], + "fieldMetadata": [], + "index": [], + "objectMetadata": [ + { + "errors": [ + { + "code": "INVALID_FIELD_INPUT", + "message": "TS Vector is not supported for field creation", + }, + ], + "flatEntityMinimalInformation": {}, + "type": "create_field", + }, + ], + "routeTrigger": [], + "serverlessFunction": [], + "view": [], + "viewField": [], + "viewFilter": [], + "viewGroup": [], + }, + "message": "Validation failed for 0 object(s) and 0 field(s)", + "summary": { + "invalidCronTrigger": 0, + "invalidDatabaseEventTrigger": 0, + "invalidFieldMetadata": 0, + "invalidIndex": 0, + "invalidObjectMetadata": 0, + "invalidRouteTrigger": 0, + "invalidServerlessFunction": 0, + "invalidView": 0, + "invalidViewField": 0, + "invalidViewFilter": 0, + "invalidViewGroup": 0, + "totalErrors": 0, + }, + "userFriendlyMessage": "Validation failed for 0 object(s) and 0 field(s)", + }, + "message": "Multiple validation errors occurred while creating field", + "name": "GraphQLError", + }, +] +`; diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/failing-create-one-field-metadata.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/field-metadata/failing-create-one-field-metadata.integration-spec.ts new file mode 100644 index 0000000000..a5b9ee1157 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/failing-create-one-field-metadata.integration-spec.ts @@ -0,0 +1,89 @@ +import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-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'; + +describe('Failing create field metadata tests suite', () => { + let createdObjectMetadataId: string; + + beforeAll(async () => { + const { data } = await createOneObjectMetadata({ + expectToFail: false, + input: { + nameSingular: 'testFailingObject', + namePlural: 'testFailingObjects', + labelSingular: 'Test Failing Object', + labelPlural: 'Test Failing Objects', + icon: 'IconTestPipe', + isLabelSyncedWithName: false, + }, + }); + + createdObjectMetadataId = data.createOneObject.id; + }); + + afterAll(async () => { + await updateOneObjectMetadata({ + expectToFail: false, + input: { + idToUpdate: createdObjectMetadataId, + updatePayload: { isActive: false }, + }, + }); + await deleteOneObjectMetadata({ + expectToFail: false, + input: { idToDelete: createdObjectMetadataId }, + }); + }); + + it.each([ + { + type: FieldMetadataType.NUMERIC, + name: 'numericField', + expectedMessage: 'NUMERIC', + }, + { + type: FieldMetadataType.POSITION, + name: 'positionField', + expectedMessage: 'POSITION', + }, + { + type: FieldMetadataType.TS_VECTOR, + name: 'tsVectorField', + expectedMessage: 'TS Vector', + }, + ])( + 'should fail to create $type field type via API', + async ({ type, name, expectedMessage }) => { + const { data, errors } = await createOneFieldMetadata({ + expectToFail: true, + input: { + objectMetadataId: createdObjectMetadataId, + type, + name, + label: name, + isLabelSyncedWithName: false, + }, + gqlFields: ` + id + type + name + label + `, + }); + + expect(data).toBeNull(); + expect(errors).toBeDefined(); + expect(errors).toMatchSnapshot( + extractRecordIdsAndDatesAsExpectAny(errors), + ); + expect(errors[0].extensions.code).toBe('METADATA_VALIDATION_FAILED'); + + const errorDetails = JSON.stringify(errors[0].extensions.errors); + + expect(errorDetails).toContain(expectedMessage); + }, + ); +});