Lowercase email (#17775)

Fixes https://github.com/twentyhq/core-team-issues/issues/120 #16976
Partially related to #17711

Frontend check surprisingly was one-liner covering both email input and
import files

Migration script will be done in next commit

---------

Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
This commit is contained in:
BOHEUS
2026-02-10 18:24:49 +00:00
committed by GitHub
parent bc72879c70
commit 16d414590b
9 changed files with 368 additions and 9 deletions
@@ -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(
@@ -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);
});
});
});
@@ -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}"` },
);
}
return value;
};
@@ -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,
@@ -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(
@@ -0,0 +1,42 @@
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)) 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 &&
isNonEmptyString(value)
) {
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;
};