Skip to content

Repository files navigation

PlaceHires Backend

Development Setup

Prerequisites

  • Environmental Variables: you'll need correct variables defined in .env file.
  • Redis: install Redis and have it running on its default port.

Starting development

$ npm install
$ npm start

Commonly used NPM scripts

# watch and run the main server (app.ts)
$ npm run dev

# watch and run the worker that processes background jobs (worker.ts)
$ npm run worker

# run prettier to reformat code base
$ npm run format

# run lint checks (fix warnings and errors that appear please)
$ npm run lint

# run the full test suite
$ npm test

Project Structure

  • test: contains test utilities (not actual test suites).
  • src: contains the source code written in Typescript.
    • config: contains configuration variables of the app (use this instead of process.env[...]).

    • constants: contains shared constants.

    • generated: contains generated files (do not edit them manually).

    • middleware: contains express middleware for route requests.

    • models: contains models that are part of application business logic that interact with some type of data within application.

      Each model is a single source of truth for data fetching, business logic, security for that particular type of data.

      For example, we have an Application Model and a method for fetching a single application by id. Within this application, we currently only ever want the application to be viewed by the applicant of this application, or the recruiter whom this application was sent to:

      class Application extends BaseModel<ApplicationProps> {
        static async getById({ user }: AppContext, id: Types.ObjectId | string) {
          const application = await MongooseApplication.findById(id);
          if (!application) return null;
          if (user.role === Role.APPLICANT) checkCorrectUser(user, application.applicantId);
          else if (user.role === Role.RECRUITER) await checkRecruiterHasAccessToApplication(user, application);
          else throw new ForbiddenError("You don't have permission to access this application");
          return new Application(application);
        }
      }

      By convention, we will pass AppContext as the first parameter that contains Request Context such as the current User for methods that need to handle permissions.

      Let's say our business requirement changes, and we want to allow Admin users to be able to view any Application. Also, we're introducing Redis as a caching layer for data fetching. Then all we have to do is update the model as follows:

      class Application extends BaseModel<ApplicationProps> {
        static async getById({ user }: AppContext, id: Types.ObjectId | string) {
          let application: ApplicantProps;
          application = await Redis.get(...something);
          if (!application) {
            application = await MongooseApplication.findById(id);
            await Redis.set(...cacheTheFoundApplicationInRedis);
          };
          if (!application) return null;
          if (user.role === Role.APPLICANT) checkCorrectUser(user, application.applicantId);
          else if (user.role === Role.RECRUITER) await checkRecruiterHasAccessToApplication(user, application);
          else if (user.role !== Role.ADMIN) throw new ForbiddenError("You don't have permission to access this application");
          return new Application(application);
        }
      }

      Because this static getById method is a single source of truth for fetching a single application, we don't need to update it anywhere else. If we didn't do this and simply did await MongooseApplication.findById(id); everywhere we needed to fetch an application, we would have to track down all these use cases, and add the redis logic, and update the permission logic for each of the use cases.

    • public: contains public assets.

    • queues: contains applications queues and workers for background jobs. We use Bull for this: https://docs.bullmq.io/. Jobs can be viewed on bull (dash)board: http://localhost:3999/admin with credentials from set with env variables ADMIN_USERNAME and ADMIN_SECRET.

    • routes: contains express routes. Most requests should not go in here because we use GraphQL.

    • schema: contains all GraphQL related files - types, resolvers, etc. We use GraphQL Nexus to construct the schema with Typescripts' type safety. See Nexus docs here: https://nexusjs.org/.

    • services: contains services that are also part of business logic that interact with third-party service within application.

      For example, payment-service contains payment related business logic that uses Stripe API.

    • templates: contains HTML templates as strings in ts files.

    • types: contains shared TypeScript type definitions.

    • utils: contains application utilities.

    • views: contains EJS template files: https://ejs.co/.

Tests

https://jestjs.io/

Testing GraphQL API (manually)

Some of the tools you could use to test GraphQL API manually: GraphQL Playground, Postman, Insomnia, etc.

To test queries/mutations/subscriptions that need an authenticated user, you will need to retrieve an ID Token through:

GET http://localhost:3999/dev/token?email=${USER_EMAIL}

Using retrieved token, set Authorization header to Bearer {idToken}.

Note: these ID Tokens expire after an hour, so re-retrieve them once they have expired.

About

Backend

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages