Skip to content

Latest commit

 

History

History
190 lines (104 loc) · 10.5 KB

README.md

File metadata and controls

190 lines (104 loc) · 10.5 KB

Bloom Backend

TypeScript NestJS Prisma Postgres

Overview

This is the backend services container. Data is stored in a Postgres database and served over HTTPS to the frontend (either at build time for things that can be server-rendered, otherwise at run time). Most services are part of a NestJS application. Prisma is the application's ORM. OpenAPI Swagger documentation is automatically generated by the server at http://localhost:3100/api/ in local development environments, or in any other environment by adding /api to the api URL. This can be helpful to get a more visual overview of all available endpoints.

Installation

The following commands are for macOS / Linux, but you can find equivalent instructions for Windows machines online.

Environment Variables

Configuration of the backend is pulled from environment variables defined in an .env file in the api directory. Copy the .env.template file in api into a .env file. Some keys are secret and are internally available. The template file includes default values and descriptions of each variable.

Dependencies

If you don't have yarn installed, you can install homebrew with these instructions and then do so with brew install yarn.

Installing Node

We are currently using Node version 18. You can install Node using homebrew with the following command: brew install node@18.

If you have multiple versions of Node installed, you can use nvm (node version manager), or other similar tools, to switch between them. Ensure you're on the right version by checking with node -v.

If along the way you get env: node: No such file or directory, inspect the output from installing node for instructions on if you might need to add node to certain terminal paths.

Installing Postgresql

You can install Postgres using homebrew with the following command: brew install postgresql@15. You then start it with brew services start postgresql@15.

Project dependencies

Install project dependencies with yarn install from within the api directory.

Starting locally

The following command will generate and build the Prisma schema and setup the database with seeded data: yarn setup:dev.

If you would prefer to have it setup with more realistic data you can instead run: yarn setup.

If this is your first time running this command and you see psql: error: FATAL: database "<username>" does not exist you may need to run createdb <username> first.

You will also need to update the DATABASE_URL environment variable to include your username.

If you're using VSCode, you can install the Postgres explorer extension to inspect your local database. When you click on the + to create a new connection, you can use the following inputs to each question to create a connection to the newly created database: localhost, <username>, hit enter for password, 5432, standard, bloom_prisma, and a descriptive name like local-bloom. Once the connection is established, you can inspect the database.

To start the application run: yarn dev.

Modifying the Schema

If you're using VSCode, you can install the Prisma extension to add syntax highlighting and formatting to Prisma schema files.

To modify the Prisma schema you will need to work with the schema.prisma file. This file controls the following:

  1. The Structure of each model (entity if you are more familiar with TypeORM)
  2. The Relationships between models
  3. Enum creation for use in both the API and the database
  4. How Prisma connects to the database

You will need to:

  1. Add the field in the DTO
  2. Run yarn generate:client to add the type to the swagger file
  3. Manually add the field to schema.prisma
  4. Run yarn prisma migrate dev --name <name of migration> to create the migration file

Conventions

We use the following conventions:

  • model and enum names are capitalized camel case (e.g. HelloWorld)
  • model and enum names are @@map()ed to lowercase snake case (e.g. hello_world)
  • a model's fields are lowercase camel case (e.g. helloWorld)
  • a model's fields are @map()ed to lowercase snake case (e.g. hello_world)
This is to make the api easier to work with, and to respect postgres's name space conventions.

Controllers

Backend endpoints live in controllers under src/controllers. They follow the NestJs standards

Conventions

Controllers are given the extension .controller.ts and the model name (listing, application, etc) is singular. So for example listing.controller.ts.

The exported class should be in capitalized camel case (e.g. ListingController).

DTOs

DTOs (Data Transfer Objects) are how we flag what fields endpoints will take in, and what the form of the response from the backend will be.

We use the packages class-transformer & class-validator for this.

DTOs are stored under src/dtos, and are broken up by what model they are related to. There are also shared DTOs which are stored under the shared sub-directory.

Conventions

DTOs are given the extension .dto.ts and the file name is lowercase kebab case (e.g. listings-filter-params.dto.ts).

The exported class should be in capitalized camel case (e.g. ListingFilterParams) and does not include the DTO as a suffix.

Enums

These are enums used by NestJs primarily for either taking in a request or sending out a response. Database enums (enums from Prisma) are part of the Prisma schema and are not housed here.

They are housed under src/enums and the file name is lowercase kebab case and end with -enum.ts.

So for example filter-key-enum.ts.

Conventions

The exported enum should be in capitalized camel case (e.g. ListingFilterKeys).

Modules

Modules connect the controllers to services and follow NestJS standards.

Conventions

Modules are housed under src/modules and are given the extension .module.ts. The model name (listing, application, etc) is singular. So for example listing.module.ts.

The exported class should be in capitalized camel case (e.g. ListingModule).

Services

Services are where business logic is performed as well as interfacing with the database.

Controllers should be calling functions in services in order to do their work.

The follow the NestJS standards.

Conventions

Services are housed under src/services and are given the extension .services.ts. The model name (listing, application, etc) is singular. So for example listing.service.ts.

The exported class should be in capitalized camel case (e.g. ListingService).

Guards & Passport Strategies

We currently use guards for 2 purposes. Passport guards and Permissioning guards.

Passport guards (jwt.guard.ts, mfa.guard.ts, and optional.guard.ts) verify that the request is from a legitimate user. JwtAuthGuard does this by verifying the incoming jwt token (off the request's cookies) matches a user. MfaAuthGuard does this by verifying the incoming login information (email, password, mfaCode) matches a user's information. OptionalAuthGuard is used to allow requests from users not logged in through. It will still verify the user through the JwtAuthGuard if a user was logged in.

Passport guards are paired with a passport strategy (jwt.strategy.ts, and mfa.strategy.ts), this is where the code to actually verify the requester lives.

Hopefully that makes sense, if not think of guards as customs agents, and the passport strategy is what the guards look for in a request to allow entry to a requester. Allowing them access the endpoint that the guard protects.

NestJS passport docs NestJS guards docs

Permissioning guards (permission.guard.ts, and user-profile-permission-guard.ts) verify that the requester has access to the resource and action they are trying to perform. For example a user that is not logged in (anonymous user) can submit applications, but cannot create listings. We leverage Casbin to do user verification.

Testing

There are 2 different kinds of tests that the backend supports: Integration tests and Unit tests.

Integration Tests are tests that DO interface with the database, reading/writing/updating/deleting data from that database.

Unit Tests are tests that MOCK interaction with the database, or test functionality directly that does not interact with the database.

Integration Testing

Integration Tests are housed under test/integration, and files are given the extension .e2e-spec.ts.

These tests will generally test going through the controller's endpoints and will mock as little as possible. When testing the database should start off as empty and should be reset to empty once tests are completed (i.e. data is cleaned up).

How to run integration tests

Running the following will run all integration tests yarn test:e2e.

Unit Testing

Unit Tests are housed under test/unit, and files are given the extension .spec.ts.

These tests will generally test the functions of a service, or helper functions. These tests will mock Prisma and therefore will not interface directly with the database. This allows for verifying the correct business logic is performed without having to set up the database.

How to run unit tests

Running the following will run all unit tests: yarn test

Testing with code coverage

We have set up both code coverage and code coverage benchmarks. These benchmarks must be met for your PR to pass CI checks. Test coverage is calculated against both the integration and unit test runs. You can run test coverage with the following: yarn test:cov