From eac6723d78a2cfa0a1d077ef172c7703c09deed4 Mon Sep 17 00:00:00 2001 From: Sonarly Claude Code Date: Thu, 19 Mar 2026 15:13:33 +0000 Subject: [PATCH] fix: handle string-typed options in transformEnumValue https://sonarly.com/issue/16534?type=bug `transformEnumValue()` calls `.map()` on the `options` value without checking if it's actually an array. When a client sends `options` as a JSON-encoded string (e.g., `"[{...}]"`) instead of a native JSON array via the `updateOneField` mutation, the `@Transform` decorator passes the raw string to `transformEnumValue()`, causing a `TypeError`. Fix: **`transform-enum-value.ts`**: The function now accepts `string` in addition to `FieldMetadataDefaultOption[]`. When a string is received (from clients sending JSON-encoded options via GraphQL), it parses it with `JSON.parse`. It also guards against non-array parsed values by returning them as-is, letting downstream validation handle the error. **`field-metadata.dto.ts`**: Removed the unsafe `as FieldMetadataDefaultOption[]` cast from the `@Transform` decorator, which was masking the type mismatch at compile time. The `transformEnumValue` function now handles the real runtime types directly. Removed the now-unused `FieldMetadataDefaultOption` import. **`transform-enum-value.spec.ts`**: Added tests covering: undefined input, array input with digit-prefixed values, JSON string input, and JSON string input with digit-prefixed values. --- .../field-metadata/dtos/field-metadata.dto.ts | 5 +- .../__tests__/transform-enum-value.spec.ts | 59 +++++++++++++++++++ .../src/engine/utils/transform-enum-value.ts | 17 +++++- 3 files changed, 75 insertions(+), 6 deletions(-) create mode 100644 packages/twenty-server/src/engine/utils/__tests__/transform-enum-value.spec.ts diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto.ts index b2c778b4ec5..9d525ac3b85 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto.ts @@ -33,7 +33,6 @@ import { import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; import { IsValidMetadataName } from 'src/engine/decorators/metadata/is-valid-metadata-name.decorator'; import { FieldStandardOverridesDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-standard-overrides.dto'; -import { type FieldMetadataDefaultOption } from 'src/engine/metadata-modules/field-metadata/dtos/options.input'; import { ObjectMetadataDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-metadata.dto'; import { transformEnumValue } from 'src/engine/utils/transform-enum-value'; @@ -132,9 +131,7 @@ export class FieldMetadataDTO { @Field(() => GraphQLJSON, { nullable: true }) defaultValue?: FieldMetadataDefaultValue; - @Transform(({ value }) => - transformEnumValue(value as FieldMetadataDefaultOption[]), - ) + @Transform(({ value }) => transformEnumValue(value)) @IsOptional() @Field(() => GraphQLJSON, { nullable: true }) options?: FieldMetadataOptions; diff --git a/packages/twenty-server/src/engine/utils/__tests__/transform-enum-value.spec.ts b/packages/twenty-server/src/engine/utils/__tests__/transform-enum-value.spec.ts new file mode 100644 index 00000000000..58f8892c321 --- /dev/null +++ b/packages/twenty-server/src/engine/utils/__tests__/transform-enum-value.spec.ts @@ -0,0 +1,59 @@ +import { transformEnumValue } from 'src/engine/utils/transform-enum-value'; + +describe('transformEnumValue', () => { + it('should return undefined when options is undefined', () => { + expect(transformEnumValue(undefined)).toBeUndefined(); + }); + + it('should prefix option values starting with a digit', () => { + const options = [ + { value: '1_OPTION', label: 'Option 1', color: 'green' as const }, + { value: 'OPTION_2', label: 'Option 2', color: 'blue' as const }, + ]; + + const result = transformEnumValue(options); + + expect(result).toEqual([ + { value: '_1_OPTION', label: 'Option 1', color: 'green' }, + { value: 'OPTION_2', label: 'Option 2', color: 'blue' }, + ]); + }); + + it('should parse and transform options passed as a JSON string', () => { + const options = JSON.stringify([ + { value: 'STARTUP', label: 'Startup', color: 'green' }, + { value: 'SME', label: 'SME', color: 'turquoise' }, + ]); + + const result = transformEnumValue(options); + + expect(result).toEqual([ + { value: 'STARTUP', label: 'Startup', color: 'green' }, + { value: 'SME', label: 'SME', color: 'turquoise' }, + ]); + }); + + it('should prefix digit-starting values in JSON string options', () => { + const options = JSON.stringify([ + { value: '3RD_PARTY', label: '3rd Party', color: 'red' }, + ]); + + const result = transformEnumValue(options); + + expect(result).toEqual([ + { value: '_3RD_PARTY', label: '3rd Party', color: 'red' }, + ]); + }); + + it('should return options unchanged when no values start with a digit', () => { + const options = [ + { value: 'STARTUP', label: 'Startup', color: 'green' as const }, + ]; + + const result = transformEnumValue(options); + + expect(result).toEqual([ + { value: 'STARTUP', label: 'Startup', color: 'green' }, + ]); + }); +}); diff --git a/packages/twenty-server/src/engine/utils/transform-enum-value.ts b/packages/twenty-server/src/engine/utils/transform-enum-value.ts index 74bd674e573..47a95544550 100644 --- a/packages/twenty-server/src/engine/utils/transform-enum-value.ts +++ b/packages/twenty-server/src/engine/utils/transform-enum-value.ts @@ -1,7 +1,20 @@ import { type FieldMetadataDefaultOption } from 'src/engine/metadata-modules/field-metadata/dtos/options.input'; -export function transformEnumValue(options?: FieldMetadataDefaultOption[]) { - return options?.map((option) => { +export function transformEnumValue( + options?: FieldMetadataDefaultOption[] | string, +) { + if (!options) { + return options; + } + + const parsedOptions = + typeof options === 'string' ? JSON.parse(options) : options; + + if (!Array.isArray(parsedOptions)) { + return parsedOptions; + } + + return parsedOptions.map((option: FieldMetadataDefaultOption) => { if (/^\d/.test(option.value)) { return { ...option,