@tellescope/validation 0.0.39 → 0.0.43

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/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 got none")
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') {
@@ -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
- if (!isMobilePhone(phone)) {
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
- : "+" + escaped
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: '',