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

Patch Admin Notes #46

Merged
merged 1 commit into from
Apr 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions apps/backend/src/db/qldb.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import { QLDBSessionClientConfig } from '@aws-sdk/client-qldb-session';
import { NodeHttpHandlerOptions } from '@aws-sdk/node-http-handler';
import {
QldbDriver,
RetryConfig,
TransactionExecutor,
} from 'amazon-qldb-driver-nodejs';
import { QldbDriver, RetryConfig } from 'amazon-qldb-driver-nodejs';
import { Agent } from 'https';

// IMPORTANT: this must match the ledger name defined in template.yaml
Expand Down
11 changes: 11 additions & 0 deletions apps/backend/src/db/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { AdminNotes } from '../schema/schema.js';
import { qldbDriver, tableName } from './qldb.js';

export async function insertDocument(
Expand All @@ -24,3 +25,13 @@ export async function fetchDocumentById(id: string) {
return result.getResultList();
});
}

export async function updateDocumentAdminNotes(id: string, notes: AdminNotes) {
return await qldbDriver.executeLambda(async (txn) => {
return await txn.execute(
`UPDATE ${tableName} as f SET f.adminNotes = ? where f.id = ?`,
notes,
id
);
});
}
55 changes: 55 additions & 0 deletions apps/backend/src/handlers/patchForm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { APIGatewayEvent } from 'aws-lambda';
import { createTableIfNotExists } from '../db/createTable.js';
import { updateDocumentAdminNotes } from '../db/utils.js';
import { AdminNotes, adminNotesSchema } from '../schema/schema.js';
/**
* An HTTP post method to add one form to the QLDB table.
*/
export const patchFormHandler = async (event: APIGatewayEvent) => {
const headers = {
'Access-Control-Allow-Headers': 'Content-Type, Access-Control-Allow-Origin',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'PATCH',
};

if (event.httpMethod !== 'PATCH') {
return {
statusCode: 400,
headers,
body: `patchMethod only accepts PATCH method, you tried: ${event.httpMethod} method.`,
};
}
// All log statements are written to CloudWatch
console.info('received:', event);

const id = event.pathParameters!.id!;
const JSONbody = JSON.parse(event.body!);
let notes: AdminNotes;
try {
notes = adminNotesSchema.parse(JSONbody);
} catch (error) {
const errorMessage = 'Admin notes does not match schema. Error: ' + error;
console.error(errorMessage);
return {
statusCode: 400,
headers,
body: errorMessage,
};
}

try {
await createTableIfNotExists();
await updateDocumentAdminNotes(id, notes);
return {
statusCode: 201,
headers,
};
} catch (error) {
console.error(error);
return {
statusCode: 500,
headers,
body: 'Error updating database.',
};
}
};
5 changes: 4 additions & 1 deletion apps/backend/src/schema/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ const adminNoteSchema = z.object({
updatedAt: dateSchema,
});

export const adminNotesSchema = adminNoteSchema.array();
export type AdminNotes = z.infer<typeof adminNotesSchema>;

