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

feat(lamdba): add admin function for NOSO 2 #1060

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
5 changes: 4 additions & 1 deletion lib/lambda/sinkChangelog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
transformUpdateValuesSchema,
transformDeleteSchema,
transformedUpdateIdSchema,
transformSubmitValuesSchema,
} from "./update/adminChangeSchemas";
import { getPackageChangelog } from "libs/api/package";

Expand Down Expand Up @@ -67,7 +68,9 @@ const processAndIndex = async ({
// query all changelog entries for this ID and create copies of all entries with new ID
if (record.isAdminChange) {
const schema = transformDeleteSchema(offset).or(
transformUpdateValuesSchema(offset).or(transformedUpdateIdSchema),
transformUpdateValuesSchema(offset)
.or(transformedUpdateIdSchema)
.or(transformSubmitValuesSchema),
);

const result = schema.safeParse(record);
Expand Down
4 changes: 3 additions & 1 deletion lib/lambda/sinkMainProcessors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ import {
deleteAdminChangeSchema,
updateValuesAdminChangeSchema,
updateIdAdminChangeSchema,
submitNOSOAdminSchema,
} from "./update/adminChangeSchemas";

const removeDoubleQuotesSurroundingString = (str: string) => str.replace(/^"|"$/g, "");
const adminRecordSchema = deleteAdminChangeSchema
.or(updateValuesAdminChangeSchema)
.or(updateIdAdminChangeSchema);
.or(updateIdAdminChangeSchema)
.or(submitNOSOAdminSchema);

type OneMacRecord = {
id: string;
Expand Down
22 changes: 22 additions & 0 deletions lib/lambda/update/adminChangeSchemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,25 @@ export const transformedUpdateIdSchema = updateIdAdminChangeSchema.transform((da
id: `${data.id}`,
timestamp: Date.now(),
}));

export const submitNOSOAdminSchema = z
.object({
id: z.string(),
authority: z.string(),
status: z.string(),
submitterEmail: z.string(),
submitterName: z.string(),
adminChangeType: z.literal("NOSO"),
changeMade: z.string(),
changeReason: z.string(),
})
.and(z.record(z.string(), z.any()));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this supposed to be the attachments property? What are we expecting here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added this so when I add the properties in "produceMessage" then it is sent to sinkMain those properties aren't cut out.

(New properties I want to stay: origin, isAdminChange, state, makoChangedDate, changedDate, statusDate

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay and also I left a confusing comment that I have since deleted but for this type of NOSO's we don't need to have attachments.


export const transformSubmitValuesSchema = submitNOSOAdminSchema.transform((data) => ({
...data,
adminChangeType: "NOSO",
event: "NOSO",
id: data.id,
packageId: data.id,
timestamp: Date.now(),
}));
111 changes: 111 additions & 0 deletions lib/lambda/update/submitNOSO.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { handler } from "./submitNOSO";
import { APIGatewayEvent } from "node_modules/shared-types";

import { NOT_EXISTING_ITEM_ID, TEST_ITEM_ID } from "mocks";

vi.mock("libs/handler-lib", () => ({
response: vi.fn((data) => data),
}));

describe("handler", () => {
beforeEach(() => {
vi.clearAllMocks();
process.env.topicName = "test-topic";
});

it("should return 400 if event body is missing", async () => {
const event = {} as APIGatewayEvent;
const result = await handler(event);
const expectedResult = { statusCode: 400, body: { message: "Event body required" } };

expect(result).toStrictEqual(expectedResult);
});

it("should return 400 if package ID is not found", async () => {
const noActionevent = {
body: JSON.stringify({ packageId: "123", changeReason: "Nunya", authority: "test" }),
} as APIGatewayEvent;

const resultPackage = await handler(noActionevent);

expect(resultPackage?.statusCode).toBe(400);
});
it("should return 400 if admingChangeType is not found", async () => {
const noApackageEvent = {
body: JSON.stringify({ action: "123", changeReason: "Nunya" }),
} as APIGatewayEvent;

const resultAction = await handler(noApackageEvent);

expect(resultAction?.statusCode).toBe(400);
});
it("should return 400 if existing item is entered", async () => {
const noActionevent = {
body: JSON.stringify({
id: TEST_ITEM_ID,
adminChangeType: "NOSO",
authority: "SPA",
submitterEmail: "[email protected]",
submitterName: "Name",
status: "submitted",
changeMade: "change",
changeReason: "reason",
}),
} as APIGatewayEvent;

const result = await handler(noActionevent);

const expectedResult = {
statusCode: 400,
body: { message: `Package with id: ${TEST_ITEM_ID} already exists.` },
};
expect(result).toStrictEqual(expectedResult);
});

it("should submit a new item", async () => {
const validItem = {
body: JSON.stringify({
id: NOT_EXISTING_ITEM_ID,
authority: "Medicaid SPA",
status: "submitted",
submitterEmail: "[email protected]",
submitterName: "Name",
adminChangeType: "NOSO",
changeMade: "change",
changeReason: "reason",
}),
} as APIGatewayEvent;

const result = await handler(validItem);

const expectedResult = {
statusCode: 200,
body: { message: `${NOT_EXISTING_ITEM_ID} has been submitted.` },
};
expect(result).toStrictEqual(expectedResult);
});

it("should fail to create a package ID with no topic name", async () => {
process.env.topicName = "";
const validItem = {
body: JSON.stringify({
id: NOT_EXISTING_ITEM_ID,
authority: "Medicaid SPA",
status: "submitted",
submitterEmail: "[email protected]",
submitterName: "Name",
adminChangeType: "NOSO",
changeMade: "change",
changeReason: "reason",
}),
} as APIGatewayEvent;

const result = await handler(validItem);
const expectedResult = {
statusCode: 500,
body: { message: "Topic name is not defined" },
};
expect(result).toStrictEqual(expectedResult);
});
});
98 changes: 98 additions & 0 deletions lib/lambda/update/submitNOSO.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { response } from "libs/handler-lib";
import { APIGatewayEvent } from "aws-lambda";
import { produceMessage } from "libs/api/kafka";
import { getPackage } from "libs/api/package";
import { ItemResult } from "shared-types/opensearch/main";
import { submitNOSOAdminSchema } from "./adminChangeSchemas";
import { z } from "zod";

import { getStatus } from "shared-types";

interface submitMessageType {
id: string;
authority: string;
status: string;
submitterEmail: string;
submitterName: string;
adminChangeType: string;
stateStatus: string;
cmsStatus: string;
}

const sendSubmitMessage = async (item: submitMessageType) => {
const topicName = process.env.topicName as string;
if (!topicName) {
throw new Error("Topic name is not defined");
}

const currentTime = Date.now();

await produceMessage(
topicName,
item.id,
JSON.stringify({
...item,
packageId: item.id,
origin: "SEATool",
isAdminChange: true,
adminChangeType: "NOSO",
description: null,
event: "NOSO",
state: item.id.substring(0, 2),
makoChangedDate: currentTime,
changedDate: currentTime,
statusDate: currentTime,
}),
);

return response({
statusCode: 200,
body: { message: `${item.id} has been submitted.` },
});
};

export const handler = async (event: APIGatewayEvent) => {
if (!event.body) {
return response({
statusCode: 400,
body: { message: "Event body required" },
});
}

try {
const item = submitNOSOAdminSchema.parse(
typeof event.body === "string" ? JSON.parse(event.body) : event.body,
);
Comment on lines +63 to +65
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there an advantage to this check? If event.body is null, wouldn't that throw an error in JSON.parse, which will then be caught by the catch block?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a weird thing that I do actually need, in aws the lamdba body is actually sent as a JSON object but if it was used as backend call it would be a string and actually the type: APIGatewayEvent.

Since this is the last comment, thanks for the thorough review!


const { stateStatus, cmsStatus } = getStatus(item.status);
// check if it already exsists
const currentPackage: ItemResult | undefined = await getPackage(item.id);

if (currentPackage && currentPackage.found == true) {
// if it exists and has origin OneMAC we shouldn't override it
if (currentPackage._source.origin === "OneMAC") {
return response({
statusCode: 400,
body: { message: `Package with id: ${item.id} already exists.` },
});
}
//otherwise we need to add the property origin so it shows up on our dashboard
item["origin"] = "SEATool";
}

return await sendSubmitMessage({ ...item, stateStatus, cmsStatus });
} catch (err) {
console.error("Error has occured submitting package:", err);
if (err instanceof z.ZodError) {
return response({
statusCode: 400,
body: { message: err.errors },
});
}

return response({
statusCode: 500,
body: { message: err.message || "Internal Server Error" },
});
}
};
3 changes: 2 additions & 1 deletion lib/packages/shared-types/opensearch/changelog/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@ export type Document = Omit<AppkDocument, "event"> &
| "withdraw-rai"
| "update-values"
| "update-id"
| "delete";
| "delete"
| "NOSO";
};

export type Response = Res<Document>;
Expand Down
11 changes: 11 additions & 0 deletions lib/stacks/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,17 @@ export class Api extends cdk.NestedStack {
indexNamespace,
},
},
{
id: "submitNOSO",
entry: join(__dirname, "../lambda/update/submitNOSO.ts"),
environment: {
dbInfoSecretName,
topicName,
brokerString,
osDomain: `https://${openSearchDomainEndpoint}`,
indexNamespace,
},
},
{
id: "getSystemNotifs",
entry: join(__dirname, "../lambda/getSystemNotifs.ts"),
Expand Down
3 changes: 2 additions & 1 deletion react-app/src/features/package/admin-changes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,13 @@ export const AdminChange: FC<opensearch.changelog.Document> = (props) => {
}
return ["Disable Formal RAI Response Withdraw", AC_WithdrawDisabled];
}
case "NOSO":
case "legacy-admin-change":
return [props.changeType || "Manual Update", AC_LegacyAdminChange];
default:
return [BLANK_VALUE, AC_Update];
}
}, [props.actionType, props.changeType]);
}, [props.event, props.changeType, props.raiWithdrawEnabled]);

return (
<AccordionItem key={props.id} value={props.id}>
Expand Down
Loading