Skip to content

Commit

Permalink
CMDCT-4224: POC for update report validation using yup schemas (#90)
Browse files Browse the repository at this point in the history
  • Loading branch information
angelaco11 authored and Rocio De Santiago committed Jan 24, 2025
1 parent 1e4755b commit 14b7b20
Show file tree
Hide file tree
Showing 8 changed files with 505 additions and 16 deletions.
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
File renamed without changes.
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(),
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(),
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

0 comments on commit 14b7b20

Please sign in to comment.