Project Design
The project has a very specific design which we expect future developers to understand and follow when making changes.
Root
There are several important files in the root directory. The .gitattributes
informs Git to ignore the local docs and coverage report in language statistics. The Procfile
is what tells Heroku how to start the application. jest.config.ts
contains the configuration information for Jest, where we specify where tests are (testMatch
), what to do before running the tests (globalSetup
), in what way to generate coverage reports, etc. package.json
and package-lock.json
are important for NPM, and, in most cases, should not be modified directly. requirements.txt
is for Python’s package manager, pip, and contains information about each of the packages required to run our scripts. The root tsconfig.json
is there as a placeholder. The remaining files should be self-explanatory.
One file in the root of the project that does not show up on GitHub is the .env
file. It contains the contents of the config variables found in the app settings. The application cannot run without this file. It is extremely important that the contents of this file be kept hidden, which is why the file is not stored in the remote repository. The file can be acquired from Jon Lund.
Top-Level Miscellaneous
The coverage
directory contains a copy of the generated coverage report. See more about our coverage reports here. The screenshots
directory, if it exists, will contain screenshots of various pages of the site taken from different browsers across many emulated devices. See more about our emulation testing here. The node_modules
directory is created and managed by NPM, and contains all project dependencies. Developers should never need to interact with this directory directly.
Scripts
The scripts
directory consists of several useful Python scripts. See more about them here.
Tests
The tests are briefly explained on the scripts page. Extending the UI tests simply by looking at the existing tests should be very easy for developers. Extending the tests for the service layer is a bit more complicated. Modifying existing tests should be fairly simple. To add an entirely new test, create a new file in /test/services
and call it <testName>.test.ts
. The typical format for service test files is as follows:
import { getDBM, closeDBM } from "./util";
// Test <testName> service
test("<testName>", async () => {
const dbm = await getDBM();
// Your test code here
await closeDBM(dbm);
});
For information on how to use Jest, read the Jest docs. The file /test/services/util.ts
contains useful functions which can be used when testing. Developers are welcome to add to this file if needed. Developers are expected to check the coverage reports to ensure they are covering all testable functions, lines, and statements.
Project Structure
The main part of the project, contained within the src
directory, is organized into four layers: the views, the routing layer, the service layer, and the database. Somewhat independent of these layers are the files at the top-level of /src
.
Top-Level Files
index.ts
is the entry point of the application. It configures the Express server, the Express Handlebars view engine, configures routing, initializes the database, and finally starts the webserver.
routes.ts
exports all routes from the /src/routes
directory, which are used in index.ts
to configure routing.
services.ts
exports a class containing every available database service, and the ability to execute queries directly.
dbinit.ts
contains the SQL code that initializes the database and all tables within it. In the event that developers need to make modifications to the database schema, this is the file where they would do so.
db.ts
provides a wrapper around the mysql NPM package, making it easier to execute queries asynchronously.
emailer.ts
contains functions that allow for sending emails. The /src/emails
directory houses a series of text and HTML files that contain the content that will be sent to users via email. For each email that needs to be sent, we send both the text and HTML versions of the email so that if the user’s email client supports HTML (which it should, given that this is the 21st century), that is what will be displayed. If HTML is not supported, it will simply display the text version of the email. The sendFormattedEmail
function supports the use of named placeholders within the email body. See the function documentation for further details.
asyncCatch.ts
exports a function that wraps an asynchronous function in a try/catch block. It is imperative that developers use this function when configuring routing, as Express does not have a way of catching errors that occur within asynchronous routes.
config.ts
contains default values for the Meta
database table.
proxy.ts
exports a function that will essentially reroute a request to a different URL.
helpers.ts
contains useful helper functions that can be used in the view layer.
View Layer
The view layer is kept within the /src/views
and /src/static
directories. The views directory houses all HTML files used in the view layer. There are two subdirectories as well, both of which are necessary for our view engine, Express Handlebars. /src/views/layouts
has the one and only layout. Our view engine is configured to use /src/views/layouts/default.html
as a template, and render the specified view (one of the HTML files within the views directory) in place of {{{body}}}
. /src/views/partials
contains partial templates, of which we have only the one we use for each page of the admin control panel. For information on the syntax Express Handlebars supports, see the NPM page or the GitHub repo. The /src/static
directory is served statically. As an example, /src/static/css/main.css
is served as /css/main.css
, and in a view, you could link it like so:
<link rel="stylesheet" type="text/css" href="/css/main.css" />
Our CSS and JavaScript are in their respective subdirectories within /src/static
. Both main.css
and main.js
are included in every page on the site. The remaining files are exclusive to the pages they are associated with.
Routing Layer
The routing layer is kept within the /src/routes
directory. Each TypeScript file in this directory represents one set of subpages in the application. The format for a route is as follows:
/**
* <routeName> routes.
* @packageDocumentation
*/
import { Router } from "express";
import { renderPage } from "./util";
import wrapRoute from "../asyncCatch";
// Other imports
/**
* The <routeName> router.
*/
export const routeNameRouter = Router();
// A page
routeNameRouter.get(
"/",
wrapRoute(async (req, res) => {
await renderPage(req, res, "<viewName>");
})
);
// Another page
routeNameRouter.get(
"/other",
wrapRoute(async (req, res) => {
await renderPage(req, res, "<otherViewName>");
})
);
// Other pages
The wrapRoute
function must be used in order to ensure any errors that occur within the following block of code will be caught and handle appropriately. Additionally, the new router object, called routeNameRouter
above, will need to be exported from /src/routes.ts
:
export * from "./routes/<routeName>";
The new route can be used in /src/index.ts
:
app.use("/<path>", routes.routeNameRouter);
Developers are strongly encouraged to look at other files in the routing directory in order to best follow the development patterns we have established in the application.
Service Layer
The service layer deals with interaction with the database and is kept within the /src/services
directory. Each TypeScript file in this directory loosely corresponds to one table in the database. Each file contains a class that inherits from the BaseService
class in /src/services/util.ts
. The format for a service is as follows:
/**
* Services for <serviceName>.
* @packageDocumentation
*/
import { BaseService } from "./util";
/**
* <serviceName> architecture.
*/
export interface ServiceName {
id: number;
name: string;
}
/**
* <serviceName> services.
*/
export class ServiceNameService extends BaseService {
/**
* Documentation goes here.
*/
public async someService(): Promise<ServiceName[]> {
const sql = `SOME SQL CODE`;
const params = [];
const rows: ServiceName[] = await this.dbm.execute(sql, params);
return rows;
}
}
We encourage developers to use TypeDoc documentation syntax to document new classes, functions/methods, interfaces, etc. See the repos page for more information on TypeDoc.
Any new services must be added in /src/services.ts
. This includes importing the new service class, defining it as a read-only property of the DatabaseManager
class, and initializing it in the constructor. When adding new services, developers should always write tests for the new services to ensure everything executes as intended.
Developers are strongly encouraged to look at other files in the services directory in order to best follow the development patterns we have established in the application.
Database Layer
The MySQL database, as stated earlier, is initialized in /src/dbinit.ts
and accessed via the service layer. In the event that developers need to interact directly with the database, they can do so using the database script. See the scripts page for more details.