-
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 7 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 errors = []; | ||
const warnings = []; | ||
|
||
if (schema) { | ||
const asyncTasks = []; | ||
|
||
schema.pages?.forEach(page => | ||
page.sections?.forEach(section => | ||
section.questions?.forEach(question => { | ||
asyncTasks.push( | ||
handleQuestionValidation(question, errors, warnings, configObject), | ||
handleAnswerValidation(question, errors), | ||
); | ||
question.type === 'obsGroup' && | ||
question.questions?.forEach(obsGrpQuestion => | ||
asyncTasks.push( | ||
handleQuestionValidation(obsGrpQuestion, errors, configObject, warnings), | ||
handleAnswerValidation(question, errors), | ||
), | ||
); | ||
}), | ||
), | ||
); | ||
await Promise.all(asyncTasks); | ||
|
||
return [errors, warnings]; | ||
} | ||
}; | ||
|
||
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}`); | ||
if (data.results.length) { | ||
const [resObject] = data.results; | ||
|
||
resObject.datatype.name === 'Boolean' && | ||
conceptObject.questionOptions.answers.forEach(answer => { | ||
if ( | ||
answer.concept !== 'cf82933b-3f3f-45e7-a5ab-5d31aaee3da3' && | ||
answer.concept !== '488b58ff-64f5-4f8a-8979-fa79940b1594' | ||
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 would do something like: if ( ![ConceptTrue, ConceptFalse].includes(answer.concept) ) {
// handle error
} The ConceptTrue and ConceptFalse are driven by a System configuration: Ideally, we should introduce a new hook that will read this system setting. It would be great if you offered to address this otherwise you can use the already defined constants and we address this later on. 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 this is well noted. I would also like to take on the issue of the hook reading the values from the backend if possible. |
||
) { | ||
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.