Skip to content
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

CMDCT-4224: POC for update report validation using yup schemas #90

Merged
merged 10 commits into from
Jan 21, 2025
10 changes: 8 additions & 2 deletions services/app-api/forms/qms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,14 @@ export const qmsReportTemplate: ReportTemplate = {
type: ElementType.Radio,
label: "Which quality measure will be reported?",
value: [
{ label: "{Measure name version 1}", value: "measure-1" },
{ label: "{Measure name version 2}", value: "measure-2" },
{
label: "{Measure name version 1}",
value: "measure-1",
},
{
label: "{Measure name version 2}",
value: "measure-2",
},
],
},
],
Expand Down
2 changes: 1 addition & 1 deletion services/app-api/handlers/banners/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
} from "../../libs/response-lib";
import { canWriteBanner } from "../../utils/authorization";
import { parseBannerId } from "../../libs/param-lib";
import { validateBannerPayload } from "../../utils/validation";
import { validateBannerPayload } from "../../utils/bannerValidation";
import { logger } from "../../libs/debug-lib";
import { BannerData } from "../../types/banner";

Expand Down
27 changes: 15 additions & 12 deletions services/app-api/types/reports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,19 +37,19 @@ export interface MeasureOptions {

export enum MeasureTemplateName {
// required measures
"LTSS-1",
"LTSS-2",
"LTSS-6",
"LTSS-7",
"LTSS-8",
"LTSS-1" = "LTSS-1",
"LTSS-2" = "LTSS-2",
"LTSS-6" = "LTSS-6",
"LTSS-7" = "LTSS-7",
"LTSS-8" = "LTSS-8",
//optional measures
"FASI-1",
"FASI-2",
"HCBS-10",
"LTSS-3",
"LTSS-4",
"LTSS-5",
"MLTSS",
"FASI-1" = "FASI-1",
"FASI-2" = "FASI-2",
"HCBS-10" = "HCBS-10",
"LTSS-3" = "LTSS-3",
"LTSS-4" = "LTSS-4",
"LTSS-5" = "LTSS-5",
"MLTSS" = "MLTSS",
}

export enum ReportStatus {
Expand Down Expand Up @@ -207,12 +207,14 @@ export type TextboxTemplate = {
type: ElementType.Textbox;
label: string;
helperText?: string;
answer?: string;
};

export type DateTemplate = {
type: ElementType.Date;
label: string;
helperText: string;
answer?: string;
};

export type AccordionTemplate = {
Expand All @@ -238,6 +240,7 @@ export type RadioTemplate = {
label: string;
helperText?: string;
value: ChoiceTemplate[];
answer?: string;
};

export type ButtonLinkTemplate = {
Expand Down
252 changes: 252 additions & 0 deletions services/app-api/utils/reportValidation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
import {
array,
boolean,
lazy,
mixed,
number,
object,
Schema,
string,
} from "yup";
import {
ReportStatus,
ReportType,
MeasureTemplateName,
PageType,
ElementType,
PageElement,
} from "../types/reports";
import { error } from "./constants";

const headerTemplateSchema = object().shape({
type: string().required(ElementType.Header),
text: string().required(),
});

const subHeaderTemplateSchema = object().shape({
type: string().required(ElementType.SubHeader),
text: string().required(),
});

const paragraphTemplateSchema = object().shape({
type: string().required(ElementType.Paragraph),
text: string().required(),
title: string().notRequired(),
});

const textboxTemplateSchema = object().shape({
type: string().required(ElementType.Textbox),
label: string().required(),
helperText: string().notRequired(),
answer: string().notRequired(),
});

const dateTemplateSchema = object().shape({
type: string().required(ElementType.Date),
label: string().required(),
helperText: string().required(),
angelaco11 marked this conversation as resolved.
Show resolved Hide resolved
answer: string().notRequired(),
});

const accordionTemplateSchema = object().shape({
type: string().required(ElementType.Accordion),
label: string().required(),
value: string().required(),
});

const resultRowButtonTemplateSchema = object().shape({
type: string().required(ElementType.ResultRowButton),
value: string().required(),
modalId: string().required(),
to: string().required(),
});

const pageElementSchema = lazy((value: PageElement): Schema<any> => {
if (!value.type) {
throw new Error();
}
switch (value.type) {
case ElementType.Header:
return headerTemplateSchema;
case ElementType.SubHeader:
return subHeaderTemplateSchema;
case ElementType.Paragraph:
return paragraphTemplateSchema;
case ElementType.Textbox:
return textboxTemplateSchema;
case ElementType.Date:
return dateTemplateSchema;
case ElementType.Accordion:
return accordionTemplateSchema;
case ElementType.ResultRowButton:
return resultRowButtonTemplateSchema;
case ElementType.Radio:
return radioTemplateSchema;
case ElementType.ButtonLink:
return buttonLinkTemplateSchema;
case ElementType.MeasureTable:
return measureTableTemplateSchema;
case ElementType.QualityMeasureTable:
return qualityMeasureTableTemplateSchema;
case ElementType.StatusTable:
return statusTableTemplateSchema;
default:
return mixed().notRequired(); // Fallback, although it should never be hit
}
});

const radioTemplateSchema = object().shape({
type: string().required(ElementType.Radio),
label: string().required(),
helperText: string().notRequired(),
rocio-desantiago marked this conversation as resolved.
Show resolved Hide resolved
value: array().of(
object().shape({
label: string().required(),
value: string().required(),
checked: boolean().notRequired(),
checkedChildren: lazy(() => array().of(pageElementSchema).notRequired()),
})
),
answer: string().notRequired(),
});

const buttonLinkTemplateSchema = object().shape({
type: string().required(ElementType.ButtonLink),
label: string().required(),
to: string().required(),
});

const measureTableTemplateSchema = object().shape({
type: string().required(ElementType.MeasureTable),
measureDisplay: string()
.oneOf(["required", "stratified", "optional"])
.required(),
});

const qualityMeasureTableTemplateSchema = object().shape({
type: string().required(ElementType.QualityMeasureTable),
measureDisplay: string().required("quality"),
});

const statusTableTemplateSchema = object().shape({
type: string().required(ElementType.StatusTable),
to: string().required(),
});

const parentPageTemplateSchema = object().shape({
id: string().required(),
childPageIds: array().of(string()).required(),
});

const formPageTemplateSchema = object().shape({
id: string().required(),
title: string().required(),
type: mixed<PageType>().oneOf(Object.values(PageType)).required(),
elements: array().of(pageElementSchema).required(),
sidebar: boolean().notRequired(),
hideNavButtons: boolean().notRequired(),
childPageIds: array().of(string()).notRequired(),
});

// MeasurePageTemplate extends FormPageTemplate
const measurePageTemplateSchema = formPageTemplateSchema.shape({
cmit: number().notRequired(),
required: boolean().notRequired(),
stratified: boolean().notRequired(),
optional: boolean().notRequired(),
substitutable: boolean().notRequired(),
});

const measureOptionsArraySchema = array().of(
object().shape({
cmit: number().required(),
required: boolean().required(),
stratified: boolean().required(),
measureTemplate: mixed()
.oneOf(Object.values(MeasureTemplateName))
.required(),
})
);

const measureLookupSchema = object().shape({
defaultMeasures: measureOptionsArraySchema,
// TODO: Add option groups
});

/**
* This schema is meant to represent the pages field in the ReportTemplate type.
* The following yup `lazy` function is building up the union type:
* `(ParentPageTemplate | FormPageTemplate | MeasurePageTemplate)[]`
* and outputs the correct type in the union based on various fields
* on the page object that gets passed through.
*/
const pagesSchema = array()
.of(
lazy((pageObject) => {
if (!pageObject.type) {
if (pageObject.id && pageObject.childPageIds) {
return parentPageTemplateSchema;
} else {
throw new Error();
}
} else {
if (pageObject.type == PageType.Measure) {
return measurePageTemplateSchema;
}
return formPageTemplateSchema;
}
})
)
.required();

/**
* This schema represents a typescript type of Record<MeasureTemplateName, MeasurePageTemplate>
*
* The following code is looping through the MeasureTemplateName enum and building
* a yup validation object that looks like so:
* {
* [MeasureTemplateName["LTSS-1"]]: measurePageTemplateSchema,
* [MeasureTemplateName["LTSS-2"]]: measurePageTemplateSchema,
* [MeasureTemplateName["LTSS-6"]]: measurePageTemplateSchema,
* ...
* ...
* }
*/
const measureTemplatesSchema = object().shape(
Object.fromEntries(
Object.keys(MeasureTemplateName).map((meas) => [
meas,
measurePageTemplateSchema,
])
)
);

const reportValidateSchema = object().shape({
id: string().notRequired(),
state: string().required(),
created: number().notRequired(),
lastEdited: number().notRequired(),
lastEditedBy: string().required(),
lastEditedByEmail: string().required(),
status: mixed<ReportStatus>().oneOf(Object.values(ReportStatus)).required(),
name: string().notRequired(),
type: mixed<ReportType>().oneOf(Object.values(ReportType)).required(),
title: string().required(),
pages: pagesSchema,
measureLookup: measureLookupSchema,
measureTemplates: measureTemplatesSchema,
});

export const validateUpdateReportPayload = async (
payload: object | undefined
) => {
if (!payload) {
throw new Error(error.MISSING_DATA);
}

const validatedPayload = await reportValidateSchema.validate(payload, {
stripUnknown: true,
});

return validatedPayload;
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { validateBannerPayload } from "../validation";
import { validateBannerPayload } from "../bannerValidation";

const validObject = {
key: "1023",
Expand Down
Loading
Loading