// Part of form to be filled out by the child's parent/legal guardian
const guardianFormSchema = z.object({
childsName: z.string().min(1),
Expand Down Expand Up @@ -60,5 +63,5 @@ export const formSchema = z.object({
id: z.string(),
guardianForm: guardianFormSchema,
medicalForm: medicalFormSchema,
adminNotes: adminNoteSchema.array(),
adminNotes: adminNotesSchema,
});
20 changes: 19 additions & 1 deletion apps/backend/template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Globals:
Api:
TracingEnabled: true
Cors:
AllowMethods: "'GET,POST,OPTIONS'"
AllowMethods: "'GET,POST,PATCH,OPTIONS'"
AllowHeaders: "'content-type, Access-Control-Allow-Origin'"
AllowOrigin: "'*'"

Expand Down Expand Up @@ -142,6 +142,24 @@ Resources:
Method: POST
RestApiId: !Ref ApiGateway

patchFormFunction:
Type: AWS::Serverless::Function
Properties:
Handler: dist/handlers/patchForm.patchFormHandler
Runtime: nodejs18.x
Architectures:
- x86_64
MemorySize: 128
Timeout: 100
Description: An HTTP patch method to update a document in the QLDB table.
Role: !GetAtt QLDBSendCommandRole.Arn
Events:
Api:
Type: Api
Properties:
Path: /form/{id}/notes
Method: PATCH
RestApiId: !Ref ApiGateway
# Creates an AWS KMS key to encrypt/decrypt data in QLDB
# the key policy gives root account users key management permissions
# and gives the IAM role given to the Lambdas permission to use the key (necessary to interact with QLDB)
Expand Down
78 changes: 78 additions & 0 deletions apps/frontend/src/components/adminNotes/AdminNotes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { Button, Flex, Heading, Text, Textarea } from '@chakra-ui/react';
import { useState } from 'react';
import { AdminNotes } from '../../types/formSchema';
import { patchAdminNotes } from '../../utils/sendRequest';
import { useParams } from 'react-router-dom';

interface AdminNotesProps {
notes: AdminNotes;
}

export const ViewAdminNotes: React.FC<AdminNotesProps> = ({ notes }) => {
const [savedNotes, setSavedNotes] = useState(notes);
const [newNote, setNewNote] = useState('');
const [error, setError] = useState<null | string>(null);

const { id } = useParams();
const onSave = () => {
if (id) {
const newSavedNotes = [
{ note: newNote, updatedAt: new Date() },
...savedNotes,
];
patchAdminNotes(id, newSavedNotes)
.then(() => {
setSavedNotes(newSavedNotes);
setNewNote('');
})
.catch((e) => {
setError('Error encountered while saving note.');
console.error(e);
});
}
};
return (
<Flex flexDirection="column" marginTop="40px">
<Heading size="md" marginBottom="16px">
Admin Notes
</Heading>
<Flex flexDirection="column" maxWidth="60ch">
{error && (
<Text color="red" marginBottom="8px">
{error}
</Text>
)}

<Textarea
placeholder="Create a new note"
value={newNote}
onChange={(e) => {
setNewNote(e.target.value);
}}
/>

<Button
colorScheme="teal"
margin="16px 0px 40px"
isDisabled={newNote === ''}
onClick={onSave}
>
Save
</Button>
</Flex>
<Heading size="md" marginBottom="16px">
Previous Notes
</Heading>
{savedNotes
.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime())
.map((note) => (
<Flex flexDirection="column" key={`${note.note}`}>
<Text size="xs" color="gray.500" margin="4px">
Last updated: {note.updatedAt.toLocaleString()}
</Text>
<Textarea maxWidth="60ch" value={note.note} readOnly={true} />
</Flex>
))}
</Flex>
);
};
5 changes: 4 additions & 1 deletion apps/frontend/src/constants/endpoints.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
// TODO: For testing locally, replace with aws api gateway url
export const BASE_URL =
'https://bk3ffpsl08.execute-api.us-east-1.amazonaws.com/Prod/';
'https://bk3ffpsl08.execute-api.us-east-1.amazonaws.com/Prod';

export const GET_ALL_FORMS_URL = `${BASE_URL}/forms`;

export const GET_FORM_BY_ID_URL = (id: string) => `${BASE_URL}/form/${id}`;

export const POST_FORM_URL = `${BASE_URL}/form`;

export const PATCH_ADMIN_NOTES_URL = (id: string) =>
`${BASE_URL}/form/${id}/notes`;
4 changes: 3 additions & 1 deletion apps/frontend/src/pages/OneFormPage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Container, Flex, Heading, Spacer, Spinner } from '@chakra-ui/react';
import { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import { ViewAdminNotes } from '../components/adminNotes/AdminNotes';
import { ErrorMessage } from '../components/ErrorMessage';
import { GuardianForm } from '../components/viewForm/GuardianForm';
import { MedicalForm } from '../components/viewForm/MedicalForm';
Expand Down Expand Up @@ -33,7 +34,7 @@ const OneFormPage: React.FC = () => {
return <ErrorMessage message={error} />;
} else {
return (
<Container maxWidth="90ch" padding="0px 32px 32px">
<Container maxWidth="90ch" padding="0px 32px 80px">
<Heading
size="lg"
textAlign="center"
Expand All @@ -51,6 +52,7 @@ const OneFormPage: React.FC = () => {
)}
{formData && <GuardianForm guardianForm={formData.guardianForm} />}
{formData && <MedicalForm medicalForm={formData.medicalForm} />}
{formData && <ViewAdminNotes notes={formData.adminNotes} />}
</Container>
);
}
Expand Down
13 changes: 9 additions & 4 deletions apps/frontend/src/types/formSchema.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { Asserts } from 'yup';
import * as Yup from 'yup';

const addressSchema = Yup.object({
Expand All @@ -14,6 +15,13 @@ const adminNoteSchema = Yup.object().shape({
.required(),
});

export const adminNotesSchema = Yup.array()
.of(adminNoteSchema)
.required()
.default(() => []);

export type AdminNotes = Asserts<typeof adminNotesSchema>;

export const guardianFormSchema = Yup.object().shape({
childsName: Yup.string().min(1).required(),
dob: Yup.date()
Expand Down Expand Up @@ -59,8 +67,5 @@ export const formSchema = Yup.object().shape({
id: Yup.string().default(''),
guardianForm: guardianFormSchema,
medicalForm: medicalFormSchema,
adminNotes: Yup.array()
.of(adminNoteSchema)
.required()
.default(() => []),
adminNotes: adminNotesSchema,
});
10 changes: 9 additions & 1 deletion apps/frontend/src/utils/sendRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import { FormValues } from '../components/form/Form';
import {
GET_ALL_FORMS_URL,
GET_FORM_BY_ID_URL,
PATCH_ADMIN_NOTES_URL,
POST_FORM_URL,
} from '../constants/endpoints';
import { formSchema } from '../types/formSchema';
import { FormData } from '../types/formData';
import { AdminNotes, formSchema } from '../types/formSchema';

export const submitForm = async (body: FormValues): Promise<void> => {
try {
Expand All @@ -31,3 +32,10 @@ export const getAllForms = async (): Promise<FormData[]> => {
export const getFormById = async (id: string): Promise<AxiosResponse> => {
return await axios.get(GET_FORM_BY_ID_URL(id));
};

export const patchAdminNotes = async (
id: string,
notes: AdminNotes
): Promise<AxiosResponse> => {
return await axios.patch(PATCH_ADMIN_NOTES_URL(id), notes);
};