Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -120,10 +120,11 @@ export class DataArgProcessor {
);
}

const fieldMetadata = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: fieldMetadataId,
flatEntityMaps: flatFieldMetadataMaps,
});
const fieldMetadata =
findFlatEntityByIdInFlatEntityMaps<FlatFieldMetadata>({
flatEntityId: fieldMetadataId,
flatEntityMaps: flatFieldMetadataMaps,
});

if (!fieldMetadata) {
throw new CommonQueryRunnerException(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,5 +83,36 @@ describe('validateEmailsFieldOrThrow', () => {
validateEmailsFieldOrThrow(emailsValue, 'testField'),
).toThrow(CommonQueryRunnerException);
});

it('should throw when additionalEmails is an invalid string', () => {
const emailsValue = {
additionalEmails: 'ADDITIONALexample.com',
};

expect(() =>
validateEmailsFieldOrThrow(emailsValue, 'testField'),
).toThrow(CommonQueryRunnerException);
});

it('should throw when primaryEmail is invalid but additionalEmails are valid', () => {
const emailsValue = {
primaryEmail: 'Primaryexample.com',
additionalEmails: ['additional@example.com'],
};

expect(() =>
validateEmailsFieldOrThrow(emailsValue, 'testField'),
).toThrow(CommonQueryRunnerException);
});

it('should throw when one of additionalEmails is invalid', () => {
const emailsValue = {
additionalEmails: ['Additional1example.com', 'additional2@example.com'],
};

expect(() =>
validateEmailsFieldOrThrow(emailsValue, 'testField'),
).toThrow(CommonQueryRunnerException);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { inspect } from 'util';

import { msg } from '@lingui/core/macro';
import { isNull } from '@sniptt/guards';

import { validateEmailsPrimaryEmailSubfieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-emails-primary-email-subfield-or-throw.util';
import {
CommonQueryRunnerException,
CommonQueryRunnerExceptionCode,
} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';

export const validateEmailsAdditionalEmailsSubfieldOrThrow = (
value: unknown,
fieldName: string,
): string | string[] | null => {
if (isNull(value)) return null;

if (typeof value === 'string') {
return validateEmailsPrimaryEmailSubfieldOrThrow(value, fieldName);
}

if (
!Array.isArray(value) ||
value.some((item) =>
isNull(validateEmailsPrimaryEmailSubfieldOrThrow(item, fieldName)),
)
) {
const inspectedValue = inspect(value);

throw new CommonQueryRunnerException(
`Invalid value ${inspectedValue} for field "${fieldName} - Array values need to be string"`,
CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA,
{ userFriendlyMessage: msg`Invalid value: "${inspectedValue}"` },
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: userFriendlyMessage repeats inspected technical details; per guidance it should stay generic and avoid duplicating the internal error.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-emails-additional-emails-subfield-or-throw.util.ts, line 34:

<comment>userFriendlyMessage repeats inspected technical details; per guidance it should stay generic and avoid duplicating the internal error.</comment>

<file context>
@@ -0,0 +1,39 @@
+    throw new CommonQueryRunnerException(
+      `Invalid value ${inspectedValue} for field "${fieldName} - Array values need to be string"`,
+      CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA,
+      { userFriendlyMessage: msg`Invalid value: "${inspectedValue}"` },
+    );
+  }
</file context>
Fix with Cubic

);
}

return value;
};
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { msg } from '@lingui/core/macro';
import { isNull } from '@sniptt/guards';

import { validateArrayFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-array-field-or-throw.util';
import { validateEmailsAdditionalEmailsSubfieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-emails-additional-emails-subfield-or-throw.util';
import { validateEmailsPrimaryEmailSubfieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-emails-primary-email-subfield-or-throw.util';
import { validateRawJsonFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-raw-json-field-or-throw.util';
import { validateTextFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-text-field-or-throw.util';
import {
CommonQueryRunnerException,
CommonQueryRunnerExceptionCode,
Expand All @@ -23,10 +23,16 @@ export const validateEmailsFieldOrThrow = (
for (const [subField, subFieldValue] of Object.entries(preValidatedValue)) {
switch (subField) {
case 'primaryEmail':
validateTextFieldOrThrow(subFieldValue, `${fieldName}.${subField}`);
validateEmailsPrimaryEmailSubfieldOrThrow(
subFieldValue,
`${fieldName}.${subField}`,
);
break;
case 'additionalEmails':
validateArrayFieldOrThrow(subFieldValue, `${fieldName}.${subField}`);
validateEmailsAdditionalEmailsSubfieldOrThrow(
subFieldValue,
`${fieldName}.${subField}`,
);
break;
default:
throw new CommonQueryRunnerException(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { inspect } from 'util';

import { msg } from '@lingui/core/macro';
import { isNonEmptyString, isNull } from '@sniptt/guards';
import { z } from 'zod';

import {
CommonQueryRunnerException,
CommonQueryRunnerExceptionCode,
} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';

export const validateEmailsPrimaryEmailSubfieldOrThrow = (
value: unknown,
fieldName: string,
): string | null => {
if (isNull(value) || !isNonEmptyString(value)) return null;

if (typeof value !== 'string') {
const inspectedValue = inspect(value);

throw new CommonQueryRunnerException(
`Invalid string value ${inspectedValue} for email field "${fieldName}"`,
CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA,
{ userFriendlyMessage: msg`Invalid value: "${inspectedValue}"` },
);
}

if (!z.email({ pattern: z.regexes.unicodeEmail }).safeParse(value).success) {
const inspectedValue = inspect(value);

throw new CommonQueryRunnerException(
`Invalid string value ${inspectedValue} for email field "${fieldName}"`,
CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA,
{ userFriendlyMessage: msg`Invalid value: "${inspectedValue}"` },
);
}

return value;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import { transformEmailsValue } from 'src/engine/core-modules/record-transformer/utils/transform-emails-value.util';

describe('transformEmailsValue', () => {
it('should return undefined when value is undefined', () => {
const result = transformEmailsValue(undefined);

expect(result).toBeUndefined();
});

it('should return null when value is null', () => {
const result = transformEmailsValue(null);

expect(result).toBeNull();
});

it('should convert primaryEmail to lowercase', () => {
const value = {
primaryEmail: 'TEST@EXAMPLE.COM',
additionalEmails: null,
};

const result = transformEmailsValue(value);

expect(result.primaryEmail).toBe('test@example.com');
});

it('should return null for primaryEmail when it is empty string', () => {
const value = {
primaryEmail: '',
additionalEmails: null,
};

const result = transformEmailsValue(value);

expect(result.primaryEmail).toBeNull();
});

it('should return null for primaryEmail when it is null', () => {
const value = {
primaryEmail: null,
additionalEmails: null,
};

const result = transformEmailsValue(value);

expect(result.primaryEmail).toBeNull();
});

it('should return null for primaryEmail when it is undefined', () => {
const value = {
additionalEmails: null,
};

const result = transformEmailsValue(value);

expect(result.primaryEmail).toBeNull();
});

it('should convert additionalEmails array to lowercase JSON string', () => {
const value = {
primaryEmail: 'test@example.com',
additionalEmails: ['USER1@EXAMPLE.COM', 'USER2@EXAMPLE.COM'],
};

const result = transformEmailsValue(value);

expect(result.additionalEmails).toBe(
'["user1@example.com","user2@example.com"]',
);
});

it('should parse and convert additionalEmails JSON string to lowercase', () => {
const value = {
primaryEmail: 'test@example.com',
additionalEmails: '["USER1@EXAMPLE.COM","USER2@EXAMPLE.COM"]',
};

const result = transformEmailsValue(value);

expect(result.additionalEmails).toBe(
'["user1@example.com","user2@example.com"]',
);
});

it('should return null for additionalEmails when it is an empty array', () => {
const value = {
primaryEmail: 'test@example.com',
additionalEmails: [],
};

const result = transformEmailsValue(value);

expect(result.additionalEmails).toBeNull();
});

it('should return null for additionalEmails when it is null', () => {
const value = {
primaryEmail: 'test@example.com',
additionalEmails: null,
};

const result = transformEmailsValue(value);

expect(result.additionalEmails).toBeNull();
});

it('should handle mixed case emails in additionalEmails array', () => {
const value = {
primaryEmail: 'test@example.com',
additionalEmails: ['Test1@Example.COM', 'TEST2@example.com'],
};

const result = transformEmailsValue(value);

expect(result.additionalEmails).toBe(
'["test1@example.com","test2@example.com"]',
);
});

it('should transform both primaryEmail and additionalEmails correctly', () => {
const value = {
primaryEmail: 'PRIMARY@EXAMPLE.COM',
additionalEmails: ['ADDITIONAL1@EXAMPLE.COM', 'ADDITIONAL2@EXAMPLE.COM'],
};

const result = transformEmailsValue(value);

expect(result).toEqual({
primaryEmail: 'primary@example.com',
additionalEmails: '["additional1@example.com","additional2@example.com"]',
});
});

it('should handle case where primaryEmail is null and additionalEmails exist', () => {
const value = {
primaryEmail: null,
additionalEmails: ['USER@EXAMPLE.COM'],
};

const result = transformEmailsValue(value);

expect(result).toEqual({
primaryEmail: null,
additionalEmails: '["user@example.com"]',
});
});

it('should handle case where primaryEmail exists and additionalEmails is null', () => {
const value = {
primaryEmail: 'USER@EXAMPLE.COM',
additionalEmails: null,
};

const result = transformEmailsValue(value);

expect(result).toEqual({
primaryEmail: 'user@example.com',
additionalEmails: null,
});
});

it('should handle empty value object', () => {
const value = {};

const result = transformEmailsValue(value);

expect(result).toEqual({
primaryEmail: null,
additionalEmails: undefined,
});
});
});
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { isNonEmptyArray, isNonEmptyString } from '@sniptt/guards';
import { isDefined } from 'class-validator';

export const transformEmailsValue = (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value: any,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
): any => {
if (!value) {
if (!isDefined(value)) {
return value;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,28 @@

exports[`Create input validation - EMAILS Gql create input - failure EMAILS - should fail with : {"emailsField":"not-an-email"} 1`] = `"Expected type "EmailsCreateInput" to be an object."`;

exports[`Create input validation - EMAILS Gql create input - failure EMAILS - should fail with : {"emailsField":{"additionalEmails":"not-an-email"}} 1`] = `"Invalid string value 'not-an-email' for email field "emailsField.additionalEmails""`;
Copy link
Copy Markdown
Contributor

@etiennejouan etiennejouan Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when updating integration test, you can launch this command to be sure test is ok + to update snapshot if needed

Image

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please write down a command I can run to check if test is OK? I don't have such dropdown in my IDE

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nx run twenty-server:jest --config ./jest-integration.config.ts packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/emails-field-create-input-validation.integration-spec.ts --silent=false --updateSnapshot

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!


exports[`Create input validation - EMAILS Gql create input - failure EMAILS - should fail with : {"emailsField":{"additionalEmails":["not-an-email","additional@email.com"]}} 1`] = `"Invalid string value 'not-an-email' for email field "emailsField.additionalEmails""`;

exports[`Create input validation - EMAILS Gql create input - failure EMAILS - should fail with : {"emailsField":{"additionalEmails":["not-an-email"]}} 1`] = `"Invalid string value 'not-an-email' for email field "emailsField.additionalEmails""`;

exports[`Create input validation - EMAILS Gql create input - failure EMAILS - should fail with : {"emailsField":{"primaryEmail":"email@email.com","additionalEmails":["not-an-email"]}} 1`] = `"Invalid string value 'not-an-email' for email field "emailsField.additionalEmails""`;

exports[`Create input validation - EMAILS Gql create input - failure EMAILS - should fail with : {"emailsField":{"primaryEmail":"not-an-email","additionalEmails":["additional@email.com"]}} 1`] = `"Invalid string value 'not-an-email' for email field "emailsField.primaryEmail""`;

exports[`Create input validation - EMAILS Gql create input - failure EMAILS - should fail with : {"emailsField":{"primaryEmail":"not-an-email"}} 1`] = `"Invalid string value 'not-an-email' for email field "emailsField.primaryEmail""`;

exports[`Create input validation - EMAILS Rest create input - failure EMAILS - should fail with : {"emailsField":"not-an-email"} 1`] = `"["Invalid object value 'not-an-email' for field \\"emailsField\\""]"`;

exports[`Create input validation - EMAILS Rest create input - failure EMAILS - should fail with : {"emailsField":{"additionalEmails":"not-an-email"}} 1`] = `"["Invalid string value 'not-an-email' for email field \\"emailsField.additionalEmails\\""]"`;

exports[`Create input validation - EMAILS Rest create input - failure EMAILS - should fail with : {"emailsField":{"additionalEmails":["not-an-email","additional@email.com"]}} 1`] = `"["Invalid string value 'not-an-email' for email field \\"emailsField.additionalEmails\\""]"`;

exports[`Create input validation - EMAILS Rest create input - failure EMAILS - should fail with : {"emailsField":{"additionalEmails":["not-an-email"]}} 1`] = `"["Invalid string value 'not-an-email' for email field \\"emailsField.additionalEmails\\""]"`;

exports[`Create input validation - EMAILS Rest create input - failure EMAILS - should fail with : {"emailsField":{"primaryEmail":"email@email.com","additionalEmails":["not-an-email"]}} 1`] = `"["Invalid string value 'not-an-email' for email field \\"emailsField.additionalEmails\\""]"`;

exports[`Create input validation - EMAILS Rest create input - failure EMAILS - should fail with : {"emailsField":{"primaryEmail":"not-an-email","additionalEmails":["additional@email.com"]}} 1`] = `"["Invalid string value 'not-an-email' for email field \\"emailsField.primaryEmail\\""]"`;

exports[`Create input validation - EMAILS Rest create input - failure EMAILS - should fail with : {"emailsField":{"primaryEmail":"not-an-email"}} 1`] = `"["Invalid string value 'not-an-email' for email field \\"emailsField.primaryEmail\\""]"`;
Loading
Loading