Transform record phone field metadata (#12706)
# Introduction close https://github.com/twentyhq/twenty/issues/12343 Adding a transform step for any field phone in order to infer country code and calling code from the number if they're provided ## Edges cases ```ts RecordTransformerExceptionCode.INVALID_PHONE_NUMBER: RecordTransformerExceptionCode.INVALID_PHONE_COUNTRY_CODE: RecordTransformerExceptionCode.CONFLICTING_PHONE_COUNTRY_CODE: RecordTransformerExceptionCode.CONFLICTING_PHONE_CALLING_CODE: RecordTransformerExceptionCode.CONFLICTING_PHONE_CALLING_CODE_AND_COUNTRY_CODE: RecordTransformerExceptionCode.INVALID_PHONE_CALLING_CODE: RecordTransformerExceptionCode.INVALID_URL: ``` ## Coverage Note: Will handle REST api integration testing pivot and UPDATE operation later in the afternoon, critical bug appeared that I prefer handling before improving this PR coverage, also would be too many updates Note2: Haven't fuzzed all of the string inputs, would seem overkill for such a use case, to be debated ```ts PASS test/integration/metadata/suites/field-metadata/phone/create-one-field-metadata-phone.integration-spec.ts (23.609 s) Phone field metadata tests suite ✓ It should succeed create primary phone field (1397 ms) ✓ It should succeed create primary phone field with number and other information (930 ms) ✓ It should succeed create primary phone field with full international format and other information (893 ms) ✓ It should succeed create primary phone field with full international and infer other information from it but not the countryCode as its shared (825 ms) ✓ It should succeed create primary phone field with full international and infer other information from it (818 ms) ✓ It should succeed create primary phone field with empty payload (827 ms) ✓ It should succeed create additional phone field with number and other information (894 ms) ✓ It should succeed create additional phone field with full international format and other information (1024 ms) ✓ It should succeed create additional phone field with full international and infer other information from it but not the countryCode as its shared (808 ms) ✓ It should succeed create additional phone field with full international and infer other information from it (751 ms) ✓ It should succeed create additional phone field with empty payload (739 ms) ✓ It should fail to create primary phone field without country or calling code at all (776 ms) ✓ It should fail to create primary phone field with invalid country code (782 ms) ✓ It should fail to create primary phone field with invalid calling code (858 ms) ✓ It should fail to create primary phone field with conflicting country code and calling code (872 ms) ✓ It should fail to create primary phone field with invalid phone number format (1489 ms) ✓ It should fail to create primary phone field with conflicting phone number country code (1425 ms) ✓ It should fail to create primary phone field with conflicting phone number calling code (1553 ms) ✓ It should fail to create primary phone field without country or calling code at all (814 ms) ✓ It should fail to create primary phone field with invalid country code (813 ms) ✓ It should fail to create primary phone field with invalid calling code (742 ms) ✓ It should fail to create primary phone field with conflicting country code and calling code (783 ms) ✓ It should fail to create primary phone field with invalid phone number format (731 ms) ✓ It should fail to create primary phone field with conflicting phone number country code (947 ms) ✓ It should fail to create primary phone field with conflicting phone number calling code (822 ms) Test Suites: 1 passed, 1 total Tests: 25 passed, 25 total Snapshots: 14 passed, 14 total Time: 23.627 s ```
This commit is contained in:
@ -0,0 +1,134 @@
|
||||
import { removeUndefinedFields } from '../removeUndefinedFields';
|
||||
|
||||
interface PrimitiveTestContext {
|
||||
description: string;
|
||||
input: null | undefined | string | number | boolean;
|
||||
expected: null | undefined | string | number | boolean;
|
||||
}
|
||||
|
||||
interface ObjectTestContext {
|
||||
description: string;
|
||||
input: Record<string, unknown>;
|
||||
expected: Record<string, unknown>;
|
||||
}
|
||||
|
||||
describe('removeUndefinedFields', () => {
|
||||
describe.each<PrimitiveTestContext>([
|
||||
{
|
||||
description: 'null',
|
||||
input: null,
|
||||
expected: null,
|
||||
},
|
||||
{
|
||||
description: 'undefined',
|
||||
input: undefined,
|
||||
expected: undefined,
|
||||
},
|
||||
{
|
||||
description: 'string',
|
||||
input: 'string',
|
||||
expected: 'string',
|
||||
},
|
||||
{
|
||||
description: 'number',
|
||||
input: 123,
|
||||
expected: 123,
|
||||
},
|
||||
{
|
||||
description: 'boolean true',
|
||||
input: true,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
description: 'boolean false',
|
||||
input: false,
|
||||
expected: false,
|
||||
},
|
||||
])('primitive value: $description', ({ input, expected }) => {
|
||||
it('should return the value as is', () => {
|
||||
expect(removeUndefinedFields(input)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe.each<ObjectTestContext>([
|
||||
{
|
||||
description: 'flat object',
|
||||
input: {
|
||||
name: 'John',
|
||||
age: 30,
|
||||
email: undefined,
|
||||
phone: null,
|
||||
address: undefined,
|
||||
},
|
||||
expected: {
|
||||
name: 'John',
|
||||
age: 30,
|
||||
phone: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
description: 'nested object',
|
||||
input: {
|
||||
name: 'John',
|
||||
contact: {
|
||||
email: undefined,
|
||||
phone: '123456',
|
||||
address: {
|
||||
street: '123 Main St',
|
||||
apt: undefined,
|
||||
city: 'New York',
|
||||
},
|
||||
},
|
||||
preferences: undefined,
|
||||
},
|
||||
expected: {
|
||||
name: 'John',
|
||||
contact: {
|
||||
phone: '123456',
|
||||
address: {
|
||||
street: '123 Main St',
|
||||
city: 'New York',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: 'arrays',
|
||||
input: {
|
||||
names: ['John', undefined, 'Jane', null],
|
||||
tags: [
|
||||
{ id: 1, label: 'active' },
|
||||
{ id: undefined, label: 'pending' },
|
||||
undefined,
|
||||
{ id: 3, label: undefined },
|
||||
],
|
||||
},
|
||||
expected: {
|
||||
names: ['John', 'Jane', null],
|
||||
tags: [
|
||||
{ id: 1, label: 'active' },
|
||||
{ label: 'pending' },
|
||||
{ id: 3 },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
description: 'empty objects after cleaning',
|
||||
input: {
|
||||
name: 'John',
|
||||
metadata: {
|
||||
tags: undefined,
|
||||
flags: undefined,
|
||||
},
|
||||
settings: {},
|
||||
},
|
||||
expected: {
|
||||
name: 'John',
|
||||
},
|
||||
},
|
||||
])('object case: $description', ({ input, expected }) => {
|
||||
it('should clean undefined fields correctly', () => {
|
||||
expect(removeUndefinedFields(input)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -15,6 +15,7 @@ export {
|
||||
getLogoUrlFromDomainName,
|
||||
} from './image/getLogoUrlFromDomainName';
|
||||
export { parseJson } from './parseJson';
|
||||
export { removeUndefinedFields } from './removeUndefinedFields';
|
||||
export { capitalize } from './strings/capitalize';
|
||||
export { absoluteUrlSchema } from './url/absoluteUrlSchema';
|
||||
export { buildSignedPath } from './url/buildSignedPath';
|
||||
|
||||
43
packages/twenty-shared/src/utils/removeUndefinedFields.ts
Normal file
43
packages/twenty-shared/src/utils/removeUndefinedFields.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { isUndefined } from '@sniptt/guards';
|
||||
|
||||
export const removeUndefinedFields = <T>(input: T): T | Partial<T> => {
|
||||
if (input === undefined) {
|
||||
return input;
|
||||
}
|
||||
|
||||
if (input === null || typeof input !== 'object') {
|
||||
return input;
|
||||
}
|
||||
|
||||
if (Array.isArray(input)) {
|
||||
return input
|
||||
.map((item) => removeUndefinedFields(item))
|
||||
.filter((item) => !isUndefined(item)) as T;
|
||||
}
|
||||
|
||||
return Object.entries(input as Record<string, unknown>).reduce(
|
||||
(acc, [key, value]) => {
|
||||
if (isUndefined(value)) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (value === null || value instanceof Date) {
|
||||
return { ...acc, [key]: value };
|
||||
}
|
||||
|
||||
if (typeof value === 'object') {
|
||||
const cleaned = removeUndefinedFields(value);
|
||||
if (
|
||||
!isUndefined(cleaned) &&
|
||||
Object.keys(cleaned as object).length > 0
|
||||
) {
|
||||
return { ...acc, [key]: cleaned };
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
return { ...acc, [key]: value };
|
||||
},
|
||||
{} as Record<string, unknown>,
|
||||
) as Partial<T>;
|
||||
};
|
||||
Reference in New Issue
Block a user