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:
-6
@@ -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,
|
||||
|
||||
+1
-1
@@ -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'
|
||||
>;
|
||||
|
||||
+42
-3
@@ -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) => {
|
||||
|
||||
+171
@@ -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",
|
||||
},
|
||||
]
|
||||
`;
|
||||
+89
@@ -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);
|
||||
},
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user