Compare commits

...
Author SHA1 Message Date
Sonarly Claude Code e7f4a4cce8 chore: improve monitoring for fix: handle string-typed options in transformEnumV
**`resolver-validation.pipe.ts`**: Wrapped the `plainToInstance` call in a try-catch that converts any `@Transform` decorator errors into `UserInputError` GraphQL responses. Previously, if a `@Transform` function threw (e.g., `TypeError: options?.map is not a function`), the error propagated as an unhandled exception captured by Sentry. Now, these malformed-input errors are returned to the client as proper GraphQL `UserInputError` responses with a descriptive message, reducing Sentry noise from client-side input issues.

This follows the existing pattern in the same file where `safeClassValidatorValidateWrapper` already catches validation errors, and the pipe already throws `UserInputError` for invalid inputs.
2026-03-19 15:13:33 +00:00
Sonarly Claude Code eac6723d78 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.
2026-03-19 15:13:33 +00:00
4 changed files with 85 additions and 7 deletions
@@ -29,7 +29,16 @@ export class ResolverValidationPipe implements PipeTransform {
return value;
}
const object = plainToInstance(metatype, value);
let object: object;
try {
object = plainToInstance(metatype, value);
} catch (error) {
throw new UserInputError(
`Invalid input: ${error instanceof Error ? error.message : 'transformation failed'}`,
);
}
const errors = await safeClassValidatorValidateWrapper(object);
if (errors.length === 0) {
@@ -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<T extends FieldMetadataType = FieldMetadataType> {
@Field(() => GraphQLJSON, { nullable: true })
defaultValue?: FieldMetadataDefaultValue<T>;
@Transform(({ value }) =>
transformEnumValue(value as FieldMetadataDefaultOption[]),
)
@Transform(({ value }) => transformEnumValue(value))
@IsOptional()
@Field(() => GraphQLJSON, { nullable: true })
options?: FieldMetadataOptions<T>;
@@ -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' },
]);
});
});
@@ -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,