Import - Improve phone validation (#12901)
Context : - Phones import is a bit complex if not all subfields are provided. - Phones subfield validation are absent or different from BE validation. Solution : - Normalize callingCode and countryCode validation (BE/FE) - Ease phone import if only phoneNumber is provided
This commit is contained in:
@ -405,4 +405,39 @@ describe('buildRecordFromImportedStructuredRow', () => {
|
|||||||
ratingField: '4',
|
ratingField: '4',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should handle case where user provides only a primaryPhoneNumber without calling code', () => {
|
||||||
|
const importedStructuredRow: ImportedStructuredRow<string> = {
|
||||||
|
'Primary Phone Number (phoneField)': '5550123',
|
||||||
|
};
|
||||||
|
|
||||||
|
const fields: FieldMetadataItem[] = [
|
||||||
|
{
|
||||||
|
id: '13',
|
||||||
|
name: 'phoneField',
|
||||||
|
label: 'Phone Field',
|
||||||
|
type: FieldMetadataType.PHONES,
|
||||||
|
isNullable: true,
|
||||||
|
isActive: true,
|
||||||
|
isCustom: false,
|
||||||
|
isSystem: false,
|
||||||
|
createdAt: '2023-01-01',
|
||||||
|
updatedAt: '2023-01-01',
|
||||||
|
icon: 'IconPhone',
|
||||||
|
description: null,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = buildRecordFromImportedStructuredRow({
|
||||||
|
importedStructuredRow,
|
||||||
|
fields,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
phoneField: {
|
||||||
|
primaryPhoneNumber: '5550123',
|
||||||
|
primaryPhoneCallingCode: '+1',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,14 +1,16 @@
|
|||||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||||
import { FieldActorForInputValue } from '@/object-record/record-field/types/FieldMetadata';
|
import { FieldActorForInputValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
import { COMPOSITE_FIELD_SUB_FIELD_LABELS } from '@/settings/data-model/constants/CompositeFieldSubFieldLabel';
|
import { getSubFieldOptionKey } from '@/object-record/spreadsheet-import/utils/getSubFieldOptionKey';
|
||||||
import { ImportedStructuredRow } from '@/spreadsheet-import/types';
|
import { ImportedStructuredRow } from '@/spreadsheet-import/types';
|
||||||
import { isNonEmptyString } from '@sniptt/guards';
|
import { isNonEmptyString } from '@sniptt/guards';
|
||||||
|
import { CountryCode, parsePhoneNumberWithError } from 'libphonenumber-js';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
import { castToString } from '~/utils/castToString';
|
import { castToString } from '~/utils/castToString';
|
||||||
import { convertCurrencyAmountToCurrencyMicros } from '~/utils/convertCurrencyToCurrencyMicros';
|
import { convertCurrencyAmountToCurrencyMicros } from '~/utils/convertCurrencyToCurrencyMicros';
|
||||||
import { isEmptyObject } from '~/utils/isEmptyObject';
|
import { isEmptyObject } from '~/utils/isEmptyObject';
|
||||||
|
import { stripSimpleQuotesFromString } from '~/utils/string/stripSimpleQuotesFromString';
|
||||||
|
|
||||||
type BuildRecordFromImportedStructuredRowArgs = {
|
type BuildRecordFromImportedStructuredRowArgs = {
|
||||||
importedStructuredRow: ImportedStructuredRow<any>;
|
importedStructuredRow: ImportedStructuredRow<any>;
|
||||||
@ -16,23 +18,14 @@ type BuildRecordFromImportedStructuredRowArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const buildCompositeFieldRecord = (
|
const buildCompositeFieldRecord = (
|
||||||
fieldName: string,
|
field: FieldMetadataItem,
|
||||||
importedStructuredRow: ImportedStructuredRow<any>,
|
importedStructuredRow: ImportedStructuredRow<any>,
|
||||||
compositeFieldConfig: Record<
|
compositeFieldConfig: Record<string, ((value: any) => any) | undefined>,
|
||||||
string,
|
|
||||||
{
|
|
||||||
labelKey: string;
|
|
||||||
transform?: (value: any) => any;
|
|
||||||
}
|
|
||||||
>,
|
|
||||||
): Record<string, any> | undefined => {
|
): Record<string, any> | undefined => {
|
||||||
const compositeFieldRecord = Object.entries(compositeFieldConfig).reduce(
|
const compositeFieldRecord = Object.entries(compositeFieldConfig).reduce(
|
||||||
(
|
(acc, [compositeFieldKey, transform]) => {
|
||||||
acc,
|
|
||||||
[compositeFieldKey, { labelKey: compositeFieldLabelKey, transform }],
|
|
||||||
) => {
|
|
||||||
const value =
|
const value =
|
||||||
importedStructuredRow[`${compositeFieldLabelKey} (${fieldName})`];
|
importedStructuredRow[getSubFieldOptionKey(field, compositeFieldKey)];
|
||||||
|
|
||||||
return isDefined(value)
|
return isDefined(value)
|
||||||
? { ...acc, [compositeFieldKey]: transform?.(value) || value }
|
? { ...acc, [compositeFieldKey]: transform?.(value) || value }
|
||||||
@ -106,123 +99,49 @@ export const buildRecordFromImportedStructuredRow = ({
|
|||||||
|
|
||||||
const recordToBuild: Record<string, any> = {};
|
const recordToBuild: Record<string, any> = {};
|
||||||
|
|
||||||
const {
|
const COMPOSITE_FIELD_TRANSFORM_CONFIGS = {
|
||||||
ADDRESS: {
|
|
||||||
addressCity: addressCityLabel,
|
|
||||||
addressCountry: addressCountryLabel,
|
|
||||||
addressPostcode: addressPostcodeLabel,
|
|
||||||
addressState: addressStateLabel,
|
|
||||||
addressStreet1: addressStreet1Label,
|
|
||||||
addressStreet2: addressStreet2Label,
|
|
||||||
},
|
|
||||||
CURRENCY: {
|
|
||||||
amountMicros: amountMicrosLabel,
|
|
||||||
currencyCode: currencyCodeLabel,
|
|
||||||
},
|
|
||||||
FULL_NAME: { firstName: firstNameLabel, lastName: lastNameLabel },
|
|
||||||
LINKS: {
|
|
||||||
primaryLinkUrl: primaryLinkUrlLabel,
|
|
||||||
primaryLinkLabel: primaryLinkLabelLabel,
|
|
||||||
secondaryLinks: secondaryLinksLabel,
|
|
||||||
},
|
|
||||||
EMAILS: {
|
|
||||||
primaryEmail: primaryEmailLabel,
|
|
||||||
additionalEmails: additionalEmailsLabel,
|
|
||||||
},
|
|
||||||
PHONES: {
|
|
||||||
primaryPhoneNumber: primaryPhoneNumberLabel,
|
|
||||||
primaryPhoneCountryCode: primaryPhoneCountryCodeLabel,
|
|
||||||
primaryPhoneCallingCode: primaryPhoneCallingCodeLabel,
|
|
||||||
additionalPhones: additionalPhonesLabel,
|
|
||||||
},
|
|
||||||
RICH_TEXT_V2: { blocknote: blocknoteLabel, markdown: markdownLabel },
|
|
||||||
} = COMPOSITE_FIELD_SUB_FIELD_LABELS;
|
|
||||||
|
|
||||||
const COMPOSITE_FIELD_CONFIGS = {
|
|
||||||
[FieldMetadataType.CURRENCY]: {
|
[FieldMetadataType.CURRENCY]: {
|
||||||
amountMicros: {
|
amountMicros: (value: any) =>
|
||||||
labelKey: amountMicrosLabel,
|
convertCurrencyAmountToCurrencyMicros(Number(value)),
|
||||||
transform: (value: any) =>
|
currencyCode: undefined,
|
||||||
convertCurrencyAmountToCurrencyMicros(Number(value)),
|
|
||||||
},
|
|
||||||
currencyCode: { labelKey: currencyCodeLabel },
|
|
||||||
},
|
},
|
||||||
|
|
||||||
[FieldMetadataType.ADDRESS]: {
|
[FieldMetadataType.ADDRESS]: {
|
||||||
addressStreet1: {
|
addressStreet1: castToString,
|
||||||
labelKey: addressStreet1Label,
|
addressStreet2: castToString,
|
||||||
transform: castToString,
|
addressCity: castToString,
|
||||||
},
|
addressPostcode: castToString,
|
||||||
addressStreet2: {
|
addressState: castToString,
|
||||||
labelKey: addressStreet2Label,
|
addressCountry: castToString,
|
||||||
transform: castToString,
|
|
||||||
},
|
|
||||||
addressCity: { labelKey: addressCityLabel, transform: castToString },
|
|
||||||
addressPostcode: {
|
|
||||||
labelKey: addressPostcodeLabel,
|
|
||||||
transform: castToString,
|
|
||||||
},
|
|
||||||
addressState: { labelKey: addressStateLabel, transform: castToString },
|
|
||||||
addressCountry: {
|
|
||||||
labelKey: addressCountryLabel,
|
|
||||||
transform: castToString,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
[FieldMetadataType.LINKS]: {
|
[FieldMetadataType.LINKS]: {
|
||||||
primaryLinkLabel: {
|
primaryLinkLabel: castToString,
|
||||||
labelKey: primaryLinkLabelLabel,
|
primaryLinkUrl: castToString,
|
||||||
transform: castToString,
|
secondaryLinks: linkArrayJSONSchema.parse,
|
||||||
},
|
|
||||||
primaryLinkUrl: {
|
|
||||||
labelKey: primaryLinkUrlLabel,
|
|
||||||
transform: castToString,
|
|
||||||
},
|
|
||||||
secondaryLinks: {
|
|
||||||
labelKey: secondaryLinksLabel,
|
|
||||||
transform: linkArrayJSONSchema.parse,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
[FieldMetadataType.PHONES]: {
|
[FieldMetadataType.PHONES]: {
|
||||||
primaryPhoneCountryCode: {
|
primaryPhoneCountryCode: castToString,
|
||||||
labelKey: primaryPhoneCountryCodeLabel,
|
primaryPhoneNumber: castToString,
|
||||||
transform: castToString,
|
primaryPhoneCallingCode: castToString,
|
||||||
},
|
additionalPhones: phoneArrayJSONSchema.parse,
|
||||||
primaryPhoneNumber: {
|
|
||||||
labelKey: primaryPhoneNumberLabel,
|
|
||||||
transform: castToString,
|
|
||||||
},
|
|
||||||
primaryPhoneCallingCode: {
|
|
||||||
labelKey: primaryPhoneCallingCodeLabel,
|
|
||||||
transform: castToString,
|
|
||||||
},
|
|
||||||
additionalPhones: {
|
|
||||||
labelKey: additionalPhonesLabel,
|
|
||||||
transform: phoneArrayJSONSchema.parse,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
[FieldMetadataType.RICH_TEXT_V2]: {
|
[FieldMetadataType.RICH_TEXT_V2]: {
|
||||||
blocknote: { labelKey: blocknoteLabel, transform: castToString },
|
blocknote: castToString,
|
||||||
markdown: { labelKey: markdownLabel, transform: castToString },
|
markdown: castToString,
|
||||||
},
|
},
|
||||||
|
|
||||||
[FieldMetadataType.EMAILS]: {
|
[FieldMetadataType.EMAILS]: {
|
||||||
primaryEmail: { labelKey: primaryEmailLabel, transform: castToString },
|
primaryEmail: castToString,
|
||||||
additionalEmails: {
|
additionalEmails: stringArrayJSONSchema.parse,
|
||||||
labelKey: additionalEmailsLabel,
|
|
||||||
transform: stringArrayJSONSchema.parse,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
[FieldMetadataType.FULL_NAME]: {
|
[FieldMetadataType.FULL_NAME]: {
|
||||||
firstName: { labelKey: firstNameLabel },
|
firstName: undefined,
|
||||||
lastName: { labelKey: lastNameLabel },
|
lastName: undefined,
|
||||||
},
|
},
|
||||||
[FieldMetadataType.ACTOR]: {
|
[FieldMetadataType.ACTOR]: {
|
||||||
source: { labelKey: 'source', transform: () => 'IMPORT' },
|
source: () => 'IMPORT',
|
||||||
context: { labelKey: 'context', transform: () => ({}) },
|
context: () => ({}),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -233,20 +152,83 @@ export const buildRecordFromImportedStructuredRow = ({
|
|||||||
case FieldMetadataType.CURRENCY:
|
case FieldMetadataType.CURRENCY:
|
||||||
case FieldMetadataType.ADDRESS:
|
case FieldMetadataType.ADDRESS:
|
||||||
case FieldMetadataType.LINKS:
|
case FieldMetadataType.LINKS:
|
||||||
case FieldMetadataType.PHONES:
|
|
||||||
case FieldMetadataType.RICH_TEXT_V2:
|
case FieldMetadataType.RICH_TEXT_V2:
|
||||||
case FieldMetadataType.EMAILS:
|
case FieldMetadataType.EMAILS:
|
||||||
case FieldMetadataType.FULL_NAME: {
|
case FieldMetadataType.FULL_NAME: {
|
||||||
const compositeData = buildCompositeFieldRecord(
|
const compositeData = buildCompositeFieldRecord(
|
||||||
field.name,
|
field,
|
||||||
importedStructuredRow,
|
importedStructuredRow,
|
||||||
COMPOSITE_FIELD_CONFIGS[field.type],
|
COMPOSITE_FIELD_TRANSFORM_CONFIGS[field.type],
|
||||||
);
|
);
|
||||||
if (isDefined(compositeData)) {
|
if (isDefined(compositeData)) {
|
||||||
recordToBuild[field.name] = compositeData;
|
recordToBuild[field.name] = compositeData;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case FieldMetadataType.PHONES: {
|
||||||
|
const compositeData = buildCompositeFieldRecord(
|
||||||
|
field,
|
||||||
|
importedStructuredRow,
|
||||||
|
COMPOSITE_FIELD_TRANSFORM_CONFIGS[field.type],
|
||||||
|
);
|
||||||
|
if (!isDefined(compositeData)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
recordToBuild[field.name] = compositeData;
|
||||||
|
|
||||||
|
const primaryPhoneNumber =
|
||||||
|
importedStructuredRow[
|
||||||
|
getSubFieldOptionKey(field, 'primaryPhoneNumber')
|
||||||
|
];
|
||||||
|
|
||||||
|
const primaryPhoneCallingCode =
|
||||||
|
importedStructuredRow[
|
||||||
|
getSubFieldOptionKey(field, 'primaryPhoneCallingCode')
|
||||||
|
];
|
||||||
|
|
||||||
|
const hasUserProvidedPrimaryPhoneNumberWithoutCallingCode =
|
||||||
|
isDefined(primaryPhoneNumber) &&
|
||||||
|
(!isDefined(primaryPhoneCallingCode) ||
|
||||||
|
!isNonEmptyString(primaryPhoneCallingCode));
|
||||||
|
|
||||||
|
// To meet backend requirements, handle case where user provides only a primaryPhoneNumber without calling code
|
||||||
|
if (hasUserProvidedPrimaryPhoneNumberWithoutCallingCode) {
|
||||||
|
const primaryPhoneCountryCode =
|
||||||
|
importedStructuredRow[
|
||||||
|
getSubFieldOptionKey(field, 'primaryPhoneCountryCode')
|
||||||
|
];
|
||||||
|
|
||||||
|
const hasUserProvidedPrimaryPhoneCountryCode =
|
||||||
|
isDefined(primaryPhoneCountryCode) &&
|
||||||
|
isNonEmptyString(primaryPhoneCountryCode);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
number: parsedNumber,
|
||||||
|
countryCallingCode: parsedCountryCallingCode,
|
||||||
|
} = parsePhoneNumberWithError(
|
||||||
|
primaryPhoneNumber as string,
|
||||||
|
hasUserProvidedPrimaryPhoneCountryCode
|
||||||
|
? (primaryPhoneCountryCode as CountryCode)
|
||||||
|
: undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
recordToBuild[field.name] = {
|
||||||
|
primaryPhoneNumber: parsedNumber,
|
||||||
|
primaryPhoneCallingCode: `+${parsedCountryCallingCode}`,
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
recordToBuild[field.name] = {
|
||||||
|
primaryPhoneNumber,
|
||||||
|
primaryPhoneCallingCode:
|
||||||
|
stripSimpleQuotesFromString(
|
||||||
|
field?.defaultValue?.primaryPhoneCallingCode,
|
||||||
|
) || '+1',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
case FieldMetadataType.BOOLEAN:
|
case FieldMetadataType.BOOLEAN:
|
||||||
recordToBuild[field.name] =
|
recordToBuild[field.name] =
|
||||||
importedFieldValue === 'true' || importedFieldValue === true;
|
importedFieldValue === 'true' || importedFieldValue === true;
|
||||||
|
|||||||
@ -4,7 +4,14 @@ import { emailSchema } from '@/object-record/record-field/validation-schemas/ema
|
|||||||
import { SpreadsheetImportFieldValidationDefinition } from '@/spreadsheet-import/types';
|
import { SpreadsheetImportFieldValidationDefinition } from '@/spreadsheet-import/types';
|
||||||
import { t } from '@lingui/core/macro';
|
import { t } from '@lingui/core/macro';
|
||||||
import { isDate, isString } from '@sniptt/guards';
|
import { isDate, isString } from '@sniptt/guards';
|
||||||
import { absoluteUrlSchema, isDefined, isValidUuid } from 'twenty-shared/utils';
|
import { parsePhoneNumberWithError } from 'libphonenumber-js';
|
||||||
|
import {
|
||||||
|
absoluteUrlSchema,
|
||||||
|
getCountryCodesForCallingCode,
|
||||||
|
isDefined,
|
||||||
|
isValidCountryCode,
|
||||||
|
isValidUuid,
|
||||||
|
} from 'twenty-shared/utils';
|
||||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
const getNumberValidationDefinition = (
|
const getNumberValidationDefinition = (
|
||||||
@ -16,6 +23,20 @@ const getNumberValidationDefinition = (
|
|||||||
level: 'error',
|
level: 'error',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isValidPhoneNumber = (value: string) => {
|
||||||
|
try {
|
||||||
|
return isDefined(
|
||||||
|
parsePhoneNumberWithError(value, { defaultCallingCode: '1' }),
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isValidCallingCode = (value: string) => {
|
||||||
|
return getCountryCodesForCallingCode(value).length > 0;
|
||||||
|
};
|
||||||
|
|
||||||
export const getSpreadSheetFieldValidationDefinitions = (
|
export const getSpreadSheetFieldValidationDefinitions = (
|
||||||
type: FieldMetadataType,
|
type: FieldMetadataType,
|
||||||
fieldName: string,
|
fieldName: string,
|
||||||
@ -143,9 +164,27 @@ export const getSpreadSheetFieldValidationDefinitions = (
|
|||||||
case 'primaryPhoneNumber':
|
case 'primaryPhoneNumber':
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
rule: 'regex',
|
rule: 'function',
|
||||||
value: '^[0-9]+$',
|
isValid: isValidPhoneNumber,
|
||||||
errorMessage: `${fieldName} ${t`must contain only numbers`}`,
|
errorMessage: `${fieldName} ${t`is not a valid phone number`}`,
|
||||||
|
level: 'error',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
case 'primaryPhoneCallingCode':
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
rule: 'function',
|
||||||
|
isValid: isValidCallingCode,
|
||||||
|
errorMessage: `${fieldName} ${t`is not a valid calling code`}`,
|
||||||
|
level: 'error',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
case 'primaryPhoneCountryCode':
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
rule: 'function',
|
||||||
|
isValid: isValidCountryCode,
|
||||||
|
errorMessage: `${fieldName} ${t`is not a valid country code`}`,
|
||||||
level: 'error',
|
level: 'error',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@ -165,10 +204,9 @@ export const getSpreadSheetFieldValidationDefinitions = (
|
|||||||
callingCode: string;
|
callingCode: string;
|
||||||
countryCode: string;
|
countryCode: string;
|
||||||
}) =>
|
}) =>
|
||||||
isDefined(phone.number) &&
|
isValidPhoneNumber(phone.number) &&
|
||||||
/^[0-9]+$/.test(phone.number) &&
|
isValidCallingCode(phone.callingCode) &&
|
||||||
isDefined(phone.callingCode) &&
|
isValidCountryCode(phone.countryCode),
|
||||||
isDefined(phone.countryCode),
|
|
||||||
);
|
);
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@ -2,13 +2,12 @@ import { t } from '@lingui/core/macro';
|
|||||||
import { isNonEmptyString } from '@sniptt/guards';
|
import { isNonEmptyString } from '@sniptt/guards';
|
||||||
import {
|
import {
|
||||||
CountryCallingCode,
|
CountryCallingCode,
|
||||||
CountryCode,
|
|
||||||
getCountries,
|
|
||||||
getCountryCallingCode,
|
|
||||||
parsePhoneNumberWithError,
|
parsePhoneNumberWithError,
|
||||||
} from 'libphonenumber-js';
|
} from 'libphonenumber-js';
|
||||||
import {
|
import {
|
||||||
|
getCountryCodesForCallingCode,
|
||||||
isDefined,
|
isDefined,
|
||||||
|
isValidCountryCode,
|
||||||
parseJson,
|
parseJson,
|
||||||
removeUndefinedFields,
|
removeUndefinedFields,
|
||||||
} from 'twenty-shared/utils';
|
} from 'twenty-shared/utils';
|
||||||
@ -22,8 +21,6 @@ import {
|
|||||||
PhonesMetadata,
|
PhonesMetadata,
|
||||||
} from 'src/engine/metadata-modules/field-metadata/composite-types/phones.composite-type';
|
} from 'src/engine/metadata-modules/field-metadata/composite-types/phones.composite-type';
|
||||||
|
|
||||||
const ALL_COUNTRIES_CODE = getCountries();
|
|
||||||
|
|
||||||
export type PhonesFieldGraphQLInput =
|
export type PhonesFieldGraphQLInput =
|
||||||
| Partial<
|
| Partial<
|
||||||
Omit<PhonesMetadata, 'additionalPhones'> & {
|
Omit<PhonesMetadata, 'additionalPhones'> & {
|
||||||
@ -38,22 +35,6 @@ type AdditionalPhoneMetadataWithNumber = Partial<AdditionalPhoneMetadata> &
|
|||||||
|
|
||||||
const removePlusFromString = (str: string) => str.replace(/\+/g, '');
|
const removePlusFromString = (str: string) => str.replace(/\+/g, '');
|
||||||
|
|
||||||
const isValidCountryCode = (input: string): input is CountryCode => {
|
|
||||||
return ALL_COUNTRIES_CODE.includes(input as unknown as CountryCode);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getCountryCodesForCallingCode = (callingCode: string) => {
|
|
||||||
const cleanCallingCode = callingCode.startsWith('+')
|
|
||||||
? callingCode.slice(1)
|
|
||||||
: callingCode;
|
|
||||||
|
|
||||||
return ALL_COUNTRIES_CODE.filter((country) => {
|
|
||||||
const countryCallingCode = getCountryCallingCode(country);
|
|
||||||
|
|
||||||
return countryCallingCode === cleanCallingCode;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const validatePrimaryPhoneCountryCodeAndCallingCode = ({
|
const validatePrimaryPhoneCountryCodeAndCallingCode = ({
|
||||||
callingCode,
|
callingCode,
|
||||||
countryCode,
|
countryCode,
|
||||||
|
|||||||
@ -22,6 +22,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sniptt/guards": "^0.2.0",
|
"@sniptt/guards": "^0.2.0",
|
||||||
|
"libphonenumber-js": "^1.10.26",
|
||||||
"zod": "3.23.8"
|
"zod": "3.23.8"
|
||||||
},
|
},
|
||||||
"preconstruct": {
|
"preconstruct": {
|
||||||
|
|||||||
@ -33,3 +33,5 @@ export { isValidLocale } from './validation/isValidLocale';
|
|||||||
export { isValidUuid } from './validation/isValidUuid';
|
export { isValidUuid } from './validation/isValidUuid';
|
||||||
export { isValidVariable } from './validation/isValidVariable';
|
export { isValidVariable } from './validation/isValidVariable';
|
||||||
export { normalizeLocale } from './validation/normalizeLocale';
|
export { normalizeLocale } from './validation/normalizeLocale';
|
||||||
|
export { getCountryCodesForCallingCode } from './validation/phones-value/getCountryCodesForCallingCode';
|
||||||
|
export { isValidCountryCode } from './validation/phones-value/isValidCountryCode';
|
||||||
|
|||||||
@ -0,0 +1,15 @@
|
|||||||
|
import { getCountries, getCountryCallingCode } from 'libphonenumber-js';
|
||||||
|
|
||||||
|
const ALL_COUNTRIES_CODE = getCountries();
|
||||||
|
|
||||||
|
export const getCountryCodesForCallingCode = (callingCode: string) => {
|
||||||
|
const cleanCallingCode = callingCode.startsWith('+')
|
||||||
|
? callingCode.slice(1)
|
||||||
|
: callingCode;
|
||||||
|
|
||||||
|
return ALL_COUNTRIES_CODE.filter((country) => {
|
||||||
|
const countryCallingCode = getCountryCallingCode(country);
|
||||||
|
|
||||||
|
return countryCallingCode === cleanCallingCode;
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from './isValidCountryCode';
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
import { CountryCode, getCountries } from 'libphonenumber-js';
|
||||||
|
|
||||||
|
const ALL_COUNTRIES_CODE = getCountries();
|
||||||
|
|
||||||
|
export const isValidCountryCode = (input: string): input is CountryCode => {
|
||||||
|
return ALL_COUNTRIES_CODE.includes(input as unknown as CountryCode);
|
||||||
|
};
|
||||||
@ -56856,6 +56856,7 @@ __metadata:
|
|||||||
"@types/babel__preset-env": "npm:^7"
|
"@types/babel__preset-env": "npm:^7"
|
||||||
babel-plugin-module-resolver: "npm:^5.0.2"
|
babel-plugin-module-resolver: "npm:^5.0.2"
|
||||||
glob: "npm:^11.0.1"
|
glob: "npm:^11.0.1"
|
||||||
|
libphonenumber-js: "npm:^1.10.26"
|
||||||
tsx: "npm:^4.19.3"
|
tsx: "npm:^4.19.3"
|
||||||
zod: "npm:3.23.8"
|
zod: "npm:3.23.8"
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
|
|||||||
Reference in New Issue
Block a user