@tellescope/validation 0.0.40 → 0.0.44
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/lib/cjs/validation.d.ts +20 -1
- package/lib/cjs/validation.d.ts.map +1 -1
- package/lib/cjs/validation.js +124 -9
- package/lib/cjs/validation.js.map +1 -1
- package/lib/esm/validation.d.ts +20 -1
- package/lib/esm/validation.d.ts.map +1 -1
- package/lib/esm/validation.js +121 -9
- package/lib/esm/validation.js.map +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -7
- package/src/validation.ts +136 -10
package/src/validation.ts
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
AttendeeInfo,
|
|
28
28
|
MeetingInfo,
|
|
29
29
|
CUDSubscription,
|
|
30
|
+
FormField,
|
|
30
31
|
} from "@tellescope/types-models"
|
|
31
32
|
import {
|
|
32
33
|
UserDisplayInfo,
|
|
@@ -48,6 +49,8 @@ import isBoolean from "validator/lib/isBoolean" // better for tree-shaking in mo
|
|
|
48
49
|
// } from "@tellescope/constants"
|
|
49
50
|
|
|
50
51
|
import {
|
|
52
|
+
filter_object,
|
|
53
|
+
is_defined,
|
|
51
54
|
is_object,
|
|
52
55
|
is_whitespace,
|
|
53
56
|
object_is_empty,
|
|
@@ -110,7 +113,7 @@ export const build_validator: BuildValidator_T = (escapeFunction, options={} as
|
|
|
110
113
|
return (fieldValue: JSONType) => {
|
|
111
114
|
if (isOptional && fieldValue === undefined) return undefined
|
|
112
115
|
if (nullOk && fieldValue === null) return null
|
|
113
|
-
if (emptyStringOk && fieldValue === '') return ''
|
|
116
|
+
if ((emptyStringOk || isOptional) && fieldValue === '') return ''
|
|
114
117
|
if (!emptyStringOk && fieldValue === '') throw `Expecting non-empty string but got ${fieldValue}`
|
|
115
118
|
if (isObject && typeof fieldValue !== 'object') {
|
|
116
119
|
try {
|
|
@@ -132,7 +135,7 @@ export const build_validator: BuildValidator_T = (escapeFunction, options={} as
|
|
|
132
135
|
|
|
133
136
|
if (listOf && (fieldValue as JSONType[])?.length === 0) {
|
|
134
137
|
if (emptyListOk) return []
|
|
135
|
-
else throw new Error("Expecting a list of values but
|
|
138
|
+
else throw new Error("Expecting a list of values but list is empty")
|
|
136
139
|
}
|
|
137
140
|
|
|
138
141
|
if (toLower && typeof fieldValue === 'string') {
|
|
@@ -395,8 +398,8 @@ export const numberValidatorBuilder: ComplexEscapeBuilder<{ lower: number, upper
|
|
|
395
398
|
)
|
|
396
399
|
}
|
|
397
400
|
|
|
398
|
-
export const nonNegNumberValidator = numberValidatorBuilder({ lower: 0, upper:
|
|
399
|
-
export const numberValidator = numberValidatorBuilder({ lower: -100000000, upper:
|
|
401
|
+
export const nonNegNumberValidator = numberValidatorBuilder({ lower: 0, upper: 10000000000000 }) // max is 2286 in UTC MS
|
|
402
|
+
export const numberValidator = numberValidatorBuilder({ lower: -100000000, upper: 10000000000000 }) // max is 2286 in UTC MS
|
|
400
403
|
export const fileSizeValidator = numberValidatorBuilder({ lower: 0, upper: MAX_FILE_SIZE })
|
|
401
404
|
|
|
402
405
|
export const dateValidator: EscapeBuilder<Date> = (options={}) => build_validator(
|
|
@@ -450,16 +453,17 @@ export const phoneValidator: EscapeBuilder<string> = (options={}) => build_valid
|
|
|
450
453
|
phone => {
|
|
451
454
|
if (typeof phone !== "string") throw new Error(`Expecting phone to be string but got ${phone}`)
|
|
452
455
|
|
|
453
|
-
|
|
454
|
-
throw `Invalid phone number`
|
|
455
|
-
}
|
|
456
|
-
let escaped = escape_phone_number(phone)
|
|
457
|
-
|
|
456
|
+
let escaped = escape_phone_number(phone)
|
|
458
457
|
if (escaped.length < 10) throw new Error(`Phone number must be at least 10 digits`)
|
|
459
458
|
|
|
460
459
|
escaped = escaped.startsWith('+') ? escaped
|
|
461
460
|
: escaped.length === 10 ? '+1' + escaped // assume US country code for now
|
|
462
|
-
: "+"
|
|
461
|
+
: "+" + escaped // assume country code provided, but missing leading +
|
|
462
|
+
|
|
463
|
+
if (!isMobilePhone(escaped, 'any', { strictMode: true })) {
|
|
464
|
+
throw `Invalid phone number`
|
|
465
|
+
}
|
|
466
|
+
|
|
463
467
|
return escaped
|
|
464
468
|
},
|
|
465
469
|
{ ...options, maxLength: 25, listOf: false }
|
|
@@ -704,6 +708,128 @@ export const rejectionWithMessage: EscapeBuilder<undefined> = o => build_validat
|
|
|
704
708
|
export const numberToDateValidator = indexableNumberValidator(numberValidator(), dateValidator())
|
|
705
709
|
export const idStringToDateValidator = indexableValidator(mongoIdStringValidator(), dateValidator())
|
|
706
710
|
|
|
711
|
+
// todo: move preference to FIELD_TYPES with drop-down option in user-facing forms
|
|
712
|
+
const FIELD_TYPES = ['string', 'number', 'email', 'phone', 'multiple_choice', 'file', 'signature']
|
|
713
|
+
const VALIDATE_OPTIONS_FOR_FIELD_TYPES = {
|
|
714
|
+
'multiple_choice': {
|
|
715
|
+
choices: listOfStringsValidator({ maxLength: 100, errorMessage: "Multiple choice options must be under 100 characters, and you must have at least one option." }),
|
|
716
|
+
radio: booleanValidator({ errorMessage: "radio must be a boolean" }),
|
|
717
|
+
other: booleanValidator({ isOptional: true, errorMessage: "other must be a boolean" }),
|
|
718
|
+
REQUIRED: ['choices', 'radio'],
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
export const RESERVED_INTAKE_FIELDS = ['_id', 'id', 'externalId', 'phoneConsent', 'emailConsent', 'tags', 'journeys', 'updatedAt', 'preference', 'assignedTo', 'lastCommunication']
|
|
722
|
+
|
|
723
|
+
export const ENDUSER_FIELD_TYPES = {
|
|
724
|
+
'email': 'email',
|
|
725
|
+
'phone': 'phone',
|
|
726
|
+
'fname': 'string',
|
|
727
|
+
'lname': 'string',
|
|
728
|
+
}
|
|
729
|
+
export const INTERNAL_NAME_TO_DISPLAY_FIELD = {
|
|
730
|
+
"string": 'Text',
|
|
731
|
+
"number": 'Number',
|
|
732
|
+
"email": "Email",
|
|
733
|
+
"phone": "Phone Number",
|
|
734
|
+
multiple_choice: "Multiple Choice",
|
|
735
|
+
"signature": "Signature",
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
const isFormField = (f: JSONType, fieldOptions={ forUpdate: false }) => {
|
|
739
|
+
if (!is_object(f)) throw new Error("Expecting an object")
|
|
740
|
+
const field = f as Indexable
|
|
741
|
+
|
|
742
|
+
const { forUpdate } = fieldOptions
|
|
743
|
+
if (forUpdate) {
|
|
744
|
+
const { isOptional, type, title, description, intakeField, options } = field
|
|
745
|
+
if (
|
|
746
|
+
object_is_empty(filter_object({
|
|
747
|
+
isOptional, type, title, description, intakeField, options
|
|
748
|
+
}, is_defined))
|
|
749
|
+
)
|
|
750
|
+
{
|
|
751
|
+
throw `No update provided`
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
if (forUpdate === false || field.isOptional !== undefined)
|
|
756
|
+
field.isOptional = !!field.isOptional // coerce to bool, defaulting to false (required)
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
if (!forUpdate && !field.type) throw `field.type is required` // fieldName otherwise given as 'field' in validation for every subfield
|
|
760
|
+
if (field.type) exactMatchValidator(FIELD_TYPES)(field.type)
|
|
761
|
+
|
|
762
|
+
if (!forUpdate && !field.title) throw `field.title is required` // fieldName otherwise given as 'field' in validation for every subfield
|
|
763
|
+
if (field.title) {
|
|
764
|
+
field.title = stringValidator({
|
|
765
|
+
maxLength: 100,
|
|
766
|
+
errorMessage: "field title is required and must not exceed 100 characters"
|
|
767
|
+
})(field.title)
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
if (!forUpdate || field.description !== undefined){ // don't overwrite description on update with ''
|
|
771
|
+
field.description = stringValidator({
|
|
772
|
+
isOptional: true,
|
|
773
|
+
maxLength: 500,
|
|
774
|
+
errorMessage: "field description must be under 500 characters"
|
|
775
|
+
})(field.description) || ''
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
field.options = field.options || {} // ensure at least an empty object is provided
|
|
779
|
+
if (VALIDATE_OPTIONS_FOR_FIELD_TYPES[field.type as keyof typeof VALIDATE_OPTIONS_FOR_FIELD_TYPES] !== undefined) {
|
|
780
|
+
if (typeof field.options !== 'object') throw new Error(`Expected options to be an object but got ${typeof field.options}`)
|
|
781
|
+
|
|
782
|
+
const validators = VALIDATE_OPTIONS_FOR_FIELD_TYPES[field.type as keyof typeof VALIDATE_OPTIONS_FOR_FIELD_TYPES]
|
|
783
|
+
const requiredOptions = validators.REQUIRED
|
|
784
|
+
if (requiredOptions.length > Object.keys(field.options).length) {
|
|
785
|
+
for (const k of requiredOptions) {
|
|
786
|
+
if (field.options[k] === undefined) {
|
|
787
|
+
throw new Error(`Missing required field ${k}`)
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
for (const k in field.options) {
|
|
793
|
+
if (validators[k as keyof typeof validators] === undefined) {
|
|
794
|
+
throw new Error(`Got unexpected option ${k} for field of type ${INTERNAL_NAME_TO_DISPLAY_FIELD[field.type as keyof typeof INTERNAL_NAME_TO_DISPLAY_FIELD] || 'Text'}`)
|
|
795
|
+
}
|
|
796
|
+
field.options[k] = (validators[k as keyof typeof validators] as EscapeFunction)(field.options[k])
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
if (field.intakeField !== undefined) { // allow null to unset intake
|
|
801
|
+
if (RESERVED_INTAKE_FIELDS.includes(field.intakeField)) {
|
|
802
|
+
throw new Error(`${field.intakeField} is reserved for internal use only and cannot be used as an intake field`)
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
const intakeType = ENDUSER_FIELD_TYPES[field.intakeField as keyof typeof ENDUSER_FIELD_TYPES]
|
|
806
|
+
if (intakeType && intakeType !== field.type) {
|
|
807
|
+
throw new Error(
|
|
808
|
+
`Intake field ${field.intakeField} requires a form field type of ${INTERNAL_NAME_TO_DISPLAY_FIELD[intakeType as keyof typeof INTERNAL_NAME_TO_DISPLAY_FIELD] || 'Text'}`
|
|
809
|
+
)
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
return field
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
export const formResponsesValidator = (options={}) => build_validator(
|
|
817
|
+
responses => responses, // naively allow all types, to be validated by endpoint,
|
|
818
|
+
{ ...options, isOptional: true, listOf: true } // isOptional allows for optional fields, but should validate required vs missing in endpoint
|
|
819
|
+
)
|
|
820
|
+
|
|
821
|
+
export const intakePhoneValidator = exactMatchValidator<'optional' | 'required'>(['optional', 'required'])
|
|
822
|
+
|
|
823
|
+
export const formFieldValidator: EscapeBuilder<FormField> = (options={}, fieldOptions={ forUpdate: false }) => build_validator(
|
|
824
|
+
v => isFormField(v, fieldOptions),
|
|
825
|
+
{ ...options, isObject: true, listOf: false }
|
|
826
|
+
)
|
|
827
|
+
export const listOfFormFieldsValidator: EscapeBuilder<FormField[]> = (options={}, fieldOptions={ forUpdate: false }) => build_validator(
|
|
828
|
+
v => isFormField(v, fieldOptions),
|
|
829
|
+
{ ...options, isObject: true, listOf: true, emptyListOk: true }
|
|
830
|
+
)
|
|
831
|
+
|
|
832
|
+
|
|
707
833
|
// to ensure all topics in type have coverage at compile-time
|
|
708
834
|
const _CHAT_ROOM_TOPICS: { [K in ChatRoomTopic]: any } = {
|
|
709
835
|
task: '',
|