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:
+5
-4
@@ -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(
|
||||
|
||||
+31
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+38
@@ -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;
|
||||
};
|
||||
+10
-4
@@ -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(
|
||||
|
||||
+42
@@ -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;
|
||||
};
|
||||
+172
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
+2
-1
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+24
@@ -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""`;
|
||||
|
||||
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\\""]"`;
|
||||
|
||||
+44
@@ -345,6 +345,50 @@ export const failingCreateInputByFieldMetadataType: {
|
||||
emailsField: 'not-an-email',
|
||||
},
|
||||
},
|
||||
{
|
||||
input: {
|
||||
emailsField: {
|
||||
primaryEmail: 'not-an-email',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: {
|
||||
emailsField: {
|
||||
additionalEmails: 'not-an-email',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: {
|
||||
emailsField: {
|
||||
additionalEmails: ['not-an-email'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: {
|
||||
emailsField: {
|
||||
primaryEmail: 'email@email.com',
|
||||
additionalEmails: ['not-an-email'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: {
|
||||
emailsField: {
|
||||
primaryEmail: 'not-an-email',
|
||||
additionalEmails: ['additional@email.com'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: {
|
||||
emailsField: {
|
||||
additionalEmails: ['not-an-email', 'additional@email.com'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
[FieldMetadataType.PHONES]: [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user