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

BL-751-BE-Build an Email Service #124

Draft
wants to merge 13 commits into
base: main
Choose a base branch
from
Draft
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,4 @@ jspm_packages/
.env
.env.test
.env.local
sendgrid.env
75 changes: 60 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,24 +72,24 @@ avatarUr(not required): string,
}
```

| Method | URL | Description |
| -------- | ------------------------- | --------------------------------------------------------------------------------- |
| [GET] | /children | Returns an array containing all existing children.|
| [POST] | /children | Requires a username, name, and age. Returns the name, profile_id, and parent_id.|
| [GET] | /children/:child_id | Returns the child with the given 'id'.|
| [PUT] | /children/:child_id | Returns the updated child object|
| [DELETE] | /children/:child_id | Returns the name of the child deleted|
| [GET] | /children/:child_id/enrollments | Returns an array filled with event objects with the specified `id`. |
| [POST] | /children/:child_id/enrollments | Returns the event object with the specified `id`. Enrolls a student. |
| [PUT] | /children/enrollments/ | Returns the event object with the specified `id`. Updates a student's enrollments. <b>(Not Implemented)</b>|
| [DELETE] | /children/enrollments/:id | Returns the event object with the specified `id`. Unenrolls student from course. <b>(Not Implemented)</b>|

| Method | URL | Description |
| -------- | ------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| [GET] | /children | Returns an array containing all existing children. |
| [POST] | /children | Requires a username, name, and age. Returns the name, profile_id, and parent_id. |
| [GET] | /children/:child_id | Returns the child with the given 'id'. |
| [PUT] | /children/:child_id | Returns the updated child object |
| [DELETE] | /children/:child_id | Returns the name of the child deleted |
| [GET] | /children/:child_id/enrollments | Returns an array filled with event objects with the specified `id`. |
| [POST] | /children/:child_id/enrollments | Returns the event object with the specified `id`. Enrolls a student. |
| [PUT] | /children/enrollments/ | Returns the event object with the specified `id`. Updates a student's enrollments. <b>(Not Implemented)</b> |
| [DELETE] | /children/enrollments/:id | Returns the event object with the specified `id`. Unenrolls student from course. <b>(Not Implemented)</b> |

<h1>Instructors</h1>

```
{
instructor_id: INCREMENT (primary key, auto-increments, generated by database),
instructor_name: STRING (required),
rating: INTEGER (required),
availability: STRING (optional),
bio: STRING (required),
Expand Down Expand Up @@ -140,14 +140,15 @@ avatarUr(not required): string,
end_date: DATE (required),
location: STRING (required),
number_of_sessions: INTEGER (required),
instructor_name: STRING (required)
}
```

| Method | URL | Description |
| -------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------- |
| [GET] | /course | Returns an array containing all course objects |
| [GET] | /course/:course_id | Returns the course object with the specified `course_id`. |
| [POST] | /course | --needs to be fleshed out-- |
| [POST] | /course | --needs to be fleshed out-- |
| [PUT] | /course/:course_id | Updates and returns the updated course object with the specified `course_id`. |
| [DELETE] | /course/:course_id | Deletes the course object with the specified `course_id` and returns a message containing the deleted course_id on successful deletion |

Expand Down Expand Up @@ -180,8 +181,8 @@ avatarUr(not required): string,
}
```

