Skip to content

Commit

Permalink
Merge pull request #46 from Code-4-Community/ml-patch-admin-notes
Browse files Browse the repository at this point in the history
Patch Admin Notes
  • Loading branch information
jackielincroft authored Apr 16, 2023
2 parents 18e2284 + 31754f5 commit c4c0dc5
Show file tree
Hide file tree
Showing 10 changed files with 193 additions and 14 deletions.
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);
};

0 comments on commit c4c0dc5

Please sign in to comment.