diff --git a/src/services/api/handlers/forms.ts b/src/services/api/handlers/forms.ts deleted file mode 100644 index d871ea141..000000000 --- a/src/services/api/handlers/forms.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { response } from "../libs/handler"; -import * as fs from "fs"; -import { APIGatewayEvent } from "aws-lambda"; -import { convertRegexToString } from "shared-utils"; - -export const forms = async (event: APIGatewayEvent) => { - try { - const formId = event.queryStringParameters?.formId?.toLocaleUpperCase(); - let formVersion = event.queryStringParameters?.formVersion; - - if (!formId) { - return response({ - statusCode: 400, - body: { error: "File ID was not provided" }, - }); - } - - const filePath = getFilepathForIdAndVersion(formId, formVersion); - - if (!filePath) { - return response({ - statusCode: 404, - body: { - error: "No file was found with provided formId and formVersion", - }, - }); - } - - const jsonData = await fs.promises.readFile(filePath, "utf-8"); - - if (!jsonData) { - return response({ - statusCode: 404, - body: { - error: `File found for ${formId}, but it's empty`, - }, - }); - } - - if (!formVersion) formVersion = getMaxVersion(formId); - - try { - const formObj = await import(`/opt/${formId}/v${formVersion}.js`); - - if (formObj?.form) { - const cleanedForm = convertRegexToString(formObj.form); - return response({ - statusCode: 200, - body: cleanedForm, - }); - } - } catch (importError: any) { - console.error("Error importing module:", importError); - return response({ - statusCode: 500, - body: { - error: importError.message - ? importError.message - : "Internal server error", - }, - }); - } - } catch (error: any) { - console.error("Error:", error); - return response({ - statusCode: 502, - body: { - error: error.message ? error.message : "Internal server error", - }, - }); - } -}; - -export function getMaxVersion(formId: string) { - const files = fs.readdirSync(`/opt/${formId}`); - if (!files) return undefined; - const versionNumbers = files?.map((fileName: string) => { - const match = fileName.match(/^v(\d+)\./); - if (match) { - return parseInt(match[1], 10); - } - return 1; - }); - return Math.max(...versionNumbers).toString(); -} - -export function getFilepathForIdAndVersion( - formId: string, - formVersion: string | undefined -): string | undefined { - if (formId && formVersion) { - return `/opt/${formId}/v${formVersion}.js`; - } - - const maxVersion = getMaxVersion(formId); - - if (!maxVersion) return undefined; - return `/opt/${formId}/v${maxVersion}.js`; -} - -export const handler = forms; diff --git a/src/services/api/handlers/getAllForms.ts b/src/services/api/handlers/getAllForms.ts index e274d3460..19297ba5c 100644 --- a/src/services/api/handlers/getAllForms.ts +++ b/src/services/api/handlers/getAllForms.ts @@ -1,54 +1,27 @@ import { response } from "../libs/handler"; -import * as fs from "fs"; -import * as path from "path"; +import { webformVersions } from "../webforms"; +import { FormSchema } from "shared-types"; -interface ObjectWithArrays { - [key: string]: string[]; -} +export const mapWebformsKeys = ( + webforms: Record> +): Record => { + const result: Record = {}; -export function removeTsAndJsExtentions( - obj: ObjectWithArrays -): ObjectWithArrays { - const result: ObjectWithArrays = {}; - - for (const key in obj) { - // eslint-disable-next-line no-prototype-builtins - if (obj.hasOwnProperty(key)) { - const filteredFiles = obj[key].filter((file) => !file.endsWith(".ts")); - result[key] = filteredFiles.map((f) => - f.replace(".js", "").replace("v", "") - ); - } - } - - return result; -} - -function getAllFormsAndVersions(directoryPath: string) { - const result: ObjectWithArrays = {}; - - const subDirectories = fs.readdirSync(directoryPath); - - subDirectories.forEach((subDir) => { - const subDirPath = path.join(directoryPath, subDir); - - if (fs.statSync(subDirPath).isDirectory()) { - const files = fs.readdirSync(subDirPath); - result[subDir] = files; - } + Object.entries(webforms).forEach(([key, value]) => { + result[key] = Object.keys(value); }); - return removeTsAndJsExtentions(result); -} + return result; +}; export const getAllForms = async () => { try { - const filePath = getAllFormsAndVersions("/opt/"); + const formsWithVersions = mapWebformsKeys(webformVersions); - if (filePath) { + if (formsWithVersions) { return response({ statusCode: 200, - body: filePath, + body: formsWithVersions, }); } } catch (error: any) { diff --git a/src/services/api/handlers/getForm.ts b/src/services/api/handlers/getForm.ts new file mode 100644 index 000000000..1c810ee22 --- /dev/null +++ b/src/services/api/handlers/getForm.ts @@ -0,0 +1,76 @@ +import { response } from "../libs/handler"; +import { APIGatewayEvent } from "aws-lambda"; +import { convertRegexToString } from "shared-utils"; +import { webformVersions } from "../webforms"; + +type GetFormBody = { + formId: string; + formVersion?: string; +}; + +export const getForm = async (event: APIGatewayEvent) => { + if (!event.body) { + return response({ + statusCode: 400, + body: { message: "Event body required" }, + }); + } + try { + const body = JSON.parse(event.body) as GetFormBody; + if (!body.formId) { + return response({ + statusCode: 400, + body: { error: "File ID was not provided" }, + }); + } + + const id = body.formId.toUpperCase(); + + if (!webformVersions[id]) { + return response({ + statusCode: 400, + body: { error: "Form ID not found" }, + }); + } + + let version = "v"; + if (body.formVersion) { + version += body.formVersion; + } else { + version += getMaxVersion(id); + } + + if (id && version) { + const formObj = await webformVersions[id][version]; + const cleanedForm = convertRegexToString(formObj); + return response({ + statusCode: 200, + body: cleanedForm, + }); + } + } catch (error: any) { + console.error("Error:", error); + return response({ + statusCode: 502, + body: { + error: error.message ? error.message : "Internal server error", + }, + }); + } + return response({ + statusCode: 500, + body: { + error: "Internal server error", + }, + }); +}; + +function getMaxVersion(id: string): string { + const webform = webformVersions[id]; + + const keys = Object.keys(webform); + keys.sort(); + return keys[keys.length - 1]; +} + +export const handler = getForm; diff --git a/src/services/api/handlers/tests/forms.test.ts b/src/services/api/handlers/tests/forms.test.ts deleted file mode 100644 index 345f546a6..000000000 --- a/src/services/api/handlers/tests/forms.test.ts +++ /dev/null @@ -1,93 +0,0 @@ -import * as fs from "fs"; -import { it, describe, expect, vi } from "vitest"; -import { forms } from "../forms"; -import { APIGatewayProxyEvent } from "aws-lambda/trigger/api-gateway-proxy"; - -describe("Forms Lambda Tests", () => { - it("should return 400 with error message if formId is not provided", async () => { - const event = { - body: JSON.stringify({}), - } as APIGatewayProxyEvent; - const result = await forms(event); - - expect(result?.statusCode).toBe(400); - expect(JSON.parse(result?.body as string)).toEqual({ - error: "File ID was not provided", - }); - }); - - // it("should return 500 with error message if filePath is not found", async () => { - // const event = { - // queryStringParameters: { formId: "test", formVersion: "1" }, - // } as any; - // const result = await forms(event); - - // expect(result?.statusCode).toBe(500); - // expect(JSON.parse(result?.body as string)).toEqual({ - // error: "ENOENT: no such file or directory, open '/opt/test/v1.js'", - // }); - // }); - - // it("should return 200 with JSON data if everything is valid", async () => { - // vi.spyOn(fs.promises, "readFile").mockResolvedValue( - // JSON.stringify({ key: "value" }) - // ); - - // const event = { - // queryStringParameters: { formId: "testform", formVersion: "1" }, - // } as any; - // const result = await forms(event); - - // expect(result?.statusCode).toBe(200); - // }); - - // it("should return 500 with a custom error message for other internal errors", async () => { - // vi.spyOn(fs.promises, "readFile").mockRejectedValue( - // new Error("Internal Server Error Message") - // ); - - // const event = { - // body: JSON.stringify({ formId: "testform", formVersion: "1" }), - // } as APIGatewayProxyEvent; - - // const result = await forms(event); - - // expect(result?.statusCode).toBe(500); - // expect(JSON.parse(result?.body as string)).toEqual({ - // error: "Internal Server Error Message", - // }); - // }); - - // it("should return the correct JSON data for different file versions", async () => { - // vi.spyOn(fs.promises, "readFile").mockImplementation(async (filePath) => { - // const filePathString = filePath.toString(); - // if (filePathString.includes("/opt/testform/v1.js")) { - // return Buffer.from(JSON.stringify({ version: "1", data: "v1 data" })); - // } else { - // return Buffer.from(JSON.stringify({ version: "2", data: "v2 data" })); - // } - // }); - - // const eventV1 = { - // body: JSON.stringify({ formId: "testform", formVersion: "1" }), - // } as APIGatewayProxyEvent; - // const eventV2 = { - // body: JSON.stringify({ formId: "testform", formVersion: "2" }), - // } as APIGatewayProxyEvent; - - // const resultV1 = await forms(eventV1); - // const resultV2 = await forms(eventV2); - - // expect(resultV1?.statusCode).toBe(200); - // expect(resultV2?.statusCode).toBe(200); - - // expect(JSON.parse(resultV1?.body as string)).toEqual({ - // version: "1", - // data: "v1 data", - // }); - // expect(JSON.parse(resultV2?.body as string)).toEqual({ - // version: "2", - // data: "v2 data", - // }); - // }); -}); diff --git a/src/services/api/handlers/tests/getAllForms.test.ts b/src/services/api/handlers/tests/getAllForms.test.ts new file mode 100644 index 000000000..f803e4e94 --- /dev/null +++ b/src/services/api/handlers/tests/getAllForms.test.ts @@ -0,0 +1,37 @@ +import { describe, it, expect, vi } from "vitest"; +import { getAllForms } from "../getAllForms"; +import { response } from "../../libs/handler"; + +// Mock the dependencies +vi.mock("../../libs/handler", () => ({ + response: vi.fn((arg) => arg), +})); + +vi.mock("../../webforms", () => ({ + webformVersions: { + ABP1: { + v202401: { name: "Test Form", data: "schema1" }, + v202402: { name: "Test Form", data: "schema2" }, + }, + ABP3: { + v202401: { name: "Test Form", data: "schema3" }, + }, + }, +})); + +describe("getAllForms", () => { + it("should return a response with status code 200 and the mapped webforms", async () => { + const expectedResponse = { + statusCode: 200, + body: { + ABP1: ["v202401", "v202402"], + ABP3: ["v202401"], + }, + }; + + const result = await getAllForms(); + expect(result?.statusCode).toEqual(200); + expect(result?.body).toEqual(expectedResponse.body); + expect(response).toHaveBeenCalledWith(expectedResponse); + }); +}); diff --git a/src/services/api/handlers/tests/getForm.test.ts b/src/services/api/handlers/tests/getForm.test.ts new file mode 100644 index 000000000..b90366ec2 --- /dev/null +++ b/src/services/api/handlers/tests/getForm.test.ts @@ -0,0 +1,51 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { getForm } from "../getForm"; + +vi.mock("../../webforms", () => ({ + webformVersions: { + TESTFORM: { + v022024: { name: "Test Form", data: "hello world" }, + }, + }, +})); + +describe("forms handler", () => { + beforeEach(() => { + // Reset mocks before each test + vi.resetAllMocks(); + }); + + it("returns 400 if event body is missing", async () => { + const event = {}; + const result = await getForm(event as any); // Casting as any to simulate APIGatewayEvent + expect(result.statusCode).toBe(400); + expect(result.body).toContain("Event body required"); + }); + + it("returns 400 if form ID is not provided", async () => { + const event = { + body: JSON.stringify({}), // Empty body + }; + const result = await getForm(event as any); + expect(result.statusCode).toBe(400); + expect(result.body).toContain("File ID was not provided"); + }); + + it("returns 400 if form ID is not found", async () => { + const event = { + body: JSON.stringify({ formId: "NONEXISTENT" }), + }; + const result = await getForm(event as any); + expect(result.statusCode).toBe(400); + expect(result.body).toContain("Form ID not found"); + }); + + it("returns 200 with form data if form ID and version are valid", async () => { + const event = { + body: JSON.stringify({ formId: "TESTFORM", formVersion: "022024" }), + }; + const result = await getForm(event as any); + expect(result.statusCode).toBe(200); + expect(result.body).toContain("Test Form"); + }); +}); diff --git a/src/services/api/layers/tsconfig.json b/src/services/api/layers/tsconfig.json deleted file mode 100644 index 38a9974ea..000000000 --- a/src/services/api/layers/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2016", - "moduleResolution": "node", - "module": "commonjs", - "skipLibCheck": true, - "esModuleInterop": true - }, - "include": ["./**/*.ts"], - "exclude": ["node_modules"] -} diff --git a/src/services/api/package.json b/src/services/api/package.json index 9e965295e..d98b13699 100644 --- a/src/services/api/package.json +++ b/src/services/api/package.json @@ -29,7 +29,6 @@ "version": "0.0.0", "scripts": { "build": "tsc", - "buildLayers": "tsc -p layers", "lint": "eslint '**/*.{ts,js}'", "compileRepack": "tsc -p repack", "test": "vitest", diff --git a/src/services/api/serverless.yml b/src/services/api/serverless.yml index fde871796..aac5ce62a 100644 --- a/src/services/api/serverless.yml +++ b/src/services/api/serverless.yml @@ -68,10 +68,6 @@ custom: package:compileEvents: - yarn compileRepack - ./repack/repack.js - scripts: - hooks: - package:initialize: | - yarn buildLayers params: master: formsProvisionedConcurrency: 2 @@ -236,9 +232,7 @@ functions: ${self:custom.vpc.privateSubnets} provisionedConcurrency: ${param:submitProvisionedConcurrency} forms: - handler: handlers/forms.handler - layers: - - !Ref FormsLambdaLayer + handler: handlers/getForm.handler maximumRetryAttempts: 0 environment: region: ${self:provider.region} @@ -246,7 +240,7 @@ functions: events: - http: path: /forms - method: get + method: post cors: true vpc: securityGroupIds: @@ -256,8 +250,6 @@ functions: provisionedConcurrency: ${param:searchProvisionedConcurrency} getAllForms: handler: handlers/getAllForms.handler - layers: - - !Ref FormsLambdaLayer maximumRetryAttempts: 0 environment: region: ${self:provider.region} @@ -273,10 +265,6 @@ functions: subnetIds: >- ${self:custom.vpc.privateSubnets} provisionedConcurrency: ${param:searchProvisionedConcurrency} -layers: - forms: - path: layers - description: Lambda Layer for forms function resources: Resources: ApiGateway400ErrorCount: diff --git a/src/services/api/tsconfig.json b/src/services/api/tsconfig.json index 1ade54517..6f70a5de2 100644 --- a/src/services/api/tsconfig.json +++ b/src/services/api/tsconfig.json @@ -4,8 +4,9 @@ "moduleResolution": "node", "module": "commonjs", "skipLibCheck": true, - "strict": true + "strict": true, + "noEmit": true }, "include": ["./**/*.ts"], "exclude": ["node_modules"] -} \ No newline at end of file +} diff --git a/src/services/api/webforms/ABP1/index.ts b/src/services/api/webforms/ABP1/index.ts new file mode 100644 index 000000000..03cbbdb18 --- /dev/null +++ b/src/services/api/webforms/ABP1/index.ts @@ -0,0 +1,2 @@ +export * from "./v202401"; +export * from "./v202402"; diff --git a/src/services/api/layers/ABP1/v202401.ts b/src/services/api/webforms/ABP1/v202401.ts similarity index 99% rename from src/services/api/layers/ABP1/v202401.ts rename to src/services/api/webforms/ABP1/v202401.ts index 13216e730..727a02980 100644 --- a/src/services/api/layers/ABP1/v202401.ts +++ b/src/services/api/webforms/ABP1/v202401.ts @@ -1,6 +1,6 @@ import { FormSchema } from "shared-types"; -const ABP1: FormSchema = { +export const v202401: FormSchema = { header: "ABP 1: Alternative Benefit Plan populations", sections: [ { @@ -1318,5 +1318,3 @@ const ABP1: FormSchema = { // }, ], }; - -export const form = ABP1; diff --git a/src/services/api/layers/ABP1/v202402.ts b/src/services/api/webforms/ABP1/v202402.ts similarity index 99% rename from src/services/api/layers/ABP1/v202402.ts rename to src/services/api/webforms/ABP1/v202402.ts index d3a76da0d..6da6722e7 100644 --- a/src/services/api/layers/ABP1/v202402.ts +++ b/src/services/api/webforms/ABP1/v202402.ts @@ -1,6 +1,6 @@ import { FormSchema } from "shared-types"; -const ABP1: FormSchema = { +export const v202402: FormSchema = { header: "ABP 1: Alternative Benefit Plan populations", sections: [ { @@ -1293,5 +1293,3 @@ const ABP1: FormSchema = { // }, ], }; - -export const form = ABP1; diff --git a/src/services/api/webforms/ABP10/index.ts b/src/services/api/webforms/ABP10/index.ts new file mode 100644 index 000000000..e5cf77f4f --- /dev/null +++ b/src/services/api/webforms/ABP10/index.ts @@ -0,0 +1 @@ +export * from "./v202401"; diff --git a/src/services/api/layers/ABP10/v202401.ts b/src/services/api/webforms/ABP10/v202401.ts similarity index 98% rename from src/services/api/layers/ABP10/v202401.ts rename to src/services/api/webforms/ABP10/v202401.ts index 6fd4c658c..3244236fa 100644 --- a/src/services/api/layers/ABP10/v202401.ts +++ b/src/services/api/webforms/ABP10/v202401.ts @@ -1,6 +1,6 @@ import { FormSchema } from "shared-types"; -const ABP10: FormSchema = { +export const v202401: FormSchema = { header: "ABP 10: General assurances", sections: [ { @@ -98,5 +98,3 @@ const ABP10: FormSchema = { }, ], }; - -export const form = ABP10; diff --git a/src/services/api/webforms/ABP3/index.ts b/src/services/api/webforms/ABP3/index.ts new file mode 100644 index 000000000..e5cf77f4f --- /dev/null +++ b/src/services/api/webforms/ABP3/index.ts @@ -0,0 +1 @@ +export * from "./v202401"; diff --git a/src/services/api/layers/ABP3/v202401.ts b/src/services/api/webforms/ABP3/v202401.ts similarity index 99% rename from src/services/api/layers/ABP3/v202401.ts rename to src/services/api/webforms/ABP3/v202401.ts index 1b846b4a6..5fa711588 100644 --- a/src/services/api/layers/ABP3/v202401.ts +++ b/src/services/api/webforms/ABP3/v202401.ts @@ -1,6 +1,6 @@ import { FormSchema } from "shared-types"; -const ABP3: FormSchema = { +export const v202401: FormSchema = { header: "ABP 3: Selection of benchmark benefit package or benchmark-equivalent benefit package", sections: [ @@ -355,5 +355,3 @@ const ABP3: FormSchema = { }, ], }; - -export const form = ABP3; diff --git a/src/services/api/webforms/ABP3_1/index.ts b/src/services/api/webforms/ABP3_1/index.ts new file mode 100644 index 000000000..e5cf77f4f --- /dev/null +++ b/src/services/api/webforms/ABP3_1/index.ts @@ -0,0 +1 @@ +export * from "./v202401"; diff --git a/src/services/api/layers/ABP3_1/v202401.ts b/src/services/api/webforms/ABP3_1/v202401.ts similarity index 99% rename from src/services/api/layers/ABP3_1/v202401.ts rename to src/services/api/webforms/ABP3_1/v202401.ts index 38e98506d..8d3e29cc7 100644 --- a/src/services/api/layers/ABP3_1/v202401.ts +++ b/src/services/api/webforms/ABP3_1/v202401.ts @@ -1,6 +1,6 @@ import { FormSchema } from "shared-types"; -const ABP3_1: FormSchema = { +export const v202401: FormSchema = { header: "ABP 3.1 Selection of benchmark benefit package or benchmark-equivalent benefit package", sections: [ @@ -1785,5 +1785,3 @@ const ABP3_1: FormSchema = { }, ], }; - -export const form = ABP3_1; diff --git a/src/services/api/webforms/index.ts b/src/services/api/webforms/index.ts new file mode 100644 index 000000000..9dd0b9450 --- /dev/null +++ b/src/services/api/webforms/index.ts @@ -0,0 +1,21 @@ +import * as ABP1 from "./ABP1"; +import * as ABP10 from "./ABP10"; +import * as ABP3 from "./ABP3"; +import * as ABP3_1 from "./ABP3_1"; +import { FormSchema } from "shared-types"; + +export const webformVersions: Record> = { + ABP1: { + v202401: ABP1.v202401, + v202402: ABP1.v202402, + }, + ABP3: { + v202401: ABP3.v202401, + }, + ABP3_1: { + v202401: ABP3_1.v202401, + }, + ABP10: { + v202401: ABP10.v202401, + }, +}; diff --git a/src/services/ui/e2e/tests/a11y/index.spec.ts b/src/services/ui/e2e/tests/a11y/index.spec.ts index c1a38edd4..e2a8fd677 100644 --- a/src/services/ui/e2e/tests/a11y/index.spec.ts +++ b/src/services/ui/e2e/tests/a11y/index.spec.ts @@ -39,10 +39,11 @@ test.describe("test a11y on static routes", () => { const webformRoutes = [ "/webforms", "/guides/abp", - "/webform/abp10/1", - "/webform/abp3_1/1", - "/webform/abp3/1", - "/webform/abp1/1", + "/webform/abp10/202401", + "/webform/abp3_1/202401", + "/webform/abp3/202401", + "/webform/abp1/202401", + "/webform/abp1/202402", ]; test.describe("test a11y on webform routes", () => { diff --git a/src/services/ui/src/api/useGetForm.ts b/src/services/ui/src/api/useGetForm.ts index fb09f80b1..9367cf938 100644 --- a/src/services/ui/src/api/useGetForm.ts +++ b/src/services/ui/src/api/useGetForm.ts @@ -8,8 +8,8 @@ export const getForm = async ( formId: string, formVersion?: string ): Promise => { - const form = await API.get("os", "/forms", { - queryStringParameters: { formId, formVersion }, + const form = await API.post("os", "/forms", { + body: { formId, formVersion }, }); return reInsertRegex(form); diff --git a/src/services/ui/src/components/Webform/index.tsx b/src/services/ui/src/components/Webform/index.tsx index c5f8d75ec..4651b4836 100644 --- a/src/services/ui/src/components/Webform/index.tsx +++ b/src/services/ui/src/components/Webform/index.tsx @@ -9,6 +9,7 @@ import { Footer } from "./footer"; import { Link, useParams } from "../Routing"; import { useReadOnlyUser } from "./useReadOnlyUser"; import { useState } from "react"; +import { FormSchema } from "shared-types"; export const Webforms = () => { return ( @@ -58,7 +59,7 @@ export const Webforms = () => { interface WebformBodyProps { id: string; version: string; - data: any; + data: FormSchema; readonly: boolean; values: any; }