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
This commit is contained in:
neo773
2025-11-25 14:21:48 +05:30
committed by GitHub
parent 7109abc311
commit 316aec3c40
5 changed files with 303 additions and 10 deletions
@@ -69,12 +69,6 @@ export const SETTINGS_NON_COMPOSITE_FIELD_TYPE_CONFIGS: SettingsNonCompositeFiel
],
category: 'Basic',
} as const satisfies SettingsFieldTypeConfig<FieldTextValue>,
[FieldMetadataType.NUMERIC]: {
label: 'Numeric',
Icon: IllustrationIconNumbers,
exampleValues: [2000, 3000, 4000],
category: 'Basic',
} as const satisfies SettingsFieldTypeConfig<FieldNumberValue>,
[FieldMetadataType.NUMBER]: {
label: 'Number',
Icon: IllustrationIconNumbers,
@@ -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'
>;
@@ -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<typeof msg>,
) => {
return async (
args: GenericValidateFlatFieldMetadataTypeSpecificitiesArgs,
): Promise<FlatFieldMetadataValidationError[]> => {
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) => {
@@ -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<String>,
"name": "numericField",
"objectMetadataId": Any<String>,
},
"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<String>,
"name": "positionField",
"objectMetadataId": Any<String>,
},
"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",
},
]
`;
@@ -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);
},
);
});