@tellescope/validation 0.0.42 → 0.0.45

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tellescope/validation",
3
- "version": "0.0.42",
3
+ "version": "0.0.45",
4
4
  "description": "Input validation functions for server, client, and schema definition",
5
5
  "main": "./lib/cjs/validation.js",
6
6
  "module": "./lib/esm/validation.js",
@@ -23,11 +23,11 @@
23
23
  },
24
24
  "homepage": "https://github.com/tellescope-os/tellescope#readme",
25
25
  "dependencies": {
26
- "@tellescope/constants": "^0.0.42",
27
- "@tellescope/types-client": "^0.0.42",
28
- "@tellescope/types-models": "^0.0.42",
29
- "@tellescope/types-utilities": "^0.0.42",
30
- "@tellescope/utilities": "^0.0.42",
26
+ "@tellescope/constants": "^0.0.45",
27
+ "@tellescope/types-client": "^0.0.45",
28
+ "@tellescope/types-models": "^0.0.45",
29
+ "@tellescope/types-utilities": "^0.0.45",
30
+ "@tellescope/utilities": "^0.0.45",
31
31
  "validator": "^13.5.2"
32
32
  },
33
33
  "devDependencies": {
@@ -44,6 +44,6 @@
44
44
  "publishConfig": {
45
45
  "access": "public"
46
46
  },
47
- "gitHead": "ead8f7146baf9eaa913e4f6ecbe8fcc9ae718d5c",
47
+ "gitHead": "41ec2cff92d4567b84894abc73ffeaba21329e71",
48
48
  "composite": true
49
49
  }
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') {
@@ -305,6 +308,9 @@ export const stringValidator250: EscapeBuilder<string> = (o={}) => build_validat
305
308
  export const stringValidator5000: EscapeBuilder<string> = (o={}) => build_validator(
306
309
  escapeString(o), { ...o, maxLength: 5000, listOf: false }
307
310
  )
311
+ export const stringValidator25000: EscapeBuilder<string> = (o={}) => build_validator(
312
+ escapeString(o), { ...o, maxLength: 25000, listOf: false }
313
+ )
308
314
  export const SMSMessageValidator: EscapeBuilder<string> = (o={}) => build_validator(
309
315
  escapeString(o), { ...o, maxLength: 630, listOf: false }
310
316
  )
@@ -395,8 +401,8 @@ export const numberValidatorBuilder: ComplexEscapeBuilder<{ lower: number, upper
395
401
  )
396
402
  }
397
403
 
398
- export const nonNegNumberValidator = numberValidatorBuilder({ lower: 0, upper: 100000000 })
399
- export const numberValidator = numberValidatorBuilder({ lower: -100000000, upper: 100000000 })
404
+ export const nonNegNumberValidator = numberValidatorBuilder({ lower: 0, upper: 10000000000000 }) // max is 2286 in UTC MS
405
+ export const numberValidator = numberValidatorBuilder({ lower: -100000000, upper: 10000000000000 }) // max is 2286 in UTC MS
400
406
  export const fileSizeValidator = numberValidatorBuilder({ lower: 0, upper: MAX_FILE_SIZE })
401
407
 
