-
Notifications
You must be signed in to change notification settings - Fork 59
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Moved schema-validator logic to form engine #107
Changes from 3 commits
6cb30fa
84121be
604efb8
e1cf8ba
e32fbdf
5dc978f
e9558b5
672c387
1c17c0d
b8dc67e
e5b6a0b
3dc7968
f7ca7b0
bbf444f
84ecf2e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
import { openmrsFetch } from '@openmrs/esm-framework'; | ||
import { OHRIFormSchema, ConfigObject } from './api/types'; | ||
|
||
export const handleFormValidation = async ( | ||
schema: OHRIFormSchema, | ||
configObject: ConfigObject, | ||
): Promise<Array<Array<Record<string, any>>>> => { | ||
const errorsArray = []; | ||
const warningsArray = []; | ||
|
||
if (schema) { | ||
const asyncTasks = []; | ||
|
||
schema.pages?.forEach(page => | ||
page.sections?.forEach(section => | ||
section.questions?.forEach(question => { | ||
asyncTasks.push( | ||
handleQuestionValidation(question, errorsArray, warningsArray, configObject), | ||
handleAnswerValidation(question, errorsArray), | ||
); | ||
question.type === 'obsGroup' && | ||
question.questions?.forEach(obsGrpQuestion => | ||
asyncTasks.push( | ||
handleQuestionValidation(obsGrpQuestion, errorsArray, configObject, warningsArray), | ||
handleAnswerValidation(question, errorsArray), | ||
), | ||
); | ||
}), | ||
), | ||
); | ||
await Promise.all(asyncTasks); | ||
|
||
return [errorsArray, warningsArray]; | ||
} | ||
}; | ||
|
||
const handleQuestionValidation = async (conceptObject, errorsArray, warningsArray, configObject) => { | ||
const conceptRepresentation = | ||
'custom:(uuid,display,datatype,answers,conceptMappings:(conceptReferenceTerm:(conceptSource:(name),code)))'; | ||
|
||
const searchRef = conceptObject.questionOptions.concept | ||
? conceptObject.questionOptions.concept | ||
: conceptObject.questionOptions.conceptMappings?.length | ||
? conceptObject.questionOptions.conceptMappings | ||
?.map(mapping => { | ||
return `${mapping.type}:${mapping.value}`; | ||
}) | ||
.join(',') | ||
: ''; | ||
|
||
if (searchRef) { | ||
try { | ||
const { data } = await openmrsFetch(`/ws/rest/v1/concept?references=${searchRef}&v=${conceptRepresentation}`); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm concerned about the fact that we have fused the validation logic with API-specific logic. I advise reusing the existing // the validator runs if doValidate is set to true
// isValidating shows the validation status
const { errors, warnings, isValidating } = useSchemaValidationResults(doValidate, schema); Alternatively, you can define a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @samuelmale I had a previous implementation of how I understood your statement here, kindly advise if this logic suffices. |
||
if (data.results.length) { | ||
const [resObject] = data.results; | ||
|
||
resObject.datatype.name === 'Boolean' && | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should be driven by a config There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @samuelmale kindly expound on this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My bad @arodidev my comment regarding the config, see. As a good practice, I would create an const ConceptDataTypes {
Boolean: 'Boolean',
Coded: 'Coded',
Numeric: 'Numeric',
Text: 'Text'
// more...
};
// validator.js
if (concept.datatype.name === ConceptDataTypes.Boolean) {}
|
||
conceptObject.questionOptions.answers.forEach(answer => { | ||
if ( | ||
answer.concept !== 'cf82933b-3f3f-45e7-a5ab-5d31aaee3da3' && | ||
answer.concept !== '488b58ff-64f5-4f8a-8979-fa79940b1594' | ||
) { | ||
errorsArray.push({ | ||
errorMessage: `❌ concept "${conceptObject.questionOptions.concept}" of type "boolean" has a non-boolean answer "${answer.label}"`, | ||
field: conceptObject, | ||
}); | ||
} | ||
}); | ||
|
||
resObject.datatype.name === 'Coded' && | ||
conceptObject.questionOptions.answers.forEach(answer => { | ||
if (!resObject.answers.some(answerObject => answerObject.uuid === answer.concept)) { | ||
warningsArray.push({ | ||
warningMessage: `⚠️ answer: "${answer.label}" - "${answer.concept}" does not exist in the response answers but exists in the form`, | ||
field: conceptObject, | ||
}); | ||
} | ||
}); | ||
|
||
dataTypeChecker(conceptObject, resObject, errorsArray, configObject); | ||
} else { | ||
errorsArray.push({ | ||
errorMessage: `❓ Concept "${conceptObject.questionOptions.concept}" not found`, | ||
field: conceptObject, | ||
}); | ||
} | ||
} catch (error) { | ||
console.error(error); | ||
} | ||
} else { | ||
errorsArray.push({ | ||
errorMessage: `❓ Question with no concept UUID / mappings: ${conceptObject.id}`, | ||
field: conceptObject, | ||
}); | ||
} | ||
}; | ||
|
||
const dataTypeChecker = (conceptObject, responseObject, array, dataTypeToRenderingMap) => { | ||
Object.prototype.hasOwnProperty.call(dataTypeToRenderingMap, responseObject.datatype.name) && | ||
!dataTypeToRenderingMap[responseObject.datatype.name].includes(conceptObject.questionOptions.rendering) && | ||
array.push({ | ||
errorMessage: `❓ ${conceptObject.questionOptions.concept}: datatype "${responseObject.datatype.name}" doesn't match control type "${conceptObject.questionOptions.rendering}"`, | ||
field: conceptObject, | ||
}); | ||
|
||
!Object.prototype.hasOwnProperty.call(dataTypeToRenderingMap, responseObject.datatype.name) && | ||
array.push({ | ||
errorMessage: `❓ Untracked datatype "${responseObject.datatype.name}"`, | ||
field: conceptObject, | ||
}); | ||
}; | ||
|
||
const handleAnswerValidation = (questionObject, array) => { | ||
const answerArray = questionObject.questionOptions.answers; | ||
const conceptRepresentation = | ||
'custom:(uuid,display,datatype,conceptMappings:(conceptReferenceTerm:(conceptSource:(name),code)))'; | ||
|
||
answerArray?.length && | ||
answerArray.forEach(answer => { | ||
const searchRef = answer.concept | ||
? answer.concept | ||
: answer.conceptMappings?.length | ||
? answer.conceptMappings | ||
.map(eachMapping => { | ||
return `${eachMapping.type}:${eachMapping.value}`; | ||
}) | ||
.join(',') | ||
: ''; | ||
|
||
openmrsFetch(`/ws/rest/v1/concept?references=${searchRef}&v=${conceptRepresentation}`).then(response => { | ||
if (!response.data.results.length) { | ||
array.push({ | ||
errorMessage: `❌ answer "${answer.label}" backed by concept "${answer.concept}" not found`, | ||
field: questionObject, | ||
}); | ||
} | ||
}); | ||
}); | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is possessing a generic name; one may assume it's the general config interface for the form-engine configuration yet I think it's a form schema validator configuration?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Noted.