import { Injectable } from '@angular/core'; import { ReferenceFieldAPI } from '@core/typings/api/reference-fields.typing'; import { ReferenceFieldsUI } from '@core/typings/ui/reference-fields.typing'; import { FormAudience } from '@features/configure-forms/form.typing'; import { CustomDataTablesService } from '@features/custom-data-tables/custom-data-table.service'; import { CSVBoolean, CSVBooleanFactory, IsNumber, IsOneOf, IsString, Required, ServiceValidator, Transform, Unique, ValidatorExtras } from '@yourcause/common'; import { ReferenceFieldsService } from './reference-fields.service'; export type ValidatorReturn = any[]|{ i18nKey: string; defaultValue: string; }; export type FormFieldValidatorExtras = ValidatorExtras; @Injectable({ providedIn: 'root' }) export class ReferenceFieldsValidatorService { constructor ( private referenceFieldService: ReferenceFieldsService, private customDataTableService: CustomDataTablesService ) { } /** * * @returns validation model by audience / field type */ getValidationModelByAudienceMap (): ValidationModelByAudienceMap { return { [FormAudience.APPLICANT]: this.getValidationModelByAudience(FormAudience.APPLICANT), [FormAudience.MANAGER]: this.getValidationModelByAudience(FormAudience.MANAGER) }; } /** * * @param formAudience: form audience * @returns the validation model map by type */ getValidationModelByAudience ( formAudience: FormAudience ): ValidationModelByTypeMap { if (formAudience === FormAudience.APPLICANT) { return ValidationModelByTypeApplicantMap; } else { return ValidationModelByTypeManagerMap; } } /** * @param value: value to validate * @param extras form field validation extras * @returns if error - error object, otherwise empty array */ validateKeyDoesNotExist (value: any): ValidatorReturn { const existingKeys = this.referenceFieldService.allReferenceFields.map((field) => field.key); return existingKeys.includes(value) ? { i18nKey: 'GLOBAL:textKeyAlreadyExistsMustBeUnique', defaultValue: 'Key already exists. Please use a unique key.' } : []; } /** * @param value: value to validate * @param extras form field validation extras * @returns if error - error object, otherwise empty array */ validateDataTableGuidExists (value: any): ValidatorReturn { const existingGuids = this.customDataTableService.customDataTables.map((table) => table.guid); return !existingGuids.includes(value) ? { i18nKey: 'GLOBAL:textIDMustExist', defaultValue: 'ID must exist' } : []; } /** * @param value: value to validate * @param extras form field validation extras * @returns if error - error object, otherwise empty array */ validateParentFieldExists (_value: any, extras: FormFieldValidatorExtras): ValidatorReturn { const parentKey = extras.ent['Parent Form Field Key']; // If Parent Form Field Key defined, it must exist or is being created here if (parentKey) { const existingKeys = this.referenceFieldService.allReferenceFields.filter((field) => { return field.type === ReferenceFieldsUI.ReferenceFieldTypes.CustomDataTable; }).map((field) => field.key); const keysInGroup = extras.group.filter((row) => { return !!row.Key && row.Key !== extras.ent.Key; }).map((row) => row.Key); const allKeys = keysInGroup.concat(existingKeys); if (!allKeys.includes(parentKey)) { return { i18nKey: 'GLOBAL:textParentFormFieldKeyMustExist', defaultValue: 'Parent form field key must exist' }; } } return []; } /** * @param value: value to validate * @param extras form field validation extras * @returns if error - error object, otherwise empty array */ validateFormFieldAggregateExists (_value: any, extras: FormFieldValidatorExtras): ValidatorReturn { const formFieldKey = extras.ent['Form Field Key']; if (formFieldKey) { const availableFields = this.referenceFieldService.getApplicableAggregateFormFields(); const field = this.referenceFieldService.getReferenceFieldByKey(formFieldKey); if (!field) { return { i18nKey: 'GLOBAL:textFormFieldKeyMustExist', defaultValue: 'Form field key must exist' }; } const exists = availableFields.some((availField) => { return availField.value === field?.referenceFieldId; }); if (!exists) { return { i18nKey: 'GLOBAL:textFormFieldKeyMustBeAValidAggregateTypeField', defaultValue: 'Form field key must be a valid aggregate type field' }; } } return []; } /** * @param value: value to validate * @param extras form field validation extras * @returns if error - error object, otherwise empty array */ validateAggregateTypeExists (_value: any, extras: FormFieldValidatorExtras): ValidatorReturn { const aggregateType = (extras.ent['Aggregation type'] || '').toLowerCase(); const validAggregateImportOptions = [ 'sum', 'max', 'min', 'count', 'average' ]; if (!validAggregateImportOptions.includes(aggregateType)) { return { i18nKey: 'GLOBAL:textAggregateTypeMustBeOneOfImport', defaultValue: 'Aggregate type must be one of: sum, max, min, count, or average' }; } return []; } /** * @param value: value to validate * @param extras form field validation extras * @returns if error - error object, otherwise empty array */ validateParentFieldRequired (_value: any, extras: FormFieldValidatorExtras): ValidatorReturn { const thisGuid = extras.ent['Custom Data Table Id']; const picklist = this.customDataTableService.getCDTFromGuid(thisGuid); if ( picklist?.parentPicklistId && !extras.ent['Parent Form Field Key'] ) { return { i18nKey: 'GLOBAL:textParentFormFieldKeyRequired', defaultValue: 'Parent form field key required' }; } return []; } /** * @param value: value to validate * @param extras form field validation extras * @returns if error - error object, otherwise empty array */ validateParentField (_value: any, extras: FormFieldValidatorExtras): ValidatorReturn { const thisGuid = extras.ent['Custom Data Table Id']; const invalidKey = { i18nKey: 'GLOBAL:textParentFormFieldKeyIsNotValid', defaultValue: 'Parent form field key is not valid' }; if (thisGuid) { const parentFormFieldKey = extras.ent['Parent Form Field Key']; if (parentFormFieldKey) { const foundField = this.referenceFieldService.getReferenceFieldByKey( parentFormFieldKey ); if (foundField) { const options = this.referenceFieldService.getParentRefFieldOptionsFromDependentCDTGuid( thisGuid ); const foundValidOpt = options.find((opt) => { return opt.value === foundField.referenceFieldId; }); if (!foundValidOpt) { return invalidKey; } } else { // If field not found, it has to be found in this import const foundInGroup = extras.group.find((row) => { return row.Key === parentFormFieldKey; }); if (foundInGroup) { const cdt = this.customDataTableService.getCDTFromGuid(thisGuid); const parentCdt = this.customDataTableService.getParentCDT(cdt.parentPicklistId); // Make sure the parent field found in import has the correct CDT if (foundInGroup['Custom Data Table Id'] !== parentCdt?.guid) { return invalidKey; } } else { return invalidKey; } } } } return []; } /** * @param value: value to validate * @param extras form field validation extras * @returns if error - error object, otherwise empty array */ validateSupportsMultiCantBeTableField (_value: any, extras: FormFieldValidatorExtras): ValidatorReturn { const supportsMultiple = extras.ent['Supports multiple values']; const isTableField = extras.ent['Is table field']; if (supportsMultiple && isTableField) { return { i18nKey: 'GLOBAL:textCannotSupportMultiAndBeTableField', defaultValue: 'Cannot support multiple values and be a table field' }; } return []; } } export const KeyDoesNotExist = ServiceValidator( ReferenceFieldsValidatorService, 'validateKeyDoesNotExist' ); export const DataTableGuidExists = ServiceValidator( ReferenceFieldsValidatorService, 'validateDataTableGuidExists' ); export const ParentFieldExists = ServiceValidator( ReferenceFieldsValidatorService, 'validateParentFieldExists' ); export const FormFieldAggregateExists = ServiceValidator( ReferenceFieldsValidatorService, 'validateFormFieldAggregateExists' ); export const AggregateTypeExists = ServiceValidator( ReferenceFieldsValidatorService, 'validateAggregateTypeExists' ); export const ParentFieldRequired = ServiceValidator( ReferenceFieldsValidatorService, 'validateParentFieldRequired' ); export const ParentFieldValid = ServiceValidator( ReferenceFieldsValidatorService, 'validateParentField' ); export const SupportsMultiCantBeTableField = ServiceValidator( ReferenceFieldsValidatorService, 'validateSupportsMultiCantBeTableField' ); /** * Start Base Validators */ export class FormFieldsSimpleApplicantValidationModel { @IsString() @Required() 'Name': string; @IsString() @Unique(true) @KeyDoesNotExist() 'Key': string; @IsString() 'Description': string; } export class FormFieldsSimpleManagerValidationModel extends FormFieldsSimpleApplicantValidationModel { @CSVBoolean() 'Single response': boolean; } export class FormFieldsWithMultipleApplicantValidationModel { @IsString() @Required() 'Name': string; @IsString() @Unique(true) @KeyDoesNotExist() 'Key': string; @IsString() 'Description': string; @CSVBoolean() 'Supports multiple values': boolean; } export class FormFieldsWithMultipleManagerValidationModel extends FormFieldsWithMultipleApplicantValidationModel { @CSVBoolean() 'Single response': boolean; } export class FormFieldsWithOptionsApplicantValidationModel { @IsString() @Required() 'Name': string; @IsString() @Unique(true) @KeyDoesNotExist() 'Key': string; @IsString() 'Description': string; @IsString() @Required() @DataTableGuidExists() 'Custom Data Table Id': string; } export class FormFieldsWithOptionsManagerValidationModel extends FormFieldsWithOptionsApplicantValidationModel { @CSVBoolean() 'Single response': boolean; } export class FormFieldsWithOptionsAndMultipleApplicantValidationModel { @IsString() @Required() 'Name': string; @IsString() @Unique(true) @KeyDoesNotExist() 'Key': string; @IsString() 'Description': string; @IsString() @Required() @DataTableGuidExists() 'Custom Data Table Id': string; @IsString() @ParentFieldExists() @ParentFieldRequired() @ParentFieldValid() 'Parent Form Field Key': string; @CSVBoolean() 'Supports multiple values': boolean; } export class FormFieldsWithOptionsAndMultipleManagerValidationModel extends FormFieldsWithOptionsAndMultipleApplicantValidationModel { @CSVBoolean() 'Single response': boolean; } /** * End Base Validators */ /** * Checkboxes */ export class ApplicantCheckboxValidator extends FormFieldsSimpleApplicantValidationModel { @CSVBooleanFactory(false)() 'Is table field': boolean; } export class ManagerCheckboxValidator extends FormFieldsSimpleManagerValidationModel {} /** * Currency */ export class ApplicantCurrencyValidator extends FormFieldsSimpleApplicantValidationModel { @CSVBooleanFactory(false)() 'Is table field': boolean; } export class ManagerCurrencyValidator extends FormFieldsSimpleManagerValidationModel {} /** * Picklist */ export class ApplicantPicklistValidator extends FormFieldsWithOptionsAndMultipleApplicantValidationModel { @CSVBooleanFactory(false)() @SupportsMultiCantBeTableField() 'Is table field': boolean; @CSVBooleanFactory(false)() 'Encrypted': boolean; @CSVBooleanFactory(false)() 'Masked': boolean; } export class ManagerPicklistValidator extends FormFieldsWithOptionsAndMultipleManagerValidationModel { @CSVBooleanFactory(false)() 'Encrypted': boolean; } /** * Date */ export class ApplicantDateValidator extends FormFieldsSimpleApplicantValidationModel { @CSVBooleanFactory(false)() 'Is table field': boolean; @CSVBooleanFactory(false)() 'Encrypted': boolean; @CSVBooleanFactory(false)() 'Masked': boolean; } export class ManagerDateValidator extends FormFieldsSimpleManagerValidationModel { @CSVBooleanFactory(false)() 'Encrypted': boolean; } /** * File Upload */ export class ApplicantFileUploadValidator extends FormFieldsSimpleApplicantValidationModel {} export class ManagerFileUploadValidator extends FormFieldsSimpleManagerValidationModel {} /** * Number */ export class ApplicantNumberValidator extends FormFieldsSimpleApplicantValidationModel { @CSVBooleanFactory(false)() 'Is table field': boolean; @CSVBooleanFactory(false)() 'Encrypted': boolean; @CSVBooleanFactory(false)() 'Masked': boolean; } export class ManagerNumberValidator extends FormFieldsSimpleManagerValidationModel { @CSVBooleanFactory(false)() 'Encrypted': boolean; } /** * Radio */ export class ApplicantRadioValidator extends FormFieldsWithOptionsApplicantValidationModel { @CSVBooleanFactory(false)() 'Is table field': boolean; @CSVBooleanFactory(false)() 'Encrypted': boolean; @CSVBooleanFactory(false)() 'Masked': boolean; } export class ManagerRadioValidator extends FormFieldsWithOptionsManagerValidationModel { @CSVBooleanFactory(false)() 'Encrypted': boolean; } /** * Select Boxes */ export class ApplicantSelectBoxesValidator extends FormFieldsWithOptionsApplicantValidationModel { @CSVBooleanFactory(false)() 'Encrypted': boolean; @CSVBooleanFactory(false)() 'Masked': boolean; } export class ManagerSelectBoxesValidator extends FormFieldsWithOptionsManagerValidationModel { @CSVBooleanFactory(false)() 'Encrypted': boolean; } /** * Text Area */ export class ApplicantTextAreaValidator extends FormFieldsWithMultipleApplicantValidationModel { @CSVBooleanFactory(false)() @SupportsMultiCantBeTableField() 'Is table field': boolean; } export class ManagerTextAreaValidator extends FormFieldsWithMultipleManagerValidationModel {} /** * Text Area */ export class ApplicantTextValidator extends FormFieldsWithMultipleApplicantValidationModel { @CSVBooleanFactory(false)() @SupportsMultiCantBeTableField() 'Is table field': boolean; @CSVBooleanFactory(false)() 'Encrypted': boolean; @CSVBooleanFactory(false)() 'Masked': boolean; @IsNumber() @Transform((val: number) => { if (!val) { return ReferenceFieldAPI.ReferenceFieldFormattingType.NONE; } const isNumber = !isNaN(val); return isNumber ? +val : val; }) @IsOneOf([ ReferenceFieldAPI.ReferenceFieldFormattingType.NONE, ReferenceFieldAPI.ReferenceFieldFormattingType.EIN, ReferenceFieldAPI.ReferenceFieldFormattingType.EMAIL, ReferenceFieldAPI.ReferenceFieldFormattingType.TIME_12_HOUR, ReferenceFieldAPI.ReferenceFieldFormattingType.TIME_24_HOUR, ReferenceFieldAPI.ReferenceFieldFormattingType.URL_OPT_HTTP, ReferenceFieldAPI.ReferenceFieldFormattingType.URL_REQ_HTTP ], { message: { i18nKey: 'common:textPleaseEnterValidFormattingType', defaultValue: 'Please enter a valid formatting type. None = 0, Email = 1, 24 hour time = 2, 12 hour time = 3, EIN = 4, URL = 5, URL with http required = 6.' } }) 'Formatting': number; } export class ManagerTextValidator extends FormFieldsWithMultipleManagerValidationModel { @CSVBooleanFactory(false)() 'Encrypted': boolean; } /** * Data Point */ export class ApplicantDataPointValidator extends FormFieldsSimpleApplicantValidationModel {} export class ManagerDataPointValidator extends FormFieldsSimpleManagerValidationModel {} /** * Aggregates */ export class ManagerAggregateValidator { @IsString() @Required() 'Name': string; @IsString() 'Description': string; @IsString() @Required() @FormFieldAggregateExists() 'Form Field Key': string; @IsString() @Required() @AggregateTypeExists() 'Aggregation type': string; } export type BulkImportValidationClass = ApplicantCheckboxValidator| ManagerCheckboxValidator| ApplicantCurrencyValidator| ManagerCurrencyValidator| ApplicantPicklistValidator| ManagerPicklistValidator| ApplicantDateValidator| ManagerDateValidator| ApplicantFileUploadValidator| ManagerFileUploadValidator| ApplicantNumberValidator| ManagerNumberValidator| ApplicantRadioValidator| ManagerRadioValidator| ApplicantSelectBoxesValidator| ManagerSelectBoxesValidator| ApplicantTextAreaValidator| ManagerTextAreaValidator| ApplicantTextValidator| ManagerTextValidator| ApplicantDataPointValidator| ManagerDataPointValidator| ManagerAggregateValidator; const RefTypes = ReferenceFieldsUI.ReferenceFieldTypes; type ValidationModelByTypeMap = Record; type ValidationModelByAudienceMap = Record; export const ValidationModelByTypeApplicantMap: ValidationModelByTypeMap = { [RefTypes.Checkbox]: ApplicantCheckboxValidator, [RefTypes.Currency]: ApplicantCurrencyValidator, [RefTypes.CustomDataTable]: ApplicantPicklistValidator, [RefTypes.Date]: ApplicantDateValidator, [RefTypes.FileUpload]: ApplicantFileUploadValidator, [RefTypes.Number]: ApplicantNumberValidator, [RefTypes.Radio]: ApplicantRadioValidator, [RefTypes.SelectBoxes]: ApplicantSelectBoxesValidator, [RefTypes.TextArea]: ApplicantTextAreaValidator, [RefTypes.TextField]: ApplicantTextValidator, [RefTypes.DataPoint]: ApplicantDataPointValidator, [RefTypes.ExternalAPI]: null, [RefTypes.Aggregate]: null, [RefTypes.Table]: null, [RefTypes.Subset]: null }; export const ValidationModelByTypeManagerMap: ValidationModelByTypeMap = { [RefTypes.Checkbox]: ManagerCheckboxValidator, [RefTypes.Currency]: ManagerCurrencyValidator, [RefTypes.CustomDataTable]: ManagerPicklistValidator, [RefTypes.Date]: ManagerDateValidator, [RefTypes.FileUpload]: ManagerFileUploadValidator, [RefTypes.Number]: ManagerNumberValidator, [RefTypes.Radio]: ManagerRadioValidator, [RefTypes.SelectBoxes]: ManagerSelectBoxesValidator, [RefTypes.TextArea]: ManagerTextAreaValidator, [RefTypes.TextField]: ManagerTextValidator, [RefTypes.Aggregate]: ManagerAggregateValidator, [RefTypes.DataPoint]: ManagerDataPointValidator, [RefTypes.ExternalAPI]: null, [RefTypes.Table]: null, [RefTypes.Subset]: null };