402
408
  export const dateValidator: EscapeBuilder<Date> = (options={}) => build_validator(
@@ -705,6 +711,128 @@ export const rejectionWithMessage: EscapeBuilder<undefined> = o => build_validat
705
711
  export const numberToDateValidator = indexableNumberValidator(numberValidator(), dateValidator())
706
712
  export const idStringToDateValidator = indexableValidator(mongoIdStringValidator(), dateValidator())
707
713
 
714
+ // todo: move preference to FIELD_TYPES with drop-down option in user-facing forms
715
+ const FIELD_TYPES = ['string', 'number', 'email', 'phone', 'multiple_choice', 'file', 'signature']
716
+ const VALIDATE_OPTIONS_FOR_FIELD_TYPES = {
717
+ 'multiple_choice': {
718
+ choices: listOfStringsValidator({ maxLength: 100, errorMessage: "Multiple choice options must be under 100 characters, and you must have at least one option." }),
719
+ radio: booleanValidator({ errorMessage: "radio must be a boolean" }),
720
+ other: booleanValidator({ isOptional: true, errorMessage: "other must be a boolean" }),
721
+ REQUIRED: ['choices', 'radio'],
722
+ }
723
+ }
724
+ export const RESERVED_INTAKE_FIELDS = ['_id', 'id', 'externalId', 'phoneConsent', 'emailConsent', 'tags', 'journeys', 'updatedAt', 'preference', 'assignedTo', 'lastCommunication']
725
+
726
+ export const ENDUSER_FIELD_TYPES = {
727
+ 'email': 'email',
728
+ 'phone': 'phone',
729
+ 'fname': 'string',
730
+ 'lname': 'string',
731
+ }
732
+ export const INTERNAL_NAME_TO_DISPLAY_FIELD = {
733
+ "string": 'Text',
734
+ "number": 'Number',
735
+ "email": "Email",
736
+ "phone": "Phone Number",
737
+ multiple_choice: "Multiple Choice",
738
+ "signature": "Signature",
739
+ }
740
+
741
+ const isFormField = (f: JSONType, fieldOptions={ forUpdate: false }) => {
742
+ if (!is_object(f)) throw new Error("Expecting an object")
743
+ const field = f as Indexable
744
+
745
+ const { forUpdate } = fieldOptions
746
+ if (forUpdate) {
747
+ const { isOptional, type, title, description, intakeField, options } = field
748
+ if (
749
+ object_is_empty(filter_object({
750
+ isOptional, type, title, description, intakeField, options
751
+ }, is_defined))
752
+ )
753
+ {
754
+ throw `No update provided`
755
+ }
756
+ }
757
+
758
+ if (forUpdate === false || field.isOptional !== undefined)
759
+ field.isOptional = !!field.isOptional // coerce to bool, defaulting to false (required)
760
+
761
+
762
+ if (!forUpdate && !field.type) throw `field.type is required` // fieldName otherwise given as 'field' in validation for every subfield
763
+ if (field.type) exactMatchValidator(FIELD_TYPES)(field.type)
764
+
765
+ if (!forUpdate && !field.title) throw `field.title is required` // fieldName otherwise given as 'field' in validation for every subfield
766
+ if (field.title) {
767
+ field.title = stringValidator({
768
+ maxLength: 100,
769
+ errorMessage: "field title is required and must not exceed 100 characters"
770
+ })(field.title)
771
+ }
772
+
773
+ if (!forUpdate || field.description !== undefined){ // don't overwrite description on update with ''
774
+ field.description = stringValidator({
775
+ isOptional: true,
776
+ maxLength: 500,
777
+ errorMessage: "field description must be under 500 characters"
778
+ })(field.description) || ''
779
+ }
780
+
781
+ field.options = field.options || {} // ensure at least an empty object is provided
782
+ if (VALIDATE_OPTIONS_FOR_FIELD_TYPES[field.type as keyof typeof VALIDATE_OPTIONS_FOR_FIELD_TYPES] !== undefined) {
783
+ if (typeof field.options !== 'object') throw new Error(`Expected options to be an object but got ${typeof field.options}`)
784
+
785
+ const validators = VALIDATE_OPTIONS_FOR_FIELD_TYPES[field.type as keyof typeof VALIDATE_OPTIONS_FOR_FIELD_TYPES]
786
+ const requiredOptions = validators.REQUIRED
787
+ if (requiredOptions.length > Object.keys(field.options).length) {
788
+ for (const k of requiredOptions) {
789
+ if (field.options[k] === undefined) {
790
+ throw new Error(`Missing required field ${k}`)
791
+ }
792
+ }
793
+ }
794
+
795
+ for (const k in field.options) {
796
+ if (validators[k as keyof typeof validators] === undefined) {
797
+ 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'}`)
798
+ }
799
+ field.options[k] = (validators[k as keyof typeof validators] as EscapeFunction)(field.options[k])
800
+ }
801
+ }
802
+
803
+ if (field.intakeField !== undefined) { // allow null to unset intake
804
+ if (RESERVED_INTAKE_FIELDS.includes(field.intakeField)) {
805
+ throw new Error(`${field.intakeField} is reserved for internal use only and cannot be used as an intake field`)
806
+ }
807
+
808
+ const intakeType = ENDUSER_FIELD_TYPES[field.intakeField as keyof typeof ENDUSER_FIELD_TYPES]
809
+ if (intakeType && intakeType !== field.type) {
810
+ throw new Error(
811
+ `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'}`
812
+ )
813
+ }
814
+ }
815
+
816
+ return field
817
+ }
818
+
819
+ export const formResponsesValidator = (options={}) => build_validator(
820
+ responses => responses, // naively allow all types, to be validated by endpoint,
821
+ { ...options, isOptional: true, listOf: true } // isOptional allows for optional fields, but should validate required vs missing in endpoint
822
+ )
823
+
824
+ export const intakePhoneValidator = exactMatchValidator<'optional' | 'required'>(['optional', 'required'])
825
+
826
+ export const formFieldValidator: EscapeBuilder<FormField> = (options={}, fieldOptions={ forUpdate: false }) => build_validator(
827
+ v => isFormField(v, fieldOptions),
828
+ { ...options, isObject: true, listOf: false }
829
+ )
830
+ export const listOfFormFieldsValidator: EscapeBuilder<FormField[]> = (options={}, fieldOptions={ forUpdate: false }) => build_validator(
831
+ v => isFormField(v, fieldOptions),
832
+ { ...options, isObject: true, listOf: true, emptyListOk: true }
833
+ )
834
+
835
+
708
836
  // to ensure all topics in type have coverage at compile-time
709
837
  const _CHAT_ROOM_TOPICS: { [K in ChatRoomTopic]: any } = {
710
838
  task: '',