| Method | URL | Description |
| -------- | ------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| Method | URL | Description |
| -------- | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| [GET] | /conversation_id/ | Returns an array filled with inbox event objects. |
| [GET] | /conversation_id/:profile_id/ | Retrieves an inbox with the specified inbox_id <b>BUG(?): incorrectly labeled as profile_id in codebase rather than inbox_id</b> |
| [POST] | /conversation_id/ | Creates an inbox and returns the newly created inbox. |
Expand Down Expand Up @@ -300,3 +301,47 @@ Visual Database Schema: https://dbdesigner.page.link/WTZRbVeTR7EzLvs86 <b>\*Curr
[Loom Video PT4](https://www.loom.com/share/7da5fc043d3149afb05876c28df9bd3d)

<br />

<h2>Email Service</h2>
In api/email, the emailHelper.js file contains the following functions: sendEmail and addToList. SendEmail sends an API request to SendGrid to send a templated email to the email address its given. The other function: addToList adds the email and name to a specified SendGrid contact list (they're added to all no matter what, then also to the specified list id). Any function that wants to use sendEmail and addToList will need to import it into their file (see profileRouter.js). Parameters can be passed into it, like the toEmail, name, template_id or list_ids (which is an array so it could have more than 1 list_ids there). The parameters to use are created in the file that's calling the sendEmail function.

SendGrid's npm package info and how to set up the API key (also listed below): https://www.npmjs.com/package/@sendgrid/mail. The npm package is @sendgrid/mail. Very lightweight, highly supported, and heavily downloaded. (SendGrid also supports Java)
Copy link
Contributor

Choose a reason for hiding this comment

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

Awesome! This is great documentation for people working on this in the future. I am sure they will appreciate how thorough you are.


Loom walkthrough on backend: https://www.loom.com/share/a7d867ee6f9f4bca9a4a3e911bca3de4
Loom how to see if it's working: https://www.loom.com/share/7acaa082c8454ac4bf17e0670aec18d7

To set up SendGrid at https://sendgrid.com/:

- Create an account with SendGrid
- Create an API (replace YOUR_API_KEY with the API you receive from SendGrid into the below terminal command)
- In your terminal, do these commands:
echo "export SENDGRID_API_KEY='YOUR_API_KEY'" > sendgrid.env
echo "sendgrid.env" >> .gitignore
source ./sendgrid.env

SendGrid will look at its own .env file for the API key. Unclear as to whether it's possible to put other environment variables there - it's been inconsistent in testing. (Send new keys via the first and third commands listed above. The second one only adds sendgrid.env to .gitignore so you don't share secrets.) When accessing SendGrid's .env file, use process.env.SENDGRID_API_KEY, just as if you had put the key in the .env file.

Templates exist on SendGrid's site, under "Dynamic Templates." When requesting an email be sent, you'll use the template_id as one of the parameters you pass on to sendEmail. If you want a starting spot for what your emails could say (pending stakeholder approval), here's some copy: https://docs.google.com/document/d/1WZQ6Njj0Xt_eXLAEWm0nYA7eqACByFinpyrh7EjcU8U/edit?usp=sharing

Dynamic template data: the template will be looking for something from the json request that matches the field you put in the template. For example, "name": "Some Name" coming from your email request will match {{ name }} in the template. Double curly braces make that connection.

Potential templates to be created plus an idea for what the template_id will look like (could update this list as the templates are created on SendGrid - the template_id is generated by SendGrid when you create a Dynamic Template):

EmailType = {
WELCOME_EMAIL_PARENT: 'd-026a2f461bdd480098be08a2cb949eea', (All 3 welcome emails go out automatically upon post from the profileRouter. Change template id based on role_id.)
WELCOME_EMAIL_INSTRUCTOR: 'd-a4de80911362438bb35d481efa068398',
WELCOME_EMAIL_STUDENT: 'd-026a2f461bdd480098be08a2cb949eea',
INSTRUCTOR_SUBMITTED_APPLICATION: 'd-026a2f461bdd480098be08bxhsyeyyy', (Goes out when an instructor submits an application and lets them know about the next step in the approval process.)
INSTRUCTOR_APPLICATION_REVIEW: 'd-026a2f461bdd480098be08bxhsyeyyy', email to admin or staff that an application is ready to be approved or denied
CHANGE_PASSWORD: 'd-026a2f4hsyw20098be08a2cb949eea',
PURCHASED_COURSE_PARENT: 'd-026a2f461bdd480098be08bxhsyeyyy', (Upon enrollment of a child student, to include the start date, time, and Zoom link)
PURCHASED_COURSE_STUDENT: 'd-026a2f461bdd480098be08bxhsyeyyy', (Upon enrollment of a child student, email sent to student to include the start date, time, and Zoom link)
INSTRUCTOR_IS_APPROVED: 'd-026a2f461bdd480098be08bxhsyeyyy', (Congrats, you've been approved, with link to create a course)
INSTRUCTOR_IS_REJECTED: 'd-026a2f461bdd480098be08bxhsyeyyy', (Sorry, friend! We'll keep your info on file for future needs)
STUDENT_CLASS_REMINDER: 'd-026a2f461bdd480098be08bxhsyeyyy', (Could be 2 emails to land 1 hour, or 5 minutes before class starts, with the class start time and Zoom link)
PARENT_CLASS_REMINDER: 'd-026a2f461bdd480098be08bxhsyeyyy', (Same as above with different wording)
};

AddToList: First up , add list(s) to the SendGrid account. In addition to the default "all" list, we added instructors, parents, and students. From a marketing perspective, these are the major groups, each requiring a different kind of information, and will potentially need different mass email types.

As part of the request in emailHelper.js, you'll send the id(s) of the contact list (list_ids, an array) you want to add the email to, plus any additional data you want to add to the request, like the email address (as 'email') and name (as 'first_name'). If you use custom fields, make sure the fields are already set up in SendGrid or SG won't know where to map them.
Copy link
Contributor

Choose a reason for hiding this comment

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

This is fantastic. You've done an excellent job explaining how the process works. I feel like I could set up SendGrid after reading these instructions. Fantastic work.

105 changes: 105 additions & 0 deletions __tests__/routes/emailHelper.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
const express = require('express');

const { sendEmail, addToList } = require('../../api/email/emailHelper');
const server = express();

server.use(express.json());

jest.mock('../../api/email/emailHelper.js');
jest.mock('../../api/email/emailHelper.js', () =>
jest.fn((req, res, next) => next())
);

const newStudent = {
dynamic_template_data: {
name: 'New Student Here',
},
to: '[email protected]',
// The from email must be the email address of a verified sender in SendGrid account. If/when you verify the domain, an email coming from the domain is likely good enough.
from: '[email protected]',
template_id: 'd-a6dacc6241f9484a96554a13bbdcd971',
};

const newParent = {
dynamic_template_data: {
name: 'New Parent Here',
},
to: '[email protected]',
from: '[email protected]',
template_id: 'd-19b895416ae74cea97e285c4401fcc1f',
};

const newInstructor = {
dynamic_template_data: {
name: 'New Parent Here',
},
to: '[email protected]',
from: '[email protected]',
template_id: 'd-a4de80911362438bb35d481efa068398',
};

const newStudentContact = {
list_ids: ['e7b598d9-23ca-48df-a62b-53470b5d1d86'],
email: '[email protected]',
name: 'new student firstname',
};

const newParentContact = {
list_ids: ['e7b598d9-23ca-48df-a62b-53470b5d1d86'],
email: '[email protected]',
name: 'new parent firstname',
};

const newInstructorContact = {
list_ids: ['e7b598d9-23ca-48df-a62b-53470b5d1d86'],
email: '[email protected]',
name: 'new instructor firstname',
};

describe('Send different email types', () => {
describe('Send an email to a new student', () => {
it('Should return 202 when it successfully posts a new student email to SendGrid', async () => {
const res = await sendEmail(newStudent);
expect(res.status).toBe(202);
expect(res.headers.date.length).not.toBe(0);
});
});
describe('Send an email to a new parent', () => {
it('Should return 202 when it successfully posts a new parent email to SendGrid', async () => {
const res = await sendEmail(newParent);
expect(res.status).toBe(202);
expect(res.headers.date.length).not.toBe(0);
});
});
describe('Send an email to a new instructor', () => {
it('Should return 202 when it successfully posts a new instructor email to SendGrid', async () => {
const res = await sendEmail(newInstructor);
expect(res.status).toBe(202);
expect(res.headers.date.length).not.toBe(0);
});
});
});

describe('Add users to a contact list on SendGrid', () => {
describe('Add a new student to a contact list', () => {
it('Should return 202 when it successfully adds a new student to a contact list', async () => {
const res = await addToList(newStudentContact);
expect(res.status).toBe(202);
expect(res.headers.date.length).not.toBe(0);
});
});
describe('Add a new parent to a contact list', () => {
it('Should return 202 when it successfully adds a new parent to a contact list', async () => {
const res = await addToList(newParentContact);
expect(res.status).toBe(202);
expect(res.headers.date.length).not.toBe(0);
});
});
describe('Add a new instructor to a contact list', () => {
it('Should return 202 when it successfully adds a new instructor to a contact list', async () => {
const res = await addToList(newInstructorContact);
expect(res.status).toBe(202);
expect(res.headers.date.length).not.toBe(0);
});
});
});
2 changes: 1 addition & 1 deletion api/courses/coursesModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ const db = require('../../data/db-config');

const getAllCourses = async () => {
return await db('courses as c')
.select('c.*', 'p.program_name', 'i.instructor_id')
.select('c.*', 'p.program_name', 'i.instructor_id', 'i.instructor_name')
.leftJoin('programs as p', 'p.program_id', 'c.program_id')
.leftJoin('instructors as i', 'c.instructor_id', 'i.instructor_id');
};
Expand Down
41 changes: 41 additions & 0 deletions api/email/emailHelper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
const sgMail = require('@sendgrid/mail');
sgMail.setApiKey(process.env.SENDGRID_API_KEY);

const sendEmail = (data) => {
sgMail
.send(data)
.then((response) => {
console.log(JSON.stringify(response));
})
.catch((error) => {
console.error(error);
});
};

const addToList = (data) => {
let request = require('request');
let options = {
method: 'PUT',
url: 'https://api.sendgrid.com/v3/marketing/contacts',
headers: {
'content-type': 'application/json',
authorization: 'Bearer ' + process.env.SENDGRID_API_KEY,
},
body: {
list_ids: data.list_ids,
contacts: [
{
email: data.email,
first_name: data.name,
},
],
},
json: true,
};
request(options, function (error, response, body) {
if (error) throw new Error(error);
console.log(body);
});
};

module.exports = { sendEmail, addToList };
56 changes: 56 additions & 0 deletions api/parent/parentRouter.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,64 @@
const express = require('express');
const authRequired = require('../middleware/authRequired');
const {
roleAuthenticationParent,
} = require('../middleware/roleAuthentication.js');
const {
checkChildObject,
checkChildExist,
} = require('../children/ChildrenMiddleware');
const Parents = require('./parentModel');
const Children = require('../children/childrenModel');
const router = express.Router();

router.post(
'/',
authRequired,
roleAuthenticationParent,
checkChildObject,
async function (req, res, next) {
const { profile_id } = req.profile;
try {
let newChild = await Children.addChild(profile_id, req.body);
res.status(201).json(newChild);
} catch (error) {
next(error);
}
}
);

router.put(
'/:child_id',
authRequired,
roleAuthenticationParent,
checkChildExist,
async function (req, res, next) {
const { child_id } = req.params;
try {
let [updatedChild] = await Children.updateChild(child_id, req.body);
res.status(200).json(updatedChild);
} catch (error) {
next(error);
}
}
);

router.delete(
'/:child_id',
authRequired,
roleAuthenticationParent,
checkChildExist,
async function (req, res, next) {
const { child_id } = req.params;
try {
let { name } = await Children.removeChild(child_id);
res.status(200).json({ name });
} catch (error) {
next(error);
}
}
);

router.get('/:profile_id/children', authRequired, function (req, res) {
const { profile_id } = req.params;

Expand Down
Loading