@nixxie-cms/fields-phone 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nixxie International DMCC
4
+ Portions Copyright (c) 2023 Thinkmill Labs Pty Ltd and contributors
5
+ (this software is derived from the KeystoneJS project, https://keystonejs.com)
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in all
15
+ copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,21 @@
1
+ # @nixxie-cms/fields-phone
2
+
3
+ A phone number field for Nixxie CMS. Stores E.164-normalised values (`+14155552671`): spaces,
4
+ dashes, dots and parentheses are stripped, a leading `00` becomes `+`, and national numbers are
5
+ promoted using `defaultCountryCode`. Invalid numbers are rejected with a validation error.
6
+
7
+ ```ts
8
+ import { phone } from '@nixxie-cms/fields-phone'
9
+
10
+ fields: {
11
+ mobile: phone({
12
+ defaultCountryCode: '+92', // '0300 1234567' → '+923001234567'
13
+ validation: { isRequired: true },
14
+ isIndexed: 'unique',
15
+ }),
16
+ }
17
+ ```
18
+
19
+ Also exports `formatPhone(e164)` for display — best-effort grouping (`+1 415 555 2671`): the
20
+ country code is inferred from a small built-in table and the rest is split into groups of 3–4
21
+ digits. It does not apply national formatting conventions; invalid input is returned unchanged.
@@ -0,0 +1,32 @@
1
+ import type { TextFieldConfig } from '@nixxie-cms/core/fields';
2
+ import type { BaseCollectionTypeInfo, FieldTypeFunc } from '@nixxie-cms/core/types';
3
+ /**
4
+ * Best-effort display formatting for an E.164 number: the country code, a space, then the
5
+ * remaining digits in groups of 3 (the final group absorbs a would-be lone digit, giving 3–4).
6
+ * E.g. `+14155552671` → `+1 415 555 2671`, `+923001234567` → `+92 300 123 4567`. The country
7
+ * code length is inferred from a small built-in table, not a full numbering plan, and no
8
+ * national conventions (area-code parentheses etc.) are applied. Input that is not valid E.164
9
+ * is returned unchanged.
10
+ */
11
+ export declare function formatPhone(e164: string): string;
12
+ export type PhoneFieldConfig<CollectionTypeInfo extends BaseCollectionTypeInfo> = Omit<TextFieldConfig<CollectionTypeInfo>, 'validation'> & {
13
+ validation?: {
14
+ /** Reject saving when no value is present. */
15
+ isRequired?: boolean;
16
+ };
17
+ /**
18
+ * Country calling code (e.g. `'+92'`) prepended when the user enters a national number —
19
+ * one without a leading `+`. A single leading `0` (the national trunk prefix) is stripped
20
+ * first, so with `'+92'` both `03001234567` and `300 123 4567` become `+923001234567`.
21
+ */
22
+ defaultCountryCode?: string;
23
+ };
24
+ /**
25
+ * A phone number field storing E.164-normalised values (`+` followed by up to 15 digits). Built
26
+ * on the core `text` field: input is cleaned of spaces, dashes, dots and parentheses, a leading
27
+ * `00` becomes `+`, and national numbers are promoted via `defaultCountryCode`. Anything that
28
+ * still isn't valid E.164 is rejected with a validation error. Empty/null values pass through
29
+ * untouched. `isIndexed: 'unique'` is supported via the underlying text field.
30
+ */
31
+ export declare function phone<CollectionTypeInfo extends BaseCollectionTypeInfo>(config?: PhoneFieldConfig<CollectionTypeInfo>): FieldTypeFunc<CollectionTypeInfo>;
32
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"../../../src","sources":["index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAA;AAC9D,OAAO,KAAK,EAAE,sBAAsB,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AAenF;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAehD;AAgBD,MAAM,MAAM,gBAAgB,CAAC,kBAAkB,SAAS,sBAAsB,IAAI,IAAI,CACpF,eAAe,CAAC,kBAAkB,CAAC,EACnC,YAAY,CACb,GAAG;IACF,UAAU,CAAC,EAAE;QACX,8CAA8C;QAC9C,UAAU,CAAC,EAAE,OAAO,CAAA;KACrB,CAAA;IACD;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAA;CAC5B,CAAA;AAED;;;;;;GAMG;AACH,wBAAgB,KAAK,CAAC,kBAAkB,SAAS,sBAAsB,EACrE,MAAM,GAAE,gBAAgB,CAAC,kBAAkB,CAAM,GAChD,aAAa,CAAC,kBAAkB,CAAC,CAgCnC"}
@@ -0,0 +1,2 @@
1
+ export * from "./declarations/src/index.js";
2
+ //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibml4eGllLWNtcy1maWVsZHMtcGhvbmUuY2pzLmQudHMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuL2RlY2xhcmF0aW9ucy9zcmMvaW5kZXguZC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSJ9
@@ -0,0 +1,105 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var fields = require('@nixxie-cms/core/fields');
6
+
7
+ /** E.164: a leading `+`, a country code starting 1–9, and 7–15 digits in total. */
8
+ const E164 = /^\+[1-9]\d{6,14}$/;
9
+
10
+ /** Country calling codes that are a single digit (NANP and Russia/Kazakhstan). */
11
+ const ONE_DIGIT_CODES = new Set(['1', '7']);
12
+
13
+ /** The ITU-assigned two-digit country calling codes — anything else is three digits. */
14
+ const TWO_DIGIT_CODES = new Set(['20', '27', '30', '31', '32', '33', '34', '36', '39', '40', '41', '43', '44', '45', '46', '47', '48', '49', '51', '52', '53', '54', '55', '56', '57', '58', '60', '61', '62', '63', '64', '65', '66', '81', '82', '84', '86', '90', '91', '92', '93', '94', '95', '98']);
15
+
16
+ /**
17
+ * Best-effort display formatting for an E.164 number: the country code, a space, then the
18
+ * remaining digits in groups of 3 (the final group absorbs a would-be lone digit, giving 3–4).
19
+ * E.g. `+14155552671` → `+1 415 555 2671`, `+923001234567` → `+92 300 123 4567`. The country
20
+ * code length is inferred from a small built-in table, not a full numbering plan, and no
21
+ * national conventions (area-code parentheses etc.) are applied. Input that is not valid E.164
22
+ * is returned unchanged.
23
+ */
24
+ function formatPhone(e164) {
25
+ if (!E164.test(e164)) return e164;
26
+ const digits = e164.slice(1);
27
+ let ccLength = 3;
28
+ if (ONE_DIGIT_CODES.has(digits[0])) ccLength = 1;else if (TWO_DIGIT_CODES.has(digits.slice(0, 2))) ccLength = 2;
29
+ const countryCode = digits.slice(0, ccLength);
30
+ const rest = digits.slice(ccLength);
31
+ const groups = [];
32
+ for (let i = 0; i < rest.length; i += 3) groups.push(rest.slice(i, i + 3));
33
+ // Avoid a trailing single digit — merge it into the previous group (3 + 1 → 4).
34
+ if (groups.length > 1 && groups[groups.length - 1].length === 1) {
35
+ groups.splice(groups.length - 2, 2, groups[groups.length - 2] + groups[groups.length - 1]);
36
+ }
37
+ return [`+${countryCode}`, ...groups].join(' ');
38
+ }
39
+
40
+ /**
41
+ * Normalise user input towards E.164: strip formatting characters, convert the international
42
+ * `00` prefix to `+`, and — when a default country code is configured — promote a national
43
+ * number (no `+`, optional `0` trunk prefix) to international form.
44
+ */
45
+ function normalisePhone(value, defaultCountryCode) {
46
+ let v = value.replace(/[\s\-.()]/g, '');
47
+ if (v.startsWith('00')) v = `+${v.slice(2)}`;
48
+ if (!v.startsWith('+') && defaultCountryCode) {
49
+ v = defaultCountryCode + v.replace(/^0/, '');
50
+ }
51
+ return v;
52
+ }
53
+ /**
54
+ * A phone number field storing E.164-normalised values (`+` followed by up to 15 digits). Built
55
+ * on the core `text` field: input is cleaned of spaces, dashes, dots and parentheses, a leading
56
+ * `00` becomes `+`, and national numbers are promoted via `defaultCountryCode`. Anything that
57
+ * still isn't valid E.164 is rejected with a validation error. Empty/null values pass through
58
+ * untouched. `isIndexed: 'unique'` is supported via the underlying text field.
59
+ */
60
+ function phone(config = {}) {
61
+ const {
62
+ defaultCountryCode,
63
+ hooks,
64
+ validation,
65
+ ...rest
66
+ } = config;
67
+ const userValidate = hooks === null || hooks === void 0 ? void 0 : hooks.validate;
68
+ return fields.text({
69
+ ...rest,
70
+ validation: {
71
+ isRequired: validation === null || validation === void 0 ? void 0 : validation.isRequired
72
+ },
73
+ ui: {
74
+ description: 'International phone number, e.g. +14155552671',
75
+ ...rest.ui
76
+ },
77
+ hooks: {
78
+ ...hooks,
79
+ resolveInput: args => {
80
+ const value = args.resolvedData[args.fieldKey];
81
+ if (typeof value !== 'string' || value.length === 0) return value;
82
+ return normalisePhone(value, defaultCountryCode);
83
+ },
84
+ validate: async args => {
85
+ var _userValidate;
86
+ // Preserve any user-supplied validate hook (function or { create, update, delete } form).
87
+ if (typeof userValidate === 'function') await userValidate(args);else await (userValidate === null || userValidate === void 0 || (_userValidate = userValidate[args.operation]) === null || _userValidate === void 0 ? void 0 : _userValidate.call(userValidate, args));
88
+ const {
89
+ resolvedData,
90
+ fieldKey,
91
+ addValidationError,
92
+ operation
93
+ } = args;
94
+ if (operation === 'delete') return;
95
+ const value = resolvedData[fieldKey];
96
+ if (typeof value === 'string' && value.length > 0 && !E164.test(value)) {
97
+ addValidationError('Must be a valid international phone number (E.164, e.g. +14155552671)');
98
+ }
99
+ }
100
+ }
101
+ });
102
+ }
103
+
104
+ exports.formatPhone = formatPhone;
105
+ exports.phone = phone;
@@ -0,0 +1,100 @@
1
+ import { text } from '@nixxie-cms/core/fields';
2
+
3
+ /** E.164: a leading `+`, a country code starting 1–9, and 7–15 digits in total. */
4
+ const E164 = /^\+[1-9]\d{6,14}$/;
5
+
6
+ /** Country calling codes that are a single digit (NANP and Russia/Kazakhstan). */
7
+ const ONE_DIGIT_CODES = new Set(['1', '7']);
8
+
9
+ /** The ITU-assigned two-digit country calling codes — anything else is three digits. */
10
+ const TWO_DIGIT_CODES = new Set(['20', '27', '30', '31', '32', '33', '34', '36', '39', '40', '41', '43', '44', '45', '46', '47', '48', '49', '51', '52', '53', '54', '55', '56', '57', '58', '60', '61', '62', '63', '64', '65', '66', '81', '82', '84', '86', '90', '91', '92', '93', '94', '95', '98']);
11
+
12
+ /**
13
+ * Best-effort display formatting for an E.164 number: the country code, a space, then the
14
+ * remaining digits in groups of 3 (the final group absorbs a would-be lone digit, giving 3–4).
15
+ * E.g. `+14155552671` → `+1 415 555 2671`, `+923001234567` → `+92 300 123 4567`. The country
16
+ * code length is inferred from a small built-in table, not a full numbering plan, and no
17
+ * national conventions (area-code parentheses etc.) are applied. Input that is not valid E.164
18
+ * is returned unchanged.
19
+ */
20
+ function formatPhone(e164) {
21
+ if (!E164.test(e164)) return e164;
22
+ const digits = e164.slice(1);
23
+ let ccLength = 3;
24
+ if (ONE_DIGIT_CODES.has(digits[0])) ccLength = 1;else if (TWO_DIGIT_CODES.has(digits.slice(0, 2))) ccLength = 2;
25
+ const countryCode = digits.slice(0, ccLength);
26
+ const rest = digits.slice(ccLength);
27
+ const groups = [];
28
+ for (let i = 0; i < rest.length; i += 3) groups.push(rest.slice(i, i + 3));
29
+ // Avoid a trailing single digit — merge it into the previous group (3 + 1 → 4).
30
+ if (groups.length > 1 && groups[groups.length - 1].length === 1) {
31
+ groups.splice(groups.length - 2, 2, groups[groups.length - 2] + groups[groups.length - 1]);
32
+ }
33
+ return [`+${countryCode}`, ...groups].join(' ');
34
+ }
35
+
36
+ /**
37
+ * Normalise user input towards E.164: strip formatting characters, convert the international
38
+ * `00` prefix to `+`, and — when a default country code is configured — promote a national
39
+ * number (no `+`, optional `0` trunk prefix) to international form.
40
+ */
41
+ function normalisePhone(value, defaultCountryCode) {
42
+ let v = value.replace(/[\s\-.()]/g, '');
43
+ if (v.startsWith('00')) v = `+${v.slice(2)}`;
44
+ if (!v.startsWith('+') && defaultCountryCode) {
45
+ v = defaultCountryCode + v.replace(/^0/, '');
46
+ }
47
+ return v;
48
+ }
49
+ /**
50
+ * A phone number field storing E.164-normalised values (`+` followed by up to 15 digits). Built
51
+ * on the core `text` field: input is cleaned of spaces, dashes, dots and parentheses, a leading
52
+ * `00` becomes `+`, and national numbers are promoted via `defaultCountryCode`. Anything that
53
+ * still isn't valid E.164 is rejected with a validation error. Empty/null values pass through
54
+ * untouched. `isIndexed: 'unique'` is supported via the underlying text field.
55
+ */
56
+ function phone(config = {}) {
57
+ const {
58
+ defaultCountryCode,
59
+ hooks,
60
+ validation,
61
+ ...rest
62
+ } = config;
63
+ const userValidate = hooks === null || hooks === void 0 ? void 0 : hooks.validate;
64
+ return text({
65
+ ...rest,
66
+ validation: {
67
+ isRequired: validation === null || validation === void 0 ? void 0 : validation.isRequired
68
+ },
69
+ ui: {
70
+ description: 'International phone number, e.g. +14155552671',
71
+ ...rest.ui
72
+ },
73
+ hooks: {
74
+ ...hooks,
75
+ resolveInput: args => {
76
+ const value = args.resolvedData[args.fieldKey];
77
+ if (typeof value !== 'string' || value.length === 0) return value;
78
+ return normalisePhone(value, defaultCountryCode);
79
+ },
80
+ validate: async args => {
81
+ var _userValidate;
82
+ // Preserve any user-supplied validate hook (function or { create, update, delete } form).
83
+ if (typeof userValidate === 'function') await userValidate(args);else await (userValidate === null || userValidate === void 0 || (_userValidate = userValidate[args.operation]) === null || _userValidate === void 0 ? void 0 : _userValidate.call(userValidate, args));
84
+ const {
85
+ resolvedData,
86
+ fieldKey,
87
+ addValidationError,
88
+ operation
89
+ } = args;
90
+ if (operation === 'delete') return;
91
+ const value = resolvedData[fieldKey];
92
+ if (typeof value === 'string' && value.length > 0 && !E164.test(value)) {
93
+ addValidationError('Must be a valid international phone number (E.164, e.g. +14155552671)');
94
+ }
95
+ }
96
+ }
97
+ });
98
+ }
99
+
100
+ export { formatPhone, phone };
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@nixxie-cms/fields-phone",
3
+ "version": "1.0.0",
4
+ "license": "MIT",
5
+ "main": "dist/nixxie-cms-fields-phone.cjs.js",
6
+ "module": "dist/nixxie-cms-fields-phone.esm.js",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/nixxie-cms-fields-phone.cjs.js",
10
+ "module": "./dist/nixxie-cms-fields-phone.esm.js",
11
+ "default": "./dist/nixxie-cms-fields-phone.cjs.js"
12
+ },
13
+ "./package.json": "./package.json"
14
+ },
15
+ "dependencies": {
16
+ "@babel/runtime": "^7.24.7"
17
+ },
18
+ "devDependencies": {
19
+ "@nixxie-cms/core": "^1.1.0"
20
+ },
21
+ "peerDependencies": {
22
+ "@nixxie-cms/core": "^1.0.1"
23
+ },
24
+ "preconstruct": {
25
+ "entrypoints": [
26
+ "index.ts"
27
+ ]
28
+ },
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "https://github.com/nixxiecms/nixxie/tree/main/packages/fields-phone"
32
+ }
33
+ }
package/src/index.ts ADDED
@@ -0,0 +1,114 @@
1
+ import { text } from '@nixxie-cms/core/fields'
2
+ import type { TextFieldConfig } from '@nixxie-cms/core/fields'
3
+ import type { BaseCollectionTypeInfo, FieldTypeFunc } from '@nixxie-cms/core/types'
4
+
5
+ /** E.164: a leading `+`, a country code starting 1–9, and 7–15 digits in total. */
6
+ const E164 = /^\+[1-9]\d{6,14}$/
7
+
8
+ /** Country calling codes that are a single digit (NANP and Russia/Kazakhstan). */
9
+ const ONE_DIGIT_CODES = new Set(['1', '7'])
10
+
11
+ /** The ITU-assigned two-digit country calling codes — anything else is three digits. */
12
+ const TWO_DIGIT_CODES = new Set([
13
+ '20', '27', '30', '31', '32', '33', '34', '36', '39', '40', '41', '43', '44', '45', '46', '47',
14
+ '48', '49', '51', '52', '53', '54', '55', '56', '57', '58', '60', '61', '62', '63', '64', '65',
15
+ '66', '81', '82', '84', '86', '90', '91', '92', '93', '94', '95', '98',
16
+ ])
17
+
18
+ /**
19
+ * Best-effort display formatting for an E.164 number: the country code, a space, then the
20
+ * remaining digits in groups of 3 (the final group absorbs a would-be lone digit, giving 3–4).
21
+ * E.g. `+14155552671` → `+1 415 555 2671`, `+923001234567` → `+92 300 123 4567`. The country
22
+ * code length is inferred from a small built-in table, not a full numbering plan, and no
23
+ * national conventions (area-code parentheses etc.) are applied. Input that is not valid E.164
24
+ * is returned unchanged.
25
+ */
26
+ export function formatPhone(e164: string): string {
27
+ if (!E164.test(e164)) return e164
28
+ const digits = e164.slice(1)
29
+ let ccLength = 3
30
+ if (ONE_DIGIT_CODES.has(digits[0])) ccLength = 1
31
+ else if (TWO_DIGIT_CODES.has(digits.slice(0, 2))) ccLength = 2
32
+ const countryCode = digits.slice(0, ccLength)
33
+ const rest = digits.slice(ccLength)
34
+ const groups: string[] = []
35
+ for (let i = 0; i < rest.length; i += 3) groups.push(rest.slice(i, i + 3))
36
+ // Avoid a trailing single digit — merge it into the previous group (3 + 1 → 4).
37
+ if (groups.length > 1 && groups[groups.length - 1].length === 1) {
38
+ groups.splice(groups.length - 2, 2, groups[groups.length - 2] + groups[groups.length - 1])
39
+ }
40
+ return [`+${countryCode}`, ...groups].join(' ')
41
+ }
42
+
43
+ /**
44
+ * Normalise user input towards E.164: strip formatting characters, convert the international
45
+ * `00` prefix to `+`, and — when a default country code is configured — promote a national
46
+ * number (no `+`, optional `0` trunk prefix) to international form.
47
+ */
48
+ function normalisePhone(value: string, defaultCountryCode?: string): string {
49
+ let v = value.replace(/[\s\-.()]/g, '')
50
+ if (v.startsWith('00')) v = `+${v.slice(2)}`
51
+ if (!v.startsWith('+') && defaultCountryCode) {
52
+ v = defaultCountryCode + v.replace(/^0/, '')
53
+ }
54
+ return v
55
+ }
56
+
57
+ export type PhoneFieldConfig<CollectionTypeInfo extends BaseCollectionTypeInfo> = Omit<
58
+ TextFieldConfig<CollectionTypeInfo>,
59
+ 'validation'
60
+ > & {
61
+ validation?: {
62
+ /** Reject saving when no value is present. */
63
+ isRequired?: boolean
64
+ }
65
+ /**
66
+ * Country calling code (e.g. `'+92'`) prepended when the user enters a national number —
67
+ * one without a leading `+`. A single leading `0` (the national trunk prefix) is stripped
68
+ * first, so with `'+92'` both `03001234567` and `300 123 4567` become `+923001234567`.
69
+ */
70
+ defaultCountryCode?: string
71
+ }
72
+
73
+ /**
74
+ * A phone number field storing E.164-normalised values (`+` followed by up to 15 digits). Built
75
+ * on the core `text` field: input is cleaned of spaces, dashes, dots and parentheses, a leading
76
+ * `00` becomes `+`, and national numbers are promoted via `defaultCountryCode`. Anything that
77
+ * still isn't valid E.164 is rejected with a validation error. Empty/null values pass through
78
+ * untouched. `isIndexed: 'unique'` is supported via the underlying text field.
79
+ */
80
+ export function phone<CollectionTypeInfo extends BaseCollectionTypeInfo>(
81
+ config: PhoneFieldConfig<CollectionTypeInfo> = {}
82
+ ): FieldTypeFunc<CollectionTypeInfo> {
83
+ const { defaultCountryCode, hooks, validation, ...rest } = config
84
+ const userValidate = hooks?.validate as
85
+ | ((args: any) => unknown)
86
+ | { create?: (args: any) => unknown; update?: (args: any) => unknown; delete?: (args: any) => unknown }
87
+ | undefined
88
+
89
+ return text<CollectionTypeInfo>({
90
+ ...(rest as TextFieldConfig<CollectionTypeInfo>),
91
+ validation: { isRequired: validation?.isRequired },
92
+ ui: { description: 'International phone number, e.g. +14155552671', ...rest.ui },
93
+ hooks: {
94
+ ...hooks,
95
+ resolveInput: (args: any) => {
96
+ const value = args.resolvedData[args.fieldKey]
97
+ if (typeof value !== 'string' || value.length === 0) return value
98
+ return normalisePhone(value, defaultCountryCode)
99
+ },
100
+ validate: async (args: any) => {
101
+ // Preserve any user-supplied validate hook (function or { create, update, delete } form).
102
+ if (typeof userValidate === 'function') await userValidate(args)
103
+ else await userValidate?.[args.operation as 'create' | 'update' | 'delete']?.(args)
104
+
105
+ const { resolvedData, fieldKey, addValidationError, operation } = args
106
+ if (operation === 'delete') return
107
+ const value = resolvedData[fieldKey]
108
+ if (typeof value === 'string' && value.length > 0 && !E164.test(value)) {
109
+ addValidationError('Must be a valid international phone number (E.164, e.g. +14155552671)')
110
+ }
111
+ },
112
+ },
113
+ })
114
+ }