From d5d59de927b7fbe5e63893e177e4156fcf341177 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=A1pai=20Bal=C3=A1zs?= Date: Tue, 20 Dec 2022 15:07:21 +0100 Subject: [PATCH] [node-pod] express-typeorm-postgres kit (#639) * feat: 530 set up express REST API (#539) * feat: 530 set up express REST API * fix: version lock dependencies * fix: added jest unit test proof * fix: styles and readme updates * fix: renamed project to reflect future technology * fix: set explicit any use to error * Feat/532 add data storage to the project (#551) * feat: set up postgres docker container with seeding * feat: set up typeorm to connect to the database * feat: set up dotenv * feat: document database setup and seeding * fix: version lock new dependencies * fix: added additional commands to readme * fix: Update starters/express-typescript-typeorm-postgres/README.md Co-authored-by: Ihar Dziamidau <31627738+ihardz@users.noreply.github.com> Co-authored-by: Ihar Dziamidau <31627738+ihardz@users.noreply.github.com> * [NODE] feat: set up CRUD as example (#552) * feat: set up CRUD as example * fix: remove supertest from repo * [NODE] feat: added swagger open api generator (#559) * feat: added swagger open api generator * fix: readme update * fix: remove swagger.json generation * [NODE] feat: added healthcheck endpoint (#556) * feat: added healthcheck endpoint * fix: added status codes package * fix: added configurable ping timeout * fix: check real database connection * fix: applied code review change requests * fix: removed unused variables * fix: renamed and moved file * [node-pod] 1. Configure API middleware (#571) * chore: add cors middleware * chore: add NextFuncion * remove: unused folder * chore: add allowed origins * chore: add dynamic origins cors * fix: allowedorigins host url * fix: allowedOrigins if empty array * chore: document the cors implementation * fix: remove eslint prettier * [NODE] feat: added error handlers (#569) * feat: added error handlers * fix: return with 404 on not found technologies * feat: added unit tests (#584) * [NODE] feat: added customised logger to the application (#573) * feat: added customised logger to the application * fix: mimic console methods in log helper * feat: added unit tests * fix: remove leftover comments * fix: abstract tests * fix: simplify logger * fix: simplify cors env variable * [NODE] Fix/581 move health endpoint (#589) * fix: move health endpoint to match specification * fix: removed /api route, set api docs route to docs * [NODE] Feat/591 seed with typeorm (#593) * feat: seed database with typeorm * docs: added database seeding documentation * fix: removed initdb mount * [NODE] Feat/582 generate production package json with build (#590) * feat: added package.json generator * docs: added entry about the build process * fix: added path.join for windows compatibility * [NODE] Feat/595 add cache to starter kit (#613) * feat: set up docker-compose and database connection retry * feat: added redis cache to docker-compose * feat: set up caching for technology endpoints * feat: updated healthcheck endpoint with redis * feat: set up caching as a secondary system and updated healthcheck endpoint * docs: improved documentation, separated redis related things from cache * fix: interval times and amount, pull images in postinstall, build fix * fix: health check result if cache is down * fix: extracted db operations into a separate service file * fix: health check result updates * [NODE] feat: initial setup of bullMQ (#625) * feat: initial setup of bullMQ * feat: added health check for queue * [NODE] Fix/621 unique docker containers (#627) * fix: infrastructure set up and app startup requirements * docs: updated documentation * [NODE] Refactor/572 rename starter kit (#638) * fix: renamed folder * fix: renamed kit name everywhere * fix: removed postinstall script * fix: added nvmrc * fix: use tabs instead of spaces in the kit * feat: added express starter to starter-kits.json * [NODE] feat: added schema generator (#648) * feat: added schema generator * docs: finish readme * fix: tests and swagger.json changes * fix: use built-in redis adapter from cachified v3.0.1 * docs: update readme with review requests * fix: ran linter * fix: readme mistakes * feat: added kit to website * fix: import caseing Co-authored-by: Ihar Dziamidau <31627738+ihardz@users.noreply.github.com> Co-authored-by: Ian Sam Mungai --- packages/website/src/config.tsx | 21 ++ packages/website/src/icons/ExpressIcon.tsx | 22 ++ packages/website/src/icons/PostgresIcon.tsx | 23 ++ packages/website/src/icons/TypeOrmIcon.tsx | 23 ++ packages/website/src/icons/index.ts | 3 + starter-kits.json | 3 +- .../express-typeorm-postgres/.dockerignore | 7 + .../express-typeorm-postgres/.editorconfig | 15 + .../express-typeorm-postgres/.env.example | 37 ++ .../express-typeorm-postgres/.eslintrc.js | 25 ++ starters/express-typeorm-postgres/.gitignore | 45 +++ starters/express-typeorm-postgres/.nvmrc | 1 + starters/express-typeorm-postgres/.prettierrc | 8 + starters/express-typeorm-postgres/README.md | 205 +++++++++++ .../docker-compose.yml | 32 ++ .../express-typeorm-postgres/package.json | 96 +++++ .../express-typeorm-postgres/postgres.db.env | 3 + .../src/bootstrap-app.ts | 36 ++ .../src/cache/cache.ts | 28 ++ .../src/cache/redis-cache-client.ts | 48 +++ .../src/constants/result.ts | 5 + .../src/db/datasource.ts | 43 +++ .../src/db/entities/technology.entity.ts | 15 + .../src/db/run-seeders.ts | 29 ++ .../src/db/seeding/technology-seeder.ts | 27 ++ .../src/interfaces/results.ts | 17 + .../src/interfaces/schema.ts | 197 +++++++++++ starters/express-typeorm-postgres/src/main.ts | 31 ++ .../src/middlewares/cors/index.ts | 22 ++ .../src/modules/health/handlers/get-health.ts | 54 +++ .../src/modules/health/health.controller.ts | 8 + .../modules/health/services/health.service.ts | 95 +++++ .../src/modules/queue/queue.controller.ts | 12 + .../src/modules/router.ts | 12 + .../handlers/create-technology.spec.ts | 56 +++ .../technology/handlers/create-technology.ts | 26 ++ .../handlers/delete-technology.spec.ts | 56 +++ .../technology/handlers/delete-technology.ts | 27 ++ .../handlers/get-all-technologies.spec.ts | 51 +++ .../handlers/get-all-technologies.ts | 24 ++ .../handlers/get-technology.spec.ts | 69 ++++ .../technology/handlers/get-technology.ts | 31 ++ .../handlers/update-technology.spec.ts | 59 ++++ .../technology/handlers/update-technology.ts | 28 ++ .../technology/services/technology.service.ts | 111 ++++++ .../technology/technology.controller.ts | 18 + .../src/queue/config.constants.ts | 15 + .../src/queue/job-processor.ts | 9 + .../src/queue/queue.ts | 41 +++ .../src/utils/log-helper.spec.ts | 50 +++ .../src/utils/log-helper.ts | 31 ++ .../express-typeorm-postgres/swagger.json | 329 ++++++++++++++++++ .../express-typeorm-postgres/swagger_v3.json | 325 +++++++++++++++++ .../generators/generate-prod-package-json.js | 25 ++ .../tsconfig.build.json | 4 + .../express-typeorm-postgres/tsconfig.json | 22 ++ 56 files changed, 2654 insertions(+), 1 deletion(-) create mode 100644 packages/website/src/icons/ExpressIcon.tsx create mode 100644 packages/website/src/icons/PostgresIcon.tsx create mode 100644 packages/website/src/icons/TypeOrmIcon.tsx create mode 100644 starters/express-typeorm-postgres/.dockerignore create mode 100644 starters/express-typeorm-postgres/.editorconfig create mode 100644 starters/express-typeorm-postgres/.env.example create mode 100644 starters/express-typeorm-postgres/.eslintrc.js create mode 100644 starters/express-typeorm-postgres/.gitignore create mode 100644 starters/express-typeorm-postgres/.nvmrc create mode 100644 starters/express-typeorm-postgres/.prettierrc create mode 100644 starters/express-typeorm-postgres/README.md create mode 100644 starters/express-typeorm-postgres/docker-compose.yml create mode 100644 starters/express-typeorm-postgres/package.json create mode 100644 starters/express-typeorm-postgres/postgres.db.env create mode 100644 starters/express-typeorm-postgres/src/bootstrap-app.ts create mode 100644 starters/express-typeorm-postgres/src/cache/cache.ts create mode 100644 starters/express-typeorm-postgres/src/cache/redis-cache-client.ts create mode 100644 starters/express-typeorm-postgres/src/constants/result.ts create mode 100644 starters/express-typeorm-postgres/src/db/datasource.ts create mode 100644 starters/express-typeorm-postgres/src/db/entities/technology.entity.ts create mode 100644 starters/express-typeorm-postgres/src/db/run-seeders.ts create mode 100644 starters/express-typeorm-postgres/src/db/seeding/technology-seeder.ts create mode 100644 starters/express-typeorm-postgres/src/interfaces/results.ts create mode 100644 starters/express-typeorm-postgres/src/interfaces/schema.ts create mode 100644 starters/express-typeorm-postgres/src/main.ts create mode 100644 starters/express-typeorm-postgres/src/middlewares/cors/index.ts create mode 100644 starters/express-typeorm-postgres/src/modules/health/handlers/get-health.ts create mode 100644 starters/express-typeorm-postgres/src/modules/health/health.controller.ts create mode 100644 starters/express-typeorm-postgres/src/modules/health/services/health.service.ts create mode 100644 starters/express-typeorm-postgres/src/modules/queue/queue.controller.ts create mode 100644 starters/express-typeorm-postgres/src/modules/router.ts create mode 100644 starters/express-typeorm-postgres/src/modules/technology/handlers/create-technology.spec.ts create mode 100644 starters/express-typeorm-postgres/src/modules/technology/handlers/create-technology.ts create mode 100644 starters/express-typeorm-postgres/src/modules/technology/handlers/delete-technology.spec.ts create mode 100644 starters/express-typeorm-postgres/src/modules/technology/handlers/delete-technology.ts create mode 100644 starters/express-typeorm-postgres/src/modules/technology/handlers/get-all-technologies.spec.ts create mode 100644 starters/express-typeorm-postgres/src/modules/technology/handlers/get-all-technologies.ts create mode 100644 starters/express-typeorm-postgres/src/modules/technology/handlers/get-technology.spec.ts create mode 100644 starters/express-typeorm-postgres/src/modules/technology/handlers/get-technology.ts create mode 100644 starters/express-typeorm-postgres/src/modules/technology/handlers/update-technology.spec.ts create mode 100644 starters/express-typeorm-postgres/src/modules/technology/handlers/update-technology.ts create mode 100644 starters/express-typeorm-postgres/src/modules/technology/services/technology.service.ts create mode 100644 starters/express-typeorm-postgres/src/modules/technology/technology.controller.ts create mode 100644 starters/express-typeorm-postgres/src/queue/config.constants.ts create mode 100644 starters/express-typeorm-postgres/src/queue/job-processor.ts create mode 100644 starters/express-typeorm-postgres/src/queue/queue.ts create mode 100644 starters/express-typeorm-postgres/src/utils/log-helper.spec.ts create mode 100644 starters/express-typeorm-postgres/src/utils/log-helper.ts create mode 100644 starters/express-typeorm-postgres/swagger.json create mode 100644 starters/express-typeorm-postgres/swagger_v3.json create mode 100644 starters/express-typeorm-postgres/tools/generators/generate-prod-package-json.js create mode 100644 starters/express-typeorm-postgres/tsconfig.build.json create mode 100644 starters/express-typeorm-postgres/tsconfig.json diff --git a/packages/website/src/config.tsx b/packages/website/src/config.tsx index 392bdc1fe..90bde143d 100644 --- a/packages/website/src/config.tsx +++ b/packages/website/src/config.tsx @@ -37,6 +37,9 @@ import { QwikIcon, SolidJsIcon, DenoIcon, + ExpressIcon, + PostgresIcon, + TypeOrmIcon, } from './icons'; export interface NavItem { @@ -304,6 +307,24 @@ export const TECHNOLOGIES = [ tags: ['Data Management'], Icon: (props) => , }, + { + key: 'express', + name: 'Express.js', + tags: ['Framework'], + Icon: (props) => + }, + { + key: 'typeorm', + name: 'TypeORM', + tags: ['Data Management'], + Icon: (props) => + }, + { + key: 'postgres', + name: 'Postgres', + tags: ['Data Management'], + Icon: (props) => + } ]; export const SPONSORS_ICON = [ diff --git a/packages/website/src/icons/ExpressIcon.tsx b/packages/website/src/icons/ExpressIcon.tsx new file mode 100644 index 000000000..072943fc7 --- /dev/null +++ b/packages/website/src/icons/ExpressIcon.tsx @@ -0,0 +1,22 @@ +import { Props } from "./types"; + +export function ExpressIcon({ className, size = 48 }: Props) { + return ( + + + + + + + + + ); +} diff --git a/packages/website/src/icons/PostgresIcon.tsx b/packages/website/src/icons/PostgresIcon.tsx new file mode 100644 index 000000000..b8c03ee1a --- /dev/null +++ b/packages/website/src/icons/PostgresIcon.tsx @@ -0,0 +1,23 @@ +import { Props } from "./types"; + +export function PostgresIcon({ className, size = 48 }: Props) { + return ( + + + + + + + + + + ); +} diff --git a/packages/website/src/icons/TypeOrmIcon.tsx b/packages/website/src/icons/TypeOrmIcon.tsx new file mode 100644 index 000000000..33d58a0cf --- /dev/null +++ b/packages/website/src/icons/TypeOrmIcon.tsx @@ -0,0 +1,23 @@ +import { Props } from "./types"; + +export function TypeOrmIcon({ className, size = 48 }: Props) { + return ( + + + + + + + + + + ); +} diff --git a/packages/website/src/icons/index.ts b/packages/website/src/icons/index.ts index 112bee3f0..2acfab513 100644 --- a/packages/website/src/icons/index.ts +++ b/packages/website/src/icons/index.ts @@ -39,3 +39,6 @@ export { LinkedinIcon } from './LinkedinIcon'; export { QwikIcon } from './QwikIcon'; export { SolidJsIcon } from './SolidJsIcon'; export { DenoIcon } from './DenoIcon.tsx'; +export { ExpressIcon } from './ExpressIcon'; +export { PostgresIcon } from './PostgresIcon'; +export { TypeOrmIcon } from './TypeOrmIcon'; diff --git a/starter-kits.json b/starter-kits.json index 014b0d80a..c7738e393 100644 --- a/starter-kits.json +++ b/starter-kits.json @@ -9,5 +9,6 @@ "qwik-graphql-tailwind": "Qwik, GraphQL, and TailwindCSS", "solidjs-tailwind": "SolidJs and TailwindCSS", "angular-ngrx-scss": "Angular, NgRx, and SCSS", - "deno-oak-denodb": "Deno, Oak, and DenoDB" + "deno-oak-denodb": "Deno, Oak, and DenoDB", + "express-typeorm-postgres": "Express.js, TypeOrm, and PostgreSQL" } diff --git a/starters/express-typeorm-postgres/.dockerignore b/starters/express-typeorm-postgres/.dockerignore new file mode 100644 index 000000000..8b9ee6702 --- /dev/null +++ b/starters/express-typeorm-postgres/.dockerignore @@ -0,0 +1,7 @@ +.github +.vscode +.idea +.git +node_modules +pg_data +tools diff --git a/starters/express-typeorm-postgres/.editorconfig b/starters/express-typeorm-postgres/.editorconfig new file mode 100644 index 000000000..a3dc6724c --- /dev/null +++ b/starters/express-typeorm-postgres/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = tab +insert_final_newline = true +trim_trailing_whitespace = true + +[test/**/expected.css] +insert_final_newline = false + +[{swagger.json, swagger_v3.json, package.json,.travis.yml,.eslintrc.json}] +indent_style = space diff --git a/starters/express-typeorm-postgres/.env.example b/starters/express-typeorm-postgres/.env.example new file mode 100644 index 000000000..cad45c55e --- /dev/null +++ b/starters/express-typeorm-postgres/.env.example @@ -0,0 +1,37 @@ +# Copy this file into a .env file to make it work in dev mode + +NODE_ENV=development + +# Provide a comma separated list of log levels to control what levels log should appear. +ALLOWED_LOG_LEVELS=DEBUG,INFO,WARN,ERROR + +# Postgres database connection environment variables +DATABASE_HOST=localhost +DATABASE_PORT=5432 +DATABASE_USER=postgres +DATABASE_PASSWORD=ettpg +DATABASE_NAME=pgdatabase + +# With the following variable you can control how TypeOrm behaves regarding logging +DATABASE_ENABLE_LOGGING=true +# This variable enables syncing with TypeOrm +DATABASE_ENABLE_SYNC=true + +# Use the following environment variables to control how many times and how often +# the app should try to connect to the database at start-up +DATABASE_CONNECT_RETRY_COUNT=5 +DATABASE_CONNECT_RETRY_INTERVAL_MS=5000 + +# Connection information of the redis instance used for caching +REDIS_CACHE_HOST=localhost +REDIS_CACHE_PORT=6379 + +# With this you can control how long cached values should be kept in the cache +REDIS_CACHE_TTL_IN_MS=300000 + +# Connection information of the redis instance used for bullMQ +REDIS_QUEUE_HOST=localhost +REDIS_QUEUE_PORT=6479 + +## Provide a comma separated list of origins and uncomment the variable if you would like to enable CORS +# CORS_ALLOWED_ORIGINS= diff --git a/starters/express-typeorm-postgres/.eslintrc.js b/starters/express-typeorm-postgres/.eslintrc.js new file mode 100644 index 000000000..b08f2f817 --- /dev/null +++ b/starters/express-typeorm-postgres/.eslintrc.js @@ -0,0 +1,25 @@ +module.exports = { + parser: '@typescript-eslint/parser', + parserOptions: { + project: 'tsconfig.json', + tsconfigRootDir : __dirname, + sourceType: 'module', + }, + plugins: ['@typescript-eslint/eslint-plugin'], + extends: [ + 'plugin:@typescript-eslint/recommended', + 'plugin:prettier/recommended', + ], + root: true, + env: { + node: true, + jest: true, + }, + ignorePatterns: ['.eslintrc.js', 'schema.ts'], + rules: { + '@typescript-eslint/interface-name-prefix': 'off', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-explicit-any': 'error', + }, +}; diff --git a/starters/express-typeorm-postgres/.gitignore b/starters/express-typeorm-postgres/.gitignore new file mode 100644 index 000000000..ee99f8eea --- /dev/null +++ b/starters/express-typeorm-postgres/.gitignore @@ -0,0 +1,45 @@ +# compiled output +/dist +/node_modules + +# database and caches +misc/pg_data +misc/cache_data +misc/cache_conf +misc/queue_data +misc/queue_conf + +# Logs +logs +*.log +npm-debug.log* +pnpm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# OS +.DS_Store + +# Tests +/coverage +/.nyc_output + +# IDEs and editors +/.idea +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# IDE - VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +# secrets +.env diff --git a/starters/express-typeorm-postgres/.nvmrc b/starters/express-typeorm-postgres/.nvmrc new file mode 100644 index 000000000..431076a94 --- /dev/null +++ b/starters/express-typeorm-postgres/.nvmrc @@ -0,0 +1 @@ +16.16.0 diff --git a/starters/express-typeorm-postgres/.prettierrc b/starters/express-typeorm-postgres/.prettierrc new file mode 100644 index 000000000..f2295646b --- /dev/null +++ b/starters/express-typeorm-postgres/.prettierrc @@ -0,0 +1,8 @@ +{ + "singleQuote": true, + "trailingComma": "es5", + "endOfLine": "lf", + "printWidth": 100, + "tabWidth": 2, + "useTabs": true +} diff --git a/starters/express-typeorm-postgres/README.md b/starters/express-typeorm-postgres/README.md new file mode 100644 index 000000000..8c8baa92b --- /dev/null +++ b/starters/express-typeorm-postgres/README.md @@ -0,0 +1,205 @@ +# express-typeorm-postgres starter kit + +This starter kit features Express, Typescript API setup + +## Table of Contents + +- [express-typeorm-postgres starter kit](#express-typeorm-postgres-starter-kit) + - [Table of Contents](#table-of-contents) + - [Overview](#overview) + - [Tech Stack](#tech-stack) + - [Included Tooling](#included-tooling) + - [Installation](#installation) + - [CLI (Recommended)](#cli-recommended) + - [Manual](#manual) + - [Commands](#commands) + - [Example Controllers](#example-controllers) + - [Database and Redis](#database-and-redis) + - [Seeding](#seeding) + - [Reset infrastructure](#reset-infrastructure) + - [Production build](#production-build) + - [CORS Cross-Origin Resource Sharing](#cors-cross-origin-resource-sharing) + - [Kit Organization / Architecture](#kit-organization--architecture) + - [Folder structure](#folder-structure) + - [Express](#express) + - [TypeOrm](#typeorm) + - [Caching](#caching) + - [Queue](#queue) + - [Testing](#testing) + - [API documentation and Schema generation](#api-documentation-and-schema-generation) + +## Overview + +### Tech Stack + +- [Express v4](https://expressjs.com) +- [TypeOrm](https://typeorm.io) +- [PostgreSQL](https://www.postgresql.org) +- [Redis](https://redis.io/) +- [BullMQ](https://docs.bullmq.io/) + +### Included Tooling + +- [Jest](https://jestjs.io/) - Test runner +- [TypeScript](https://www.typescriptlang.org/) - Type checking +- [ESLint](https://eslint.org/) - Code linting +- [Prettier](https://prettier.io/) - Code formatting + +## Installation + +### CLI (Recommended) + +```bash +npm create @this-dot/starter --kit express-typeorm-postgres +``` + +or + +```bash +yarn create @this-dot/starter --kit express-typeorm-postgres +``` + +- Follow the prompts to select the `express-typeorm-postgres` starter kit and name your new project. +- `cd` into your project directory and run `npm install`. +- Make sure you have docker & docker-compose installed on your machine +- Create a `.env` file and copy the contents of `.env.example` into it. +- Run `npm run infrastructure:start` to start the database and the redis instances +- Run `npm run dev` to start the development server. +- Open your browser to `http://localhost:3333/docs` to see the API documentation with the existing endpoints. + +### Manual + +```bash +git clone https://github.com/thisdot/starter.dev.git +``` + +- Copy and rename the `starters/express-typeorm-postgres` directory to the name of your new project. +- Make sure you have docker & docker-compose installed on your machine +- `cd` into your project directory and run `npm install`. +- Make sure you have docker & docker-compose installed on your machine +- Create a `.env` file and copy the contents of `.env.example` into it. +- Run `npm run infrastructure:start` to start the database and the redis instances +- Run `npm run dev` to start the development server. +- Open your browser to `http://localhost:3333/docs` to see the API documentation with the existing endpoints. + +## Commands + +- `npm run infrastructure:start` - Starts up a postgres database and two redis instances for caching +- `npm run infrastructure:stop` - Stops the running database and redis docker containers. +- `npm run db:seed` - Allows you to seed the database (See the Seeding section) +- `npm run dev` - Starts the development server (Needs a running infrastructure first) +- `npm run build` - Builds the app. +- `npm start` - Starts the built app. (Needs a running infrastructure first) +- `npm test` - Runs the unit tests. +- `npm run lint` - Runs ESLint on the project. +- `npm run format` - Formats code for the entire project +- `npm run generate:schema` - Generates the API schema types into the `src/interfaces/schema.ts` file + +## Example Controllers + +The starter contains an example CRUD implementation for technologies. You can find the controller and its handlers under the `/src/modules/technology/` folder. + +The handlers have caching enabled using the [cachified](https://www.npmjs.com/package/cachified) package. It uses redis under the hood. For more information on these endpoints, see the code, or check out the `localhost:3333/docs` after you start up your development server. + +## Database and Redis + +In order to start up your API in dev mode with an active database connection, please follow the following steps: + +1. create a `.env` file. For the defaults, copy the contents of the `.env.example` file's content into it. +2. run `npm run infrastructure:start` +3. run `npm run dev` + +The above steps will make sure your API connects to the database and redis instances that gets started up with docker. When you finish work, run `npm run infrastructure:stop` to stop your database and redis containers. + +### Seeding + +In the `src/db/run-seeders.ts` file, we provide a script to seed the database with intial values, using TypeOrm. Under the `src/db/seeding` folder, you can find the `TechnologySeeder` class, that seeds values into the database as an example. + +In order to seed the database, you need to do the following steps: + +1. create a `.env` file. For the defaults, copy the contents of the `.env.example` file's content into it. +2. run `npm run infrastructure:start` +3. run `npm run db:seed` + +### Reset infrastructure + +If you for some reason need to clear the contents of your database and you want to reinitialise it, delete the `misc/pg_data` folder and delete the postgres docker container. After that the next `infrastructure:start` command will start up as it would the first time. + +If you would like to clear your redis cache and reinitialise it, delete the `misc/cache_conf` and the `misc/cache_data` folders and delete the cache docker container. + +If you would like to clear your redis queue and reinitialise it, delete the `misc/queue_conf` and the `misc/queue_data` folders and delete the queue docker container. + +### Production build + +The `npm run build` command compiles the typescript code into the `/dist` folder and generates a `package.json` file. To use it in production, for example in a docker container, one would copy the contents of the `/dist` folder, and then run `npm install` to have all the dependencies. + +### CORS Cross-Origin Resource Sharing + +The [Cross-Origin Resource Sharing](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) standard works by adding new HTTP headers that let servers describe which origins are permitted to read that information from a web browser. For Security reasons, browsers restrict cross-origin HTTP requests initiated from scripts. This means that you cannot request data from web application on 'https://domain-a.com' from 'https://domain-b.com/data.json'. + +This application accepts CORS from all origins by default. Some web applications may require you to add the HTTP header `'Access-Control-Allow-Origin': '*'` to allow access. + +In order to restrict origins urls that can access your api, you need to add a list of comma separated origin urls in the `CORS_ALLOWED_ORIGINS` variable located in your `.env` file. For example `CORS_ALLOWED_ORIGINS="https://starter.dev"`. In case you need to access the api in a development environment i.e. a sveltekit application, you can add the local url `http://127.0.0.1` to the `CORS_ALLOWED_ORIGINS` variable as `CORS_ALLOWED_ORIGINS=https://starter.dev,http://127.0.0.1`. + +## Kit Organization / Architecture + +### Folder structure + +```text +- misc +- src + - cache + - constants + - db + - interfaces + - middlewares + - modules + - queue + - utils +- tools +``` + +The `misc` folder contains sub-folders for the infrastructure docker containers. When you start up your infrastructure, the sub-folders get mounted to the redis and postgres docker containers. This allows persisting data during development and lets developers to quickly get rid of database contents and reinitalise their infrastructure. + +The `src` folder contains everything that is related to API development. The `cache`, `db` and `queue` folders contain everything that has to do with connecting to the redis and postgres containers. The `constants`, `utils` and `interfaces` folders contain logic, types and variables that are / can be shared across the codebase. The `middlewares` folder contains custom and/or customised middlewares for the application. + +The `src/modules` folder contains the controllers, route handlers and services separated in feature related directories. Every feature directory should contain logic related to that particular feature. + +The `tools` folder contains scripts that help with generating files or building the app. For example, a generator script is provided that creates a sanitized package.json for the production built code, which can be used to install only the dependencies used in the API. + +### Express + +The ExpressJS API starts at the `main.ts` file. The `bootstrapApp()` method creates and sets up the routes. The API routes are set up under the `src/modules` folder. This set up differentiates modules based on the feature they provide, and in a feature directory you can find the `controller`, related `services` and the `route handlers`. + +### TypeOrm + +TypeOrm related initiators are set up under the `src/db` folder, the `initialiseDataSource()` function gets called at start-up. It has a built-in retry mechanism that can be configured using environment variables. See the `.env.example` file for more information. + +The `DataSource` is set up to look for entities automatically. This kit uses the `src/db/entities` folder to store these, but feel free to store your entities in feature folders or where it makes more sense to you. + +You can create your own Entities using the tools provided by TypeOrm. For more information, please refer to [the documentation](https://typeorm.io/entities). + +### Caching + +Caching is set up with the [cachified](https://www.npmjs.com/package/cachified) library. It utilises redis in the background for caching. Under the `cache` folder you can find the redis client connection and the two functions that are used for caching and invalidating. See the `useCache` and the `clearCacheEntry` methods used in the example CRUD handlers, under `src/modules/technology/handlers`. + +### Queue + +The queue is set up using [BullMQ](https://www.npmjs.com/package/bullmq) with a redis instance separate from the cache redis instance. You can find how it is set up under the `src/queue` folder. + +We set it up to utilise processing in a separate thread. You can trigger the queue by sending a `POST` request to `localhost:3333/queue` with a request body of your choice. You can customise the queue and the job processors as you see fit, for more information on how to do it, please refer to the [BullMQ documentation](https://docs.bullmq.io/). + +### Testing + +Testing is set up with [Jest](https://jestjs.io/). You can see some example spec files under `src/modules/technology/handlers`. + +### API documentation and Schema generation + +The kit uses [express-oas-generator](https://www.npmjs.com/package/express-oas-generator) middlewares that generates the OpenAPI documentation into the `swagger.json` and `swagger_v3.json` files. When you are building new API endpoints, the API documentation for those endpoints will be generated. + +In order to for this middleware to be able to generate all the data, make sure you hit your freshly created endpoints by using Postman or other similar tools. This is how you can keep the documentation up-to-date. If you'd like to generate an entirely new API documentation, feel free to delete the swagger related json files and restart your dev-server to start from scratch. + +When you run the development server, you can find the generated Swagger API documentation page under `localhost:3333/docs`. Please note, that if you don't want to expose this documentation in production, make sure you set the `NODE_ENV` environment variable to `production`. + +If you'd like to generate a schema typescript file, run `npm run generate:schema` that will place a `schema.ts` file under the `src/interfaces` folder. This schema will be generated based on the existing `swagger_v3.json` file. + diff --git a/starters/express-typeorm-postgres/docker-compose.yml b/starters/express-typeorm-postgres/docker-compose.yml new file mode 100644 index 000000000..d9ee6479a --- /dev/null +++ b/starters/express-typeorm-postgres/docker-compose.yml @@ -0,0 +1,32 @@ +version: '3' +services: + postgres: + image: 'postgres' + ports: + - '5432:5432' + volumes: + - ./misc/pg_data:/var/lib/postgresql/data + env_file: + - ./postgres.db.env + + redis_cache: + image: 'redis:alpine' + command: redis-server + ports: + - '6379:6379' + volumes: + - ./misc/cache_data:/var/lib/redis + - ./misc/cache_conf:/usr/local/etc/redis/redis.conf + environment: + - REDIS_REPLICATION_MODE=master + + redis_queue: + image: 'redis:alpine' + command: redis-server + ports: + - '6479:6379' + volumes: + - ./misc/queue_data:/var/lib/redis + - ./misc/queue_conf:/usr/local/etc/redis/redis.conf + environment: + - REDIS_REPLICATION_MODE=master diff --git a/starters/express-typeorm-postgres/package.json b/starters/express-typeorm-postgres/package.json new file mode 100644 index 000000000..a83afe280 --- /dev/null +++ b/starters/express-typeorm-postgres/package.json @@ -0,0 +1,96 @@ +{ + "name": "express-typeorm-postgres", + "version": "0.0.1", + "description": "Express.js, TypeOrm, and PostgreSQL", + "keywords": [ + "express", + "typeorm", + "postgres" + ], + "hasShowcase": false, + "license": "MIT", + "scripts": { + "prebuild": "rimraf dist", + "build": "tsc --project ./tsconfig.build.json", + "postbuild": "node tools/generators/generate-prod-package-json.js", + "build:watch": "tsc --watch --project ./tsconfig.build.json", + "prestart": "npm run build", + "start": "nodemon dist/main", + "dev": "concurrently --raw -n \"tsc,server\" -s \"last\" -c \"magenta,yellow\" \"npm run build:watch\" \"npm run start\"", + "infrastructure:start": "docker-compose up -d", + "infrastructure:stop": "docker-compose stop", + "db:seed": "ts-node src/db/run-seeders.ts", + "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", + "format": "prettier --write \"src/**/*.ts\"", + "format:check": "prettier --check \"src/**/*.ts\"", + "test": "jest", + "test:watch": "jest --watch", + "test:cov": "jest --coverage", + "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", + "generate:schema": "npx --yes openapi-typescript swagger_v3.json --output src/interfaces/schema.ts" + }, + "dependencies": { + "bullmq": "3.3.5", + "cachified": "3.0.1", + "cors": "2.8.5", + "dotenv": "^16.0.3", + "express": "4.18.2", + "express-oas-generator": "1.0.45", + "http-status-codes": "2.2.0", + "pg": "8.8.0", + "redis": "4.5.1", + "reflect-metadata": "0.1.13", + "swagger-ui-express": "4.6.0", + "typeorm": "0.3.10", + "typeorm-extension": "2.3.0" + }, + "devDependencies": { + "@types/express": "4.17.13", + "@types/jest": "28.1.8", + "@types/node": "16.0.0", + "@typescript-eslint/eslint-plugin": "5.0.0", + "@typescript-eslint/parser": "5.0.0", + "concurrently": "7.6.0", + "eslint": "8.0.1", + "eslint-config-prettier": "8.3.0", + "eslint-plugin-prettier": "4.0.0", + "jest": "28.1.3", + "nodemon": "2.0.20", + "prettier": "2.3.2", + "rimraf": "3.0.2", + "source-map-support": "0.5.20", + "ts-jest": "28.0.8", + "ts-loader": "9.2.3", + "ts-node": "10.7.0", + "tsconfig-paths": "4.1.0", + "typescript": "4.7.4" + }, + "jest": { + "moduleFileExtensions": [ + "js", + "json", + "ts" + ], + "rootDir": "src", + "testRegex": ".*\\.spec\\.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + }, + "collectCoverageFrom": [ + "**/*.(t|j)s" + ], + "coverageDirectory": "../coverage", + "testEnvironment": "node" + }, + "nodemonConfig": { + "ignore": [ + "pg_data", + "cache_data", + "cache_conf", + "queue_data", + "queue_conf", + "**/swagger.json", + "**/swagger_v3.json" + ] + } +} diff --git a/starters/express-typeorm-postgres/postgres.db.env b/starters/express-typeorm-postgres/postgres.db.env new file mode 100644 index 000000000..18e3bd252 --- /dev/null +++ b/starters/express-typeorm-postgres/postgres.db.env @@ -0,0 +1,3 @@ +POSTGRES_DB=pgdatabase +POSTGRES_INITDB_ARGS='--data-checksums' +POSTGRES_PASSWORD=ettpg diff --git a/starters/express-typeorm-postgres/src/bootstrap-app.ts b/starters/express-typeorm-postgres/src/bootstrap-app.ts new file mode 100644 index 000000000..cb301fa83 --- /dev/null +++ b/starters/express-typeorm-postgres/src/bootstrap-app.ts @@ -0,0 +1,36 @@ +import express, { Express, NextFunction, Request, Response } from 'express'; +import expressOasGenerator, { SPEC_OUTPUT_FILE_BEHAVIOR } from 'express-oas-generator'; +import { getReasonPhrase, StatusCodes } from 'http-status-codes'; +import { apiRouter } from './modules/router'; +import { corsMiddleware } from './middlewares/cors'; + +export function bootstrapApp(): Express { + const app = express(); + + expressOasGenerator.handleResponses(app, { + specOutputPath: 'swagger.json', + writeIntervalMs: 2000, + swaggerUiServePath: 'docs', + specOutputFileBehavior: SPEC_OUTPUT_FILE_BEHAVIOR.PRESERVE, + swaggerDocumentOptions: { + version: '3.0.3', + }, + }); + app.use(express.json()); + + app.options('*', corsMiddleware); + app.use(corsMiddleware); + + app.use('/', apiRouter); + app.use(genericErrorHandler); + expressOasGenerator.handleRequests(); + return app; +} + +function genericErrorHandler(err, req: Request, res: Response, next: NextFunction) { + console.error('An unexpected error occurred', err); + res + .status(StatusCodes.INTERNAL_SERVER_ERROR) + .send({ error: getReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR) }); + return next(); +} diff --git a/starters/express-typeorm-postgres/src/cache/cache.ts b/starters/express-typeorm-postgres/src/cache/cache.ts new file mode 100644 index 000000000..b26f75ae1 --- /dev/null +++ b/starters/express-typeorm-postgres/src/cache/cache.ts @@ -0,0 +1,28 @@ +import { cachified, redisCacheAdapter } from 'cachified'; +import { GetFreshValue } from 'cachified/dist/common'; +import * as process from 'process'; +import { CACHE_HEALTH, cacheRedisClient } from './redis-cache-client'; + +const FIVE_MINUTES_IN_MS = 300_000; +const REDIS_CACHE_TTL = process.env.REDIS_CACHE_TTL_IN_MS + ? parseInt(process.env.REDIS_CACHE_TTL_IN_MS) + : FIVE_MINUTES_IN_MS; + +const cachifiedCache = redisCacheAdapter(cacheRedisClient); + +export async function useCache(key: string, cb: GetFreshValue): Promise { + if (!CACHE_HEALTH.isConnected) { + return cb(); + } + + return cachified({ + key, + cache: cachifiedCache, + getFreshValue: cb, + ttl: REDIS_CACHE_TTL, + }); +} + +export function clearCacheEntry(key: string) { + cachifiedCache.delete(key); +} diff --git a/starters/express-typeorm-postgres/src/cache/redis-cache-client.ts b/starters/express-typeorm-postgres/src/cache/redis-cache-client.ts new file mode 100644 index 000000000..4f0a78afc --- /dev/null +++ b/starters/express-typeorm-postgres/src/cache/redis-cache-client.ts @@ -0,0 +1,48 @@ +import process from 'process'; +import { createClient } from 'redis'; +import { LogHelper } from '../utils/log-helper'; + +const REDIS_CACHE_HOST = process.env.REDIS_CACHE_HOST || 'localhost'; +const REDIS_CACHE_PORT = process.env.REDIS_CACHE_PORT + ? parseInt(process.env.REDIS_CACHE_PORT) + : 6379; + +export const cacheRedisClient = createClient({ + socket: { + host: REDIS_CACHE_HOST, + port: REDIS_CACHE_PORT, + }, +}); + +export const CACHE_HEALTH = { + isConnected: false, + error: null, +}; + +cacheRedisClient.on('error', (err) => { + if (CACHE_HEALTH.isConnected) { + LogHelper.error('[CACHE] An error occurred while connecting to Redis', err); + CACHE_HEALTH.isConnected = false; + CACHE_HEALTH.error = err; + } +}); + +cacheRedisClient.on('reconnecting', () => { + LogHelper.info(`[CACHE] Reconnecting to Redis instance`); +}); + +cacheRedisClient.on('connect', () => { + LogHelper.info(`[CACHE] Successfully connected to Redis instance`); + CACHE_HEALTH.isConnected = true; + CACHE_HEALTH.error = null; +}); + +export function redisHealthCheck(): Promise<'CONNECTED'> { + return new Promise((resolve, reject) => { + if (CACHE_HEALTH.isConnected) { + resolve('CONNECTED'); + } else { + reject(CACHE_HEALTH.error); + } + }); +} diff --git a/starters/express-typeorm-postgres/src/constants/result.ts b/starters/express-typeorm-postgres/src/constants/result.ts new file mode 100644 index 000000000..bcee8748d --- /dev/null +++ b/starters/express-typeorm-postgres/src/constants/result.ts @@ -0,0 +1,5 @@ +export enum Result { + SUCCESS = 'SUCCESS', + NOT_FOUND = 'NOT_FOUND', + ERROR = 'ERROR', +} diff --git a/starters/express-typeorm-postgres/src/db/datasource.ts b/starters/express-typeorm-postgres/src/db/datasource.ts new file mode 100644 index 000000000..f6fe4c53f --- /dev/null +++ b/starters/express-typeorm-postgres/src/db/datasource.ts @@ -0,0 +1,43 @@ +import { DataSource } from 'typeorm'; + +const DATABASE_ENABLE_LOGGING = process.env.DATABASE_ENABLE_LOGGING === 'true'; +const DATABASE_ENABLE_SYNC = process.env.DATABASE_ENABLE_SYNC === 'true'; + +export const dataSource = new DataSource({ + type: 'postgres', + host: process.env.DATABASE_HOST || 'localhost', + port: parseInt(process.env.DATABASE_PORT || '5432'), + username: process.env.DATABASE_USER, + password: process.env.DATABASE_PASSWORD, + database: process.env.DATABASE_NAME, + entities: ['**/*.entity.js'], + logging: DATABASE_ENABLE_LOGGING, + synchronize: DATABASE_ENABLE_SYNC, +}); + +const DATABASE_RETRY_COUNT = process.env.DATABASE_CONNECT_RETRY_COUNT + ? parseInt(process.env.DATABASE_CONNECT_RETRY_COUNT) + : 5; + +const DATABASE_RETRY_INTERVAL_MS = process.env.DATABASE_CONNECT_RETRY_COUNT + ? parseInt(process.env.DATABASE_CONNECT_RETRY_INTERVAL_MS) + : 5000; + +export async function initialiseDataSource(retries = DATABASE_RETRY_COUNT): Promise { + return dataSource + .initialize() + .then(() => true) + .catch((err) => { + const remainingRetries = retries - 1; + console.warn(`Could not connect to the database, retrying ${remainingRetries} more time(s)`); + if (remainingRetries === 0) { + console.error(`Error during Data Source initialisation:`, err); + return false; + } + return new Promise((resolve) => { + setTimeout(() => { + initialiseDataSource(remainingRetries).then(resolve); + }, DATABASE_RETRY_INTERVAL_MS); + }); + }); +} diff --git a/starters/express-typeorm-postgres/src/db/entities/technology.entity.ts b/starters/express-typeorm-postgres/src/db/entities/technology.entity.ts new file mode 100644 index 000000000..b8f300db7 --- /dev/null +++ b/starters/express-typeorm-postgres/src/db/entities/technology.entity.ts @@ -0,0 +1,15 @@ +import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; + +@Entity({ name: 'technology' }) +export class Technology { + @PrimaryGeneratedColumn() + id!: number; + + @Column({ + length: 256, + }) + displayName!: string; + + @Column('text') + description!: string; +} diff --git a/starters/express-typeorm-postgres/src/db/run-seeders.ts b/starters/express-typeorm-postgres/src/db/run-seeders.ts new file mode 100644 index 000000000..7e364ec18 --- /dev/null +++ b/starters/express-typeorm-postgres/src/db/run-seeders.ts @@ -0,0 +1,29 @@ +// eslint-disable-next-line @typescript-eslint/no-var-requires +require('dotenv').config(); +import { DataSource, DataSourceOptions } from 'typeorm'; +import { runSeeders, SeederOptions } from 'typeorm-extension'; +import { Technology } from './entities/technology.entity'; +import TechnologySeeder from './seeding/technology-seeder'; + +const options: DataSourceOptions & SeederOptions = { + type: 'postgres', + host: process.env.DATABASE_HOST || 'localhost', + port: parseInt(process.env.DATABASE_PORT || '5432'), + username: process.env.DATABASE_USER, + password: process.env.DATABASE_PASSWORD, + database: process.env.DATABASE_NAME, + logging: true, + /** Import all of your entities you want to seed to the database */ + entities: [Technology], + // Additional configuration by typeorm-extension + factories: [], + seeds: [TechnologySeeder], +}; + +const dataSource = new DataSource(options); + +dataSource.initialize().then(async () => { + await dataSource.synchronize(true); + await runSeeders(dataSource); + process.exit(); +}); diff --git a/starters/express-typeorm-postgres/src/db/seeding/technology-seeder.ts b/starters/express-typeorm-postgres/src/db/seeding/technology-seeder.ts new file mode 100644 index 000000000..79b86b755 --- /dev/null +++ b/starters/express-typeorm-postgres/src/db/seeding/technology-seeder.ts @@ -0,0 +1,27 @@ +import { DataSource } from 'typeorm'; +import { Seeder, SeederFactoryManager } from 'typeorm-extension'; +import { Technology } from '../entities/technology.entity'; + +export default class TechnologySeeder implements Seeder { + public async run(dataSource: DataSource, factoryManager: SeederFactoryManager): Promise { + const repository = dataSource.getRepository(Technology); + + await repository.insert([ + { + displayName: 'Express', + description: + 'Express is a minimal and flexible Node.js web application framework that provides a robust set of features for web and mobile applications. APIs.', + }, + { + displayName: 'TypeOrm', + description: + 'TypeORM is a TypeScript ORM (object-relational mapper) library that makes it easy to link your TypeScript application up to a relational database database.', + }, + { + displayName: 'Postgres', + description: + 'PostgreSQL, also known as Postgres, is a free and open-source relational database management system emphasizing extensibility and SQL compliance.', + }, + ]); + } +} diff --git a/starters/express-typeorm-postgres/src/interfaces/results.ts b/starters/express-typeorm-postgres/src/interfaces/results.ts new file mode 100644 index 000000000..914121736 --- /dev/null +++ b/starters/express-typeorm-postgres/src/interfaces/results.ts @@ -0,0 +1,17 @@ +import { Result } from '../constants/result'; + +export interface SuccessResult { + type: Result.SUCCESS; + data: T; +} + +export interface NotFoundResult { + type: Result.NOT_FOUND; + message: string; +} + +export interface ErrorResult { + type: Result.ERROR; + message: string; + error?: unknown; +} diff --git a/starters/express-typeorm-postgres/src/interfaces/schema.ts b/starters/express-typeorm-postgres/src/interfaces/schema.ts new file mode 100644 index 000000000..3864fde5e --- /dev/null +++ b/starters/express-typeorm-postgres/src/interfaces/schema.ts @@ -0,0 +1,197 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +export interface paths { + '*': { + /** * */ + options: { + /** * */ + responses: {}; + }; + }; + '/health': { + /** /health */ + get: { + /** /health */ + responses: { + /** @description OK */ + 200: { + content: { + 'application/json': { + /** @example PostgreSQL 15.1 (Debian 15.1-1.pgdg110+1) on aarch64-unknown-linux-gnu, compiled by gcc (Debian 10.2.1-6) 10.2.1 20210110, 64-bit */ + database?: string; + /** @example CONNECTED */ + redisCacheConnection?: string; + redisQueueHealth?: { + /** @example PONG */ + connection?: string; + /** @example 0 */ + activeCount?: number; + /** @example 0 */ + waitingCount?: number; + /** @example 2 */ + completedCount?: number; + /** @example 0 */ + failedCount?: number; + }; + }; + }; + }; + 304: never; + }; + }; + }; + '/technology': { + /** /technology */ + get: { + /** /technology */ + responses: { + /** @description OK */ + 200: { + content: { + 'application/json': { + id: number; + displayName: string; + description: string; + }[]; + }; + }; + 304: never; + }; + }; + /** /technology */ + post: { + /** /technology */ + requestBody: components['requestBodies']['Body']; + responses: { + /** @description Accepted */ + 202: { + content: { + 'application/json': { + /** @example 12 */ + id?: number; + }; + }; + }; + }; + }; + }; + '/technology/{technologyId}': { + /** /technology/{technologyId} */ + get: { + /** /technology/{technologyId} */ + parameters: { + path: { + technologyId: string; + }; + }; + responses: { + /** @description OK */ + 200: { + content: { + '*/*': { + id: number; + displayName: string; + description: string; + }; + }; + }; + 304: never; + }; + }; + /** /technology/{technologyId} */ + put: { + /** /technology/{technologyId} */ + parameters: { + path: { + technologyId: string; + }; + }; + requestBody: components['requestBodies']['Body']; + responses: { + /** @description Accepted */ + 200: { + content: { + '*/*': { + /** @example 12 */ + id?: number; + }; + }; + }; + }; + }; + /** /technology/{technologyId} */ + delete: { + /** /technology/{technologyId} */ + parameters: { + path: { + technologyId: string; + }; + }; + responses: { + /** @description Accepted */ + 200: { + content: { + '*/*': { + /** @example 12 */ + id?: number; + }; + }; + }; + }; + }; + }; + '/queue': { + /** /queue */ + post: { + /** /queue */ + requestBody: { + content: { + 'application/json': { + /** @example It can be anything */ + data?: string; + }; + }; + }; + responses: { + /** @description OK */ + 200: { + content: { + 'application/json': { + /** @example 6 */ + jobId?: string; + }; + }; + }; + }; + }; + }; +} + +export type webhooks = Record; + +export interface components { + schemas: never; + responses: never; + parameters: never; + requestBodies: { + Body: { + content: { + 'application/json': { + /** @example BullMQ */ + displayName?: string; + /** @example A javascript library that leverages Redis to set up queues */ + description?: string; + }; + }; + }; + }; + headers: never; + pathItems: never; +} + +export type external = Record; + +export type operations = Record; diff --git a/starters/express-typeorm-postgres/src/main.ts b/starters/express-typeorm-postgres/src/main.ts new file mode 100644 index 000000000..51bc62ca9 --- /dev/null +++ b/starters/express-typeorm-postgres/src/main.ts @@ -0,0 +1,31 @@ +// eslint-disable-next-line @typescript-eslint/no-var-requires +require('dotenv').config(); +import { bootstrapApp } from './bootstrap-app'; +import { cacheRedisClient } from './cache/redis-cache-client'; +import { initialiseDataSource } from './db/datasource'; +import { LogHelper } from './utils/log-helper'; + +const PORT = process.env.PORT || 3333; + +const app = bootstrapApp(); + +initialiseDataSource().then((isInitialised: boolean) => { + if (isInitialised) { + LogHelper.log(`DataSource has been initialised!`); + } else { + LogHelper.error(`Could not initialise database connection`); + } +}); + +cacheRedisClient + .connect() + .then(() => { + LogHelper.debug('Connected to redis'); + }) + .catch((e) => { + LogHelper.error(`Could not connect to redis`, e); + }); + +app.listen(PORT, () => { + LogHelper.info(`Example app listening on port ${PORT}`); +}); diff --git a/starters/express-typeorm-postgres/src/middlewares/cors/index.ts b/starters/express-typeorm-postgres/src/middlewares/cors/index.ts new file mode 100644 index 000000000..7ecc25bb0 --- /dev/null +++ b/starters/express-typeorm-postgres/src/middlewares/cors/index.ts @@ -0,0 +1,22 @@ +import cors from 'cors'; + +let middleware = cors(); + +if (process.env.CORS_ALLOWED_ORIGINS) { + const allowedOrigins: string[] = (process.env.CORS_ALLOWED_ORIGINS || '').split(','); + if (allowedOrigins.length) { + const corsOptions = { + origin: (origin, callback) => { + if (allowedOrigins.some((allowedOrigin) => allowedOrigin.startsWith(origin))) { + callback(null, true); + } else { + callback(new Error('The Current Origin is not allowed by CORS')); + } + }, + optionsSuccessStatus: 200, + }; + middleware = cors(corsOptions); + } +} + +export const corsMiddleware = middleware; diff --git a/starters/express-typeorm-postgres/src/modules/health/handlers/get-health.ts b/starters/express-typeorm-postgres/src/modules/health/handlers/get-health.ts new file mode 100644 index 000000000..d85307320 --- /dev/null +++ b/starters/express-typeorm-postgres/src/modules/health/handlers/get-health.ts @@ -0,0 +1,54 @@ +import { Request, Response } from 'express'; +import { getReasonPhrase, StatusCodes } from 'http-status-codes'; +import { Result } from '../../../constants/result'; +import { + checkDatabaseConnection, + checkRedisCacheConnection, + checkRedisQueHealth, +} from '../services/health.service'; + +export async function getHealth(req: Request, res: Response): Promise { + const [databaseVersion, redisCacheConnectionResult, redisQueueHealthResult] = await Promise.all([ + checkDatabaseConnection(), + checkRedisCacheConnection(), + checkRedisQueHealth(), + ]); + + if (databaseVersion.type === Result.ERROR) { + res.status(StatusCodes.SERVICE_UNAVAILABLE).json({ + error: getReasonPhrase(StatusCodes.SERVICE_UNAVAILABLE), + details: databaseVersion.message, + }); + return; + } + + if (redisQueueHealthResult.type === Result.ERROR) { + res.status(StatusCodes.SERVICE_UNAVAILABLE).json({ + error: getReasonPhrase(StatusCodes.SERVICE_UNAVAILABLE), + details: redisQueueHealthResult.message, + }); + return; + } + + if (redisCacheConnectionResult.type === Result.ERROR) { + /** + * We return 200 here, because if the cache server is down the application still works + * If you need to, set up alerting based on the contents of this response or notify your + * alerting system programmatically. + * + * You can also change the status code if you'd prefer this to be an error. + */ + res.status(StatusCodes.OK).json({ + database: databaseVersion.data, + redisCacheConnection: 'CONNECTION ERROR', + error: redisCacheConnectionResult.error, + }); + return; + } + + res.json({ + database: databaseVersion.data, + redisCacheConnection: redisCacheConnectionResult.data, + redisQueueHealth: redisQueueHealthResult.data, + }); +} diff --git a/starters/express-typeorm-postgres/src/modules/health/health.controller.ts b/starters/express-typeorm-postgres/src/modules/health/health.controller.ts new file mode 100644 index 000000000..cfbbddd91 --- /dev/null +++ b/starters/express-typeorm-postgres/src/modules/health/health.controller.ts @@ -0,0 +1,8 @@ +import { Router } from 'express'; +import { getHealth } from './handlers/get-health'; + +const healthRouter = Router(); + +healthRouter.get('/', getHealth); + +export const HealthController = { router: healthRouter }; diff --git a/starters/express-typeorm-postgres/src/modules/health/services/health.service.ts b/starters/express-typeorm-postgres/src/modules/health/services/health.service.ts new file mode 100644 index 000000000..881fa9d92 --- /dev/null +++ b/starters/express-typeorm-postgres/src/modules/health/services/health.service.ts @@ -0,0 +1,95 @@ +import { RedisClient } from 'bullmq'; +import { redisHealthCheck } from '../../../cache/redis-cache-client'; +import { Result } from '../../../constants/result'; +import { dataSource } from '../../../db/datasource'; +import { ErrorResult, SuccessResult } from '../../../interfaces/results'; +import { defaultQueue } from '../../../queue/queue'; +import { LogHelper } from '../../../utils/log-helper'; + +export function checkDatabaseConnection(): Promise< + SuccessResult<{ version: string }> | ErrorResult +> { + return dataSource + .query(`SELECT version()`) + .then>((databaseVersion) => ({ + type: Result.SUCCESS, + data: databaseVersion[0].version, + })) + .catch((error) => { + LogHelper.error(error); + return { + type: Result.ERROR, + message: error.message, + error, + }; + }); +} + +export function checkRedisCacheConnection(): Promise | ErrorResult> { + return redisHealthCheck() + .then>((answer: 'CONNECTED') => ({ + type: Result.SUCCESS, + data: answer, + })) + .catch((error) => { + LogHelper.error(error); + return { + type: Result.ERROR, + message: error.message, + error, + }; + }); +} + +type RedisQueueHealth = { + connection: 'PONG'; + activeCount: number; + waitingCount: number; + completedCount: number; + failedCount: number; +}; + +export function checkRedisQueHealth(): Promise | ErrorResult> { + return Promise.all([ + pingRedisQueueWithTimeout(), + defaultQueue.getActiveCount(), + defaultQueue.getWaitingCount(), + defaultQueue.getCompletedCount(), + defaultQueue.getFailedCount(), + ]) + .then>( + ([pingResult, activeCount, waitingCount, completedCount, failedCount]) => ({ + type: Result.SUCCESS, + data: { + connection: pingResult, + activeCount, + waitingCount, + completedCount, + failedCount, + }, + }) + ) + .catch((error) => { + LogHelper.error(error); + return { + type: Result.ERROR, + message: error.message, + error, + }; + }); +} + +function pingRedisQueueWithTimeout(): Promise<'PONG'> { + return defaultQueue.client.then((client: RedisClient) => { + return Promise.race([ + client.ping(), + new Promise<'PONG'>((_, reject) => { + setTimeout(() => { + reject( + new Error(`TIMEOUT ERROR, Redis Queue Client did not respond to ping under 2 seconds`) + ); + }, 2000); + }), + ]); + }); +} diff --git a/starters/express-typeorm-postgres/src/modules/queue/queue.controller.ts b/starters/express-typeorm-postgres/src/modules/queue/queue.controller.ts new file mode 100644 index 000000000..01e742063 --- /dev/null +++ b/starters/express-typeorm-postgres/src/modules/queue/queue.controller.ts @@ -0,0 +1,12 @@ +import { NextFunction, Request, Response, Router } from 'express'; +import { addJob } from '../../queue/queue'; + +const queueRouter = Router(); + +queueRouter.post('/', async (req: Request, res: Response, next: NextFunction) => { + const job = await addJob(req.body); + res.json({ jobId: job.id }); + return next(); +}); + +export const QueueRouter = { router: queueRouter }; diff --git a/starters/express-typeorm-postgres/src/modules/router.ts b/starters/express-typeorm-postgres/src/modules/router.ts new file mode 100644 index 000000000..da3e5a383 --- /dev/null +++ b/starters/express-typeorm-postgres/src/modules/router.ts @@ -0,0 +1,12 @@ +import { Router } from 'express'; +import { HealthController } from './health/health.controller'; +import { QueueRouter } from './queue/queue.controller'; +import { TechnologyController } from './technology/technology.controller'; + +const router = Router(); + +router.use('/health', HealthController.router); +router.use('/technology', TechnologyController.router); +router.use('/queue', QueueRouter.router); + +export const apiRouter = router; diff --git a/starters/express-typeorm-postgres/src/modules/technology/handlers/create-technology.spec.ts b/starters/express-typeorm-postgres/src/modules/technology/handlers/create-technology.spec.ts new file mode 100644 index 000000000..44788c5fe --- /dev/null +++ b/starters/express-typeorm-postgres/src/modules/technology/handlers/create-technology.spec.ts @@ -0,0 +1,56 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { dataSource } from '../../../db/datasource'; +import { createTechnology } from './create-technology'; + +jest.mock('../../../cache/cache', () => ({ + clearCacheEntry: () => void 0, +})); + +const MOCK_REQUEST: any = { + body: { + name: 'Jest', + }, +}; +const MOCK_RESPONSE: any = { + status: jest.fn(), + json: jest.fn(), +}; +const MOCK_NEXT_FN = jest.fn(); + +const MOCK_REPOSITORY: any = { + insert: jest.fn(), +}; + +const MOCK_TECHNOLOGY = { id: 1, technology: 'Jest ' }; + +describe(createTechnology.name, () => { + beforeEach(() => { + jest.spyOn(dataSource, 'getRepository').mockReturnValue(MOCK_REPOSITORY); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it(`Calls the 'next()' function with the error, if the database request rejects`, async () => { + MOCK_REPOSITORY.insert.mockRejectedValue(new Error('Test')); + + await createTechnology(MOCK_REQUEST, MOCK_RESPONSE, MOCK_NEXT_FN); + + expect(MOCK_NEXT_FN).toHaveBeenCalledTimes(1); + expect(MOCK_NEXT_FN).toHaveBeenCalledWith(new Error('Test')); + }); + + it(`Returns with 200 status code and with the inserted technology`, async () => { + MOCK_REPOSITORY.insert.mockResolvedValue({ raw: [MOCK_TECHNOLOGY] }); + MOCK_RESPONSE.status.mockReturnValue(MOCK_RESPONSE); + + await createTechnology(MOCK_REQUEST, MOCK_RESPONSE, MOCK_NEXT_FN); + + expect(MOCK_RESPONSE.status).toHaveBeenCalledTimes(1); + expect(MOCK_RESPONSE.status).toHaveBeenCalledWith(202); + expect(MOCK_RESPONSE.json).toHaveBeenCalledTimes(1); + expect(MOCK_RESPONSE.json).toHaveBeenCalledWith({ id: MOCK_TECHNOLOGY.id }); + expect(MOCK_NEXT_FN).toHaveBeenCalledTimes(0); + }); +}); diff --git a/starters/express-typeorm-postgres/src/modules/technology/handlers/create-technology.ts b/starters/express-typeorm-postgres/src/modules/technology/handlers/create-technology.ts new file mode 100644 index 000000000..0cfa8015f --- /dev/null +++ b/starters/express-typeorm-postgres/src/modules/technology/handlers/create-technology.ts @@ -0,0 +1,26 @@ +import { NextFunction, Request, Response } from 'express'; +import { StatusCodes } from 'http-status-codes'; +import { clearCacheEntry } from '../../../cache/cache'; +import { Result } from '../../../constants/result'; +import { LogHelper } from '../../../utils/log-helper'; +import { insertTechnology } from '../services/technology.service'; + +export async function createTechnology( + req: Request, + res: Response, + next: NextFunction +): Promise { + const inserted = await insertTechnology({ + displayName: req.body.displayName, + description: req.body.description, + }); + + if (inserted.type === Result.ERROR) { + LogHelper.error(inserted.message, inserted.error); + return next(inserted.error); + } + + clearCacheEntry(req.baseUrl); + + res.status(StatusCodes.ACCEPTED).json(inserted.data); +} diff --git a/starters/express-typeorm-postgres/src/modules/technology/handlers/delete-technology.spec.ts b/starters/express-typeorm-postgres/src/modules/technology/handlers/delete-technology.spec.ts new file mode 100644 index 000000000..14a251213 --- /dev/null +++ b/starters/express-typeorm-postgres/src/modules/technology/handlers/delete-technology.spec.ts @@ -0,0 +1,56 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { dataSource } from '../../../db/datasource'; +import { deleteTechnology } from './delete-technology'; + +jest.mock('../../../cache/cache', () => ({ + clearCacheEntry: () => void 0, +})); + +const MOCK_REQUEST: any = { + params: { + technologyId: '1', + }, +}; +const MOCK_RESPONSE: any = { + status: jest.fn(), + json: jest.fn(), +}; +const MOCK_NEXT_FN = jest.fn(); + +const MOCK_REPOSITORY: any = { + delete: jest.fn(), +}; + +const MOCK_TECHNOLOGY = { id: 1, technology: 'Jest ' }; + +describe(deleteTechnology.name, () => { + beforeEach(() => { + jest.spyOn(dataSource, 'getRepository').mockReturnValue(MOCK_REPOSITORY); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it(`Calls the 'next()' function with the error, if the database request rejects`, async () => { + MOCK_REPOSITORY.delete.mockRejectedValue(new Error('Test')); + + await deleteTechnology(MOCK_REQUEST, MOCK_RESPONSE, MOCK_NEXT_FN); + + expect(MOCK_NEXT_FN).toHaveBeenCalledTimes(1); + expect(MOCK_NEXT_FN).toHaveBeenCalledWith(new Error('Test')); + }); + + it(`Returns with 200 status code and with the inserted technology`, async () => { + MOCK_REPOSITORY.delete.mockResolvedValue({ raw: MOCK_TECHNOLOGY }); + MOCK_RESPONSE.status.mockReturnValue(MOCK_RESPONSE); + + await deleteTechnology(MOCK_REQUEST, MOCK_RESPONSE, MOCK_NEXT_FN); + + expect(MOCK_RESPONSE.status).toHaveBeenCalledTimes(1); + expect(MOCK_RESPONSE.status).toHaveBeenCalledWith(200); + expect(MOCK_RESPONSE.json).toHaveBeenCalledTimes(1); + expect(MOCK_RESPONSE.json).toHaveBeenCalledWith({ id: MOCK_TECHNOLOGY.id }); + expect(MOCK_NEXT_FN).toHaveBeenCalledTimes(0); + }); +}); diff --git a/starters/express-typeorm-postgres/src/modules/technology/handlers/delete-technology.ts b/starters/express-typeorm-postgres/src/modules/technology/handlers/delete-technology.ts new file mode 100644 index 000000000..e4eeffaf8 --- /dev/null +++ b/starters/express-typeorm-postgres/src/modules/technology/handlers/delete-technology.ts @@ -0,0 +1,27 @@ +import { NextFunction, Request, Response } from 'express'; +import { StatusCodes } from 'http-status-codes'; +import { clearCacheEntry } from '../../../cache/cache'; +import { Result } from '../../../constants/result'; +import { LogHelper } from '../../../utils/log-helper'; +import { deleteTechnologyEntry } from '../services/technology.service'; + +export async function deleteTechnology( + req: Request, + res: Response, + next: NextFunction +): Promise { + const technologyId: number = parseInt(req.params.technologyId); + const deleteResult = await deleteTechnologyEntry(technologyId); + + if (deleteResult.type === Result.ERROR) { + LogHelper.error(deleteResult.message, deleteResult.error); + return next(deleteResult.error); + } + + clearCacheEntry(req.baseUrl); + clearCacheEntry(req.originalUrl); + + res.status(StatusCodes.OK).json({ + id: technologyId, + }); +} diff --git a/starters/express-typeorm-postgres/src/modules/technology/handlers/get-all-technologies.spec.ts b/starters/express-typeorm-postgres/src/modules/technology/handlers/get-all-technologies.spec.ts new file mode 100644 index 000000000..634ddc1fd --- /dev/null +++ b/starters/express-typeorm-postgres/src/modules/technology/handlers/get-all-technologies.spec.ts @@ -0,0 +1,51 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { dataSource } from '../../../db/datasource'; +import { getAllTechnologies } from './get-all-technologies'; + +jest.mock('../../../cache/cache', () => ({ + useCache: (key, callback) => callback(), +})); + +const MOCK_REQUEST: any = {}; +const MOCK_RESPONSE: any = { + status: jest.fn(), + json: jest.fn(), +}; +const MOCK_NEXT_FN = jest.fn(); + +const MOCK_REPOSITORY: any = { + find: jest.fn(), +}; + +describe(getAllTechnologies.name, () => { + beforeEach(() => { + jest.spyOn(dataSource, 'getRepository').mockReturnValue(MOCK_REPOSITORY); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it(`Calls the 'next()' function with the error, if the database request rejects`, async () => { + MOCK_REPOSITORY.find.mockRejectedValue(new Error('Test')); + + await getAllTechnologies(MOCK_REQUEST, MOCK_RESPONSE, MOCK_NEXT_FN); + + expect(MOCK_NEXT_FN).toHaveBeenCalledTimes(1); + expect(MOCK_NEXT_FN).toHaveBeenCalledWith(new Error('Test')); + }); + + it(`Returns with status 200 and the values returned from the database`, async () => { + MOCK_REPOSITORY.find.mockResolvedValue([]); + MOCK_RESPONSE.status.mockReturnValue(MOCK_RESPONSE); + + await getAllTechnologies(MOCK_REQUEST, MOCK_RESPONSE, MOCK_NEXT_FN); + + expect(MOCK_RESPONSE.status).toHaveBeenCalledTimes(1); + expect(MOCK_RESPONSE.status).toHaveBeenCalledWith(200); + expect(MOCK_RESPONSE.json).toHaveBeenCalledTimes(1); + expect(MOCK_RESPONSE.json).toHaveBeenCalledWith([]); + + expect(MOCK_NEXT_FN).toHaveBeenCalledTimes(0); + }); +}); diff --git a/starters/express-typeorm-postgres/src/modules/technology/handlers/get-all-technologies.ts b/starters/express-typeorm-postgres/src/modules/technology/handlers/get-all-technologies.ts new file mode 100644 index 000000000..9640cc29c --- /dev/null +++ b/starters/express-typeorm-postgres/src/modules/technology/handlers/get-all-technologies.ts @@ -0,0 +1,24 @@ +import { NextFunction, Request, Response } from 'express'; +import { StatusCodes } from 'http-status-codes'; +import { useCache } from '../../../cache/cache'; +import { Result } from '../../../constants/result'; +import { LogHelper } from '../../../utils/log-helper'; +import { getTechnologies, TechnologiesResult } from '../services/technology.service'; + +export async function getAllTechnologies( + req: Request, + res: Response, + next: NextFunction +): Promise { + const technologiesResult = await useCache(req.baseUrl, getTechnologies); + + if (technologiesResult.type === Result.ERROR) { + LogHelper.error( + 'An unexpected error occurred', + technologiesResult.message, + technologiesResult.error + ); + return next(technologiesResult.error); + } + res.status(StatusCodes.OK).json(technologiesResult.data); +} diff --git a/starters/express-typeorm-postgres/src/modules/technology/handlers/get-technology.spec.ts b/starters/express-typeorm-postgres/src/modules/technology/handlers/get-technology.spec.ts new file mode 100644 index 000000000..0521393dc --- /dev/null +++ b/starters/express-typeorm-postgres/src/modules/technology/handlers/get-technology.spec.ts @@ -0,0 +1,69 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { dataSource } from '../../../db/datasource'; +import { getTechnology } from './get-technology'; + +jest.mock('../../../cache/cache', () => ({ + useCache: (key, callback) => callback(), +})); + +const MOCK_REQUEST: any = { + params: { + technologyId: '1', + }, +}; +const MOCK_RESPONSE: any = { + status: jest.fn(), + json: jest.fn(), +}; +const MOCK_NEXT_FN = jest.fn(); + +const MOCK_REPOSITORY: any = { + findOne: jest.fn(), +}; + +const MOCK_TECHNOLOGY = { id: 1, technology: 'jest ' }; + +describe(getTechnology.name, () => { + beforeEach(() => { + jest.spyOn(dataSource, 'getRepository').mockReturnValue(MOCK_REPOSITORY); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it(`Calls the 'next()' function with the error, if the database request rejects`, async () => { + MOCK_REPOSITORY.findOne.mockRejectedValue(new Error('Test')); + + await getTechnology(MOCK_REQUEST, MOCK_RESPONSE, MOCK_NEXT_FN); + + expect(MOCK_NEXT_FN).toHaveBeenCalledTimes(1); + expect(MOCK_NEXT_FN).toHaveBeenCalledWith(new Error('Test')); + }); + + it(`Returns with 200 status code and with the retrieved technology`, async () => { + MOCK_REPOSITORY.findOne.mockResolvedValue(MOCK_TECHNOLOGY); + + await getTechnology(MOCK_REQUEST, MOCK_RESPONSE, MOCK_NEXT_FN); + + expect(MOCK_RESPONSE.json).toHaveBeenCalledTimes(1); + expect(MOCK_RESPONSE.json).toHaveBeenCalledWith(MOCK_TECHNOLOGY); + expect(MOCK_NEXT_FN).toHaveBeenCalledTimes(0); + }); + + it(`Returns with 404 status code and a corresponding error message when there is no entry in the database`, async () => { + MOCK_REPOSITORY.findOne.mockResolvedValue(null); + MOCK_RESPONSE.status.mockReturnValue(MOCK_RESPONSE); + + await getTechnology(MOCK_REQUEST, MOCK_RESPONSE, MOCK_NEXT_FN); + + expect(MOCK_RESPONSE.status).toHaveBeenCalledTimes(1); + expect(MOCK_RESPONSE.status).toHaveBeenCalledWith(404); + expect(MOCK_RESPONSE.json).toHaveBeenCalledTimes(1); + expect(MOCK_RESPONSE.json).toHaveBeenCalledWith({ + error: 'Not Found', + details: `Could not find technology with id: ${MOCK_TECHNOLOGY.id}`, + }); + expect(MOCK_NEXT_FN).toHaveBeenCalledTimes(0); + }); +}); diff --git a/starters/express-typeorm-postgres/src/modules/technology/handlers/get-technology.ts b/starters/express-typeorm-postgres/src/modules/technology/handlers/get-technology.ts new file mode 100644 index 000000000..5b9ed2565 --- /dev/null +++ b/starters/express-typeorm-postgres/src/modules/technology/handlers/get-technology.ts @@ -0,0 +1,31 @@ +import { NextFunction, Request, Response } from 'express'; +import { getReasonPhrase, StatusCodes } from 'http-status-codes'; +import { useCache } from '../../../cache/cache'; +import { Result } from '../../../constants/result'; +import { LogHelper } from '../../../utils/log-helper'; +import { findTechnology, TechnologyResult } from '../services/technology.service'; + +export async function getTechnology( + req: Request, + res: Response, + next: NextFunction +): Promise { + const technologyId: number = parseInt(req.params.technologyId); + const technologyResult = await useCache(req.originalUrl, () => + findTechnology(technologyId) + ); + + if (technologyResult.type === Result.ERROR) { + LogHelper.error(technologyResult.message, technologyResult.error); + return next(technologyResult.error); + } + + if (technologyResult.type === Result.NOT_FOUND) { + res.status(StatusCodes.NOT_FOUND).json({ + error: getReasonPhrase(StatusCodes.NOT_FOUND), + details: technologyResult.message, + }); + return; + } + res.json(technologyResult.data); +} diff --git a/starters/express-typeorm-postgres/src/modules/technology/handlers/update-technology.spec.ts b/starters/express-typeorm-postgres/src/modules/technology/handlers/update-technology.spec.ts new file mode 100644 index 000000000..7ea8035a5 --- /dev/null +++ b/starters/express-typeorm-postgres/src/modules/technology/handlers/update-technology.spec.ts @@ -0,0 +1,59 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { dataSource } from '../../../db/datasource'; +import { updateTechnology } from './update-technology'; + +jest.mock('../../../cache/cache', () => ({ + clearCacheEntry: () => void 0, +})); + +const MOCK_REQUEST: any = { + params: { + technologyId: '1', + }, + body: { + name: 'Jest', + }, +}; +const MOCK_RESPONSE: any = { + status: jest.fn(), + json: jest.fn(), +}; +const MOCK_NEXT_FN = jest.fn(); + +const MOCK_REPOSITORY: any = { + update: jest.fn(), +}; + +const MOCK_TECHNOLOGY = { id: 1, technology: 'Jest ' }; + +describe(updateTechnology.name, () => { + beforeEach(() => { + jest.spyOn(dataSource, 'getRepository').mockReturnValue(MOCK_REPOSITORY); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it(`Calls the 'next()' function with the error, if the database request rejects`, async () => { + MOCK_REPOSITORY.update.mockRejectedValue(new Error('Test')); + + await updateTechnology(MOCK_REQUEST, MOCK_RESPONSE, MOCK_NEXT_FN); + + expect(MOCK_NEXT_FN).toHaveBeenCalledTimes(1); + expect(MOCK_NEXT_FN).toHaveBeenCalledWith(new Error('Test')); + }); + + it(`Returns with 200 status code and with the updated technology`, async () => { + MOCK_REPOSITORY.update.mockResolvedValue({ raw: [] }); + MOCK_RESPONSE.status.mockReturnValue(MOCK_RESPONSE); + + await updateTechnology(MOCK_REQUEST, MOCK_RESPONSE, MOCK_NEXT_FN); + + expect(MOCK_RESPONSE.status).toHaveBeenCalledTimes(1); + expect(MOCK_RESPONSE.status).toHaveBeenCalledWith(200); + expect(MOCK_RESPONSE.json).toHaveBeenCalledTimes(1); + expect(MOCK_RESPONSE.json).toHaveBeenCalledWith({ id: MOCK_TECHNOLOGY.id }); + expect(MOCK_NEXT_FN).toHaveBeenCalledTimes(0); + }); +}); diff --git a/starters/express-typeorm-postgres/src/modules/technology/handlers/update-technology.ts b/starters/express-typeorm-postgres/src/modules/technology/handlers/update-technology.ts new file mode 100644 index 000000000..179211688 --- /dev/null +++ b/starters/express-typeorm-postgres/src/modules/technology/handlers/update-technology.ts @@ -0,0 +1,28 @@ +import { NextFunction, Request, Response } from 'express'; +import { StatusCodes } from 'http-status-codes'; +import { clearCacheEntry } from '../../../cache/cache'; +import { Result } from '../../../constants/result'; +import { LogHelper } from '../../../utils/log-helper'; +import { updateTechnologyEntry } from '../services/technology.service'; + +export async function updateTechnology( + req: Request, + res: Response, + next: NextFunction +): Promise { + const technologyId: number = parseInt(req.params.technologyId); + const updateResult = await updateTechnologyEntry(technologyId, { + displayName: req.body.name, + description: req.body.description, + }); + + if (updateResult.type === Result.ERROR) { + LogHelper.error(updateResult.message, updateResult.error); + return next(updateResult.error); + } + + clearCacheEntry(req.baseUrl); + clearCacheEntry(req.originalUrl); + + res.status(StatusCodes.OK).json(updateResult.data); +} diff --git a/starters/express-typeorm-postgres/src/modules/technology/services/technology.service.ts b/starters/express-typeorm-postgres/src/modules/technology/services/technology.service.ts new file mode 100644 index 000000000..2124f04ff --- /dev/null +++ b/starters/express-typeorm-postgres/src/modules/technology/services/technology.service.ts @@ -0,0 +1,111 @@ +import { InsertResult } from 'typeorm'; +import { Result } from '../../../constants/result'; +import { dataSource } from '../../../db/datasource'; +import { Technology } from '../../../db/entities/technology.entity'; +import { ErrorResult, NotFoundResult, SuccessResult } from '../../../interfaces/results'; + +export type CreateOrUpdateTechnologyResult = SuccessResult<{ id: Technology['id'] }> | ErrorResult; +export type DeleteTechnologyResult = SuccessResult | ErrorResult; +export type TechnologiesResult = SuccessResult | ErrorResult; +export type TechnologyResult = SuccessResult | NotFoundResult | ErrorResult; + +export function updateTechnologyEntry( + technologyId: number, + technologyData: Omit +): Promise { + return dataSource + .getRepository(Technology) + .update( + { + id: technologyId, + }, + technologyData + ) + .then>(() => ({ + type: Result.SUCCESS, + data: { id: technologyId }, + })) + .catch((error) => ({ + type: Result.ERROR, + message: `An unexpected error occurred during updating technology with id ${technologyId}`, + error, + })); +} + +export function insertTechnology( + technology: Omit +): Promise { + return dataSource + .getRepository(Technology) + .insert(technology) + .then>((insertedTechnology: InsertResult) => ({ + type: Result.SUCCESS, + data: { id: insertedTechnology.raw[0].id }, + })) + .catch((error) => ({ + type: Result.ERROR, + message: `An unexpected error occurred during creating technology`, + error, + })); +} + +export function deleteTechnologyEntry(technologyId: number): Promise { + return dataSource + .getRepository(Technology) + .delete({ + id: technologyId, + }) + .then>(() => ({ + type: Result.SUCCESS, + data: null, + })) + .catch((error) => ({ + type: Result.ERROR, + message: `An unexpected error occurred while deleting technology with id ${technologyId}`, + error: error, + })); +} + +export function getTechnologies(): Promise | ErrorResult> { + return dataSource + .getRepository(Technology) + .find() + .then>((technologies: Technology[]) => ({ + type: Result.SUCCESS, + data: technologies, + })) + .catch((error) => ({ + type: Result.ERROR, + message: error.message, + error, + })); +} + +export function findTechnology(technologyId: number): Promise { + return dataSource + .getRepository(Technology) + .findOne({ + where: { + id: technologyId, + }, + }) + .then | NotFoundResult>((result) => { + if (!result) { + return { + type: Result.NOT_FOUND, + message: `Could not find technology with id: ${technologyId}`, + }; + } + return { + type: Result.SUCCESS, + data: result, + }; + }) + .catch((error) => { + return { + type: Result.ERROR, + message: `Unexpected error while fetching technology with id ${technologyId}`, + error, + }; + }); +} diff --git a/starters/express-typeorm-postgres/src/modules/technology/technology.controller.ts b/starters/express-typeorm-postgres/src/modules/technology/technology.controller.ts new file mode 100644 index 000000000..b3f1c4499 --- /dev/null +++ b/starters/express-typeorm-postgres/src/modules/technology/technology.controller.ts @@ -0,0 +1,18 @@ +import { Router } from 'express'; +import { createTechnology } from './handlers/create-technology'; +import { deleteTechnology } from './handlers/delete-technology'; +import { getAllTechnologies } from './handlers/get-all-technologies'; +import { getTechnology } from './handlers/get-technology'; +import { updateTechnology } from './handlers/update-technology'; + +const technologyRouter = Router(); + +technologyRouter.get('/', getAllTechnologies); +technologyRouter.get('/:technologyId', getTechnology); + +technologyRouter.post('/', createTechnology); +technologyRouter.put('/:technologyId', updateTechnology); + +technologyRouter.delete('/:technologyId', deleteTechnology); + +export const TechnologyController = { router: technologyRouter }; diff --git a/starters/express-typeorm-postgres/src/queue/config.constants.ts b/starters/express-typeorm-postgres/src/queue/config.constants.ts new file mode 100644 index 000000000..681a1680c --- /dev/null +++ b/starters/express-typeorm-postgres/src/queue/config.constants.ts @@ -0,0 +1,15 @@ +export const REDIS_QUEUE_HOST = process.env.REDIS_QUEUE_HOST || 'localhost'; +export const REDIS_QUEUE_PORT = process.env.REDIS_QUEUE_PORT + ? parseInt(process.env.REDIS_QUEUE_PORT) + : 6479; + +export const queueName = 'defaultQueue'; + +export const DEFAULT_REMOVE_CONFIG = { + removeOnComplete: { + age: 3600, + }, + removeOnFail: { + age: 24 * 3600, + }, +}; diff --git a/starters/express-typeorm-postgres/src/queue/job-processor.ts b/starters/express-typeorm-postgres/src/queue/job-processor.ts new file mode 100644 index 000000000..3c5901ba0 --- /dev/null +++ b/starters/express-typeorm-postgres/src/queue/job-processor.ts @@ -0,0 +1,9 @@ +import { Job } from 'bullmq'; +import { LogHelper } from '../utils/log-helper'; + +module.exports = async function jobProcessor(job: Job): Promise<'DONE'> { + await job.log(`Started processing job`); + LogHelper.info(`Job with id ${job.id}`, job.data); + await job.updateProgress(100); + return 'DONE'; +}; diff --git a/starters/express-typeorm-postgres/src/queue/queue.ts b/starters/express-typeorm-postgres/src/queue/queue.ts new file mode 100644 index 000000000..2675893da --- /dev/null +++ b/starters/express-typeorm-postgres/src/queue/queue.ts @@ -0,0 +1,41 @@ +import { Queue, Job, Worker } from 'bullmq'; +import path from 'node:path'; +import { LogHelper } from '../utils/log-helper'; +import { + DEFAULT_REMOVE_CONFIG, + queueName, + REDIS_QUEUE_HOST, + REDIS_QUEUE_PORT, +} from './config.constants'; + +export const defaultQueue = new Queue(queueName, { + connection: { + host: REDIS_QUEUE_HOST, + port: REDIS_QUEUE_PORT, + }, +}); + +const processorPath = path.join(__dirname, 'job-processor.js'); +export const defaultWorker = new Worker(queueName, processorPath, { + connection: { + host: REDIS_QUEUE_HOST, + port: REDIS_QUEUE_PORT, + }, + autorun: true, +}); + +defaultWorker.on('completed', (job: Job, returnvalue: 'DONE') => { + LogHelper.debug(`Completed job with id ${job.id}`, returnvalue); +}); + +defaultWorker.on('active', (job: Job) => { + LogHelper.debug(`Completed job with id ${job.id}`); +}); +defaultWorker.on('error', (failedReason: Error) => { + LogHelper.error(`Job encountered an error`, failedReason); +}); +export async function addJob(data: T): Promise> { + LogHelper.debug(`Adding job to queue`); + + return defaultQueue.add('job', data, DEFAULT_REMOVE_CONFIG); +} diff --git a/starters/express-typeorm-postgres/src/utils/log-helper.spec.ts b/starters/express-typeorm-postgres/src/utils/log-helper.spec.ts new file mode 100644 index 000000000..507adf6ca --- /dev/null +++ b/starters/express-typeorm-postgres/src/utils/log-helper.spec.ts @@ -0,0 +1,50 @@ +import { ALL_LOG_LEVELS } from './log-helper'; + +type LogMethods = 'log' | 'info' | 'debug' | 'warn' | 'error'; +type LogLevels = 'INFO' | 'DEBUG' | 'WARN' | 'ERROR'; + +describe(`LogHelper`, () => { + let consoleSpy; + beforeEach(() => { + jest.resetModules(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe.each([ + ['log', 'INFO'], + ['info', 'INFO'], + ['debug', 'DEBUG'], + ['warn', 'WARN'], + ['error', 'ERROR'], + ])(`%s function`, (method: LogMethods, level: LogLevels) => { + beforeEach(() => { + consoleSpy = jest.spyOn(console, method); + }); + + it(`does not call 'console.${method}()' if the ${level} is not provided in the ENV variable`, () => { + process.env.ALLOWED_LOG_LEVELS = ALL_LOG_LEVELS.replace(level, ''); + + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { LogHelper } = require('./log-helper'); + + LogHelper[method]('Test'); + + expect(consoleSpy).toHaveBeenCalledTimes(0); + }); + + it(`calls 'console.${method}()', if the ${level} is provided in the ENV variable`, () => { + process.env.ALLOWED_LOG_LEVELS = ALL_LOG_LEVELS; + + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { LogHelper } = require('./log-helper'); + + LogHelper[method]('Test'); + + expect(consoleSpy).toHaveBeenCalledTimes(1); + expect(consoleSpy).lastCalledWith(`[${level}] Test`); + }); + }); +}); diff --git a/starters/express-typeorm-postgres/src/utils/log-helper.ts b/starters/express-typeorm-postgres/src/utils/log-helper.ts new file mode 100644 index 000000000..bd71003f4 --- /dev/null +++ b/starters/express-typeorm-postgres/src/utils/log-helper.ts @@ -0,0 +1,31 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import * as process from 'process'; + +export const ALL_LOG_LEVELS = 'INFO,DEBUG,WARN,ERROR'; +type LogLevel = 'INFO' | 'DEBUG' | 'WARN' | 'ERROR'; + +const LOG_LEVELS: LogLevel[] = (process.env.ALLOWED_LOG_LEVELS || ALL_LOG_LEVELS).split( + ',' +) as LogLevel[]; + +const ENABLED_LEVELS = new Set(LOG_LEVELS); + +function logger(method, level: LogLevel = 'INFO') { + return (message?: unknown, ...optionalParams: unknown[]) => { + if (ENABLED_LEVELS.has(level)) { + if (typeof message === 'string') { + console[method](`[${level}] ${message}`, ...optionalParams); + } else { + console[method](`[${level}]`, message, ...optionalParams); + } + } + }; +} + +export const LogHelper = { + log: logger('log'), + info: logger('info'), + debug: logger('debug', 'DEBUG'), + warn: logger('warn', 'WARN'), + error: logger('error', 'ERROR'), +}; diff --git a/starters/express-typeorm-postgres/swagger.json b/starters/express-typeorm-postgres/swagger.json new file mode 100644 index 000000000..84695180b --- /dev/null +++ b/starters/express-typeorm-postgres/swagger.json @@ -0,0 +1,329 @@ +{ + "definitions": {}, + "host": "localhost:3333", + "info": { + "title": "express-typeorm-postgres", + "version": "0.0.1", + "license": { + "name": "MIT" + }, + "description": "Specification JSONs: [v2](/api-spec/v2), [v3](/api-spec/v3).\n\nexpress, typescript, REST" + }, + "paths": { + "*": { + "options": { + "summary": "*", + "consumes": [ + "application/json" + ], + "parameters": [], + "responses": {}, + "tags": [] + } + }, + "/health": { + "get": { + "summary": "/health", + "consumes": [ + "application/json" + ], + "parameters": [], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "properties": { + "database": { + "type": "string", + "example": "PostgreSQL 15.1 (Debian 15.1-1.pgdg110+1) on aarch64-unknown-linux-gnu, compiled by gcc (Debian 10.2.1-6) 10.2.1 20210110, 64-bit" + }, + "redisCacheConnection": { + "type": "string", + "example": "CONNECTED" + }, + "redisQueueHealth": { + "type": "object", + "properties": { + "connection": { + "type": "string", + "example": "PONG" + }, + "activeCount": { + "type": "number", + "example": 0 + }, + "waitingCount": { + "type": "number", + "example": 0 + }, + "completedCount": { + "type": "number", + "example": 2 + }, + "failedCount": { + "type": "number", + "example": 0 + } + } + } + } + } + }, + "304": {} + }, + "tags": [], + "produces": [ + "application/json" + ] + } + }, + "/technology": { + "get": { + "summary": "/technology", + "consumes": [ + "application/json" + ], + "parameters": [], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "displayName": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "required": [ + "id", + "displayName", + "description" + ] + } + } + }, + "304": {} + }, + "tags": [], + "produces": [ + "application/json" + ] + }, + "post": { + "summary": "/technology", + "consumes": [ + "application/json" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "type": "object", + "properties": { + "displayName": { + "type": "string", + "example": "BullMQ" + }, + "description": { + "type": "string", + "example": "A javascript library that leverages Redis to set up queues" + } + } + } + } + ], + "responses": { + "202": { + "description": "Accepted", + "schema": { + "type": "object", + "properties": { + "id": { + "type": "number", + "example": 12 + } + } + } + } + }, + "tags": [], + "produces": [ + "application/json" + ] + } + }, + "/technology/{technologyId}": { + "get": { + "summary": "/technology/{technologyId}", + "consumes": [ + "application/json" + ], + "parameters": [ + { + "name": "technologyId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "displayName": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "required": [ + "id", + "displayName", + "description" + ] + } + }, + "304": {} + }, + "tags": [] + }, + "put": { + "summary": "/technology/{technologyId}", + "consumes": [ + "application/json" + ], + "parameters": [ + { + "name": "technologyId", + "in": "path", + "required": true + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "type": "object", + "properties": { + "displayName": { + "type": "string", + "example": "BullMQ" + }, + "description": { + "type": "string", + "example": "A javascript library that leverages Redis to set up queues" + } + } + } + } + ], + "responses": { + "200": { + "description": "Accepted", + "schema": { + "type": "object", + "properties": { + "id": { + "type": "number", + "example": 12 + } + } + } + } + }, + "tags": [] + }, + "delete": { + "summary": "/technology/{technologyId}", + "consumes": [ + "application/json" + ], + "parameters": [ + { + "name": "technologyId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Accepted", + "schema": { + "type": "object", + "properties": { + "id": { + "type": "number", + "example": 12 + } + } + } + } + }, + "tags": [] + } + }, + "/queue": { + "post": { + "summary": "/queue", + "consumes": [ + "application/json" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "type": "object", + "properties": { + "data": { + "type": "string", + "example": "It can be anything" + } + } + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "properties": { + "jobId": { + "type": "string", + "example": "6" + } + } + } + } + }, + "tags": [], + "produces": [ + "application/json" + ] + } + } + }, + "schemes": [ + "http" + ], + "swagger": "2.0", + "tags": [] +} \ No newline at end of file diff --git a/starters/express-typeorm-postgres/swagger_v3.json b/starters/express-typeorm-postgres/swagger_v3.json new file mode 100644 index 000000000..319038797 --- /dev/null +++ b/starters/express-typeorm-postgres/swagger_v3.json @@ -0,0 +1,325 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "express-typeorm-postgres", + "version": "0.0.1", + "license": { + "name": "MIT" + }, + "description": "Specification JSONs: [v2](/api-spec/v2), [v3](/api-spec/v3).\n\nexpress, typescript, REST" + }, + "paths": { + "*": { + "options": { + "summary": "*", + "responses": {}, + "tags": [] + } + }, + "/health": { + "get": { + "summary": "/health", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "database": { + "type": "string", + "example": "PostgreSQL 15.1 (Debian 15.1-1.pgdg110+1) on aarch64-unknown-linux-gnu, compiled by gcc (Debian 10.2.1-6) 10.2.1 20210110, 64-bit" + }, + "redisCacheConnection": { + "type": "string", + "example": "CONNECTED" + }, + "redisQueueHealth": { + "type": "object", + "properties": { + "connection": { + "type": "string", + "example": "PONG" + }, + "activeCount": { + "type": "number", + "example": 0 + }, + "waitingCount": { + "type": "number", + "example": 0 + }, + "completedCount": { + "type": "number", + "example": 2 + }, + "failedCount": { + "type": "number", + "example": 0 + } + } + } + } + } + } + } + }, + "304": { + "description": "" + } + }, + "tags": [] + } + }, + "/technology": { + "get": { + "summary": "/technology", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "displayName": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "required": [ + "id", + "displayName", + "description" + ] + } + } + } + } + }, + "304": { + "description": "" + } + }, + "tags": [] + }, + "post": { + "summary": "/technology", + "requestBody": { + "$ref": "#/components/requestBodies/Body" + }, + "responses": { + "202": { + "description": "Accepted", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "number", + "example": 12 + } + } + } + } + } + } + }, + "tags": [] + } + }, + "/technology/{technologyId}": { + "get": { + "summary": "/technology/{technologyId}", + "parameters": [ + { + "name": "technologyId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "displayName": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "required": [ + "id", + "displayName", + "description" + ] + } + } + } + }, + "304": { + "description": "" + } + }, + "tags": [] + }, + "put": { + "summary": "/technology/{technologyId}", + "parameters": [ + { + "name": "technologyId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "$ref": "#/components/requestBodies/Body" + }, + "responses": { + "200": { + "description": "Accepted", + "content": { + "*/*": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "number", + "example": 12 + } + } + } + } + } + } + }, + "tags": [] + }, + "delete": { + "summary": "/technology/{technologyId}", + "parameters": [ + { + "name": "technologyId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Accepted", + "content": { + "*/*": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "number", + "example": 12 + } + } + } + } + } + } + }, + "tags": [] + } + }, + "/queue": { + "post": { + "summary": "/queue", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "string", + "example": "It can be anything" + } + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "jobId": { + "type": "string", + "example": "6" + } + } + } + } + } + } + }, + "tags": [] + } + } + }, + "tags": [], + "servers": [ + { + "url": "http://localhost:3333" + } + ], + "components": { + "requestBodies": { + "Body": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "displayName": { + "type": "string", + "example": "BullMQ" + }, + "description": { + "type": "string", + "example": "A javascript library that leverages Redis to set up queues" + } + } + } + } + }, + "required": true + } + } + } +} \ No newline at end of file diff --git a/starters/express-typeorm-postgres/tools/generators/generate-prod-package-json.js b/starters/express-typeorm-postgres/tools/generators/generate-prod-package-json.js new file mode 100644 index 000000000..5bfbc3f65 --- /dev/null +++ b/starters/express-typeorm-postgres/tools/generators/generate-prod-package-json.js @@ -0,0 +1,25 @@ +const path = require('path'); +const fs = require('fs'); + +const packageJsonPath = path.resolve(__dirname, '../../package.json'); +const projectPackageJsonString = fs.readFileSync(packageJsonPath, { encoding: 'utf-8' }); +const projectPackageJson = JSON.parse(projectPackageJsonString); + +const productionPackageJson = { + name: projectPackageJson.name || '', + version: projectPackageJson.version || '', + description: projectPackageJson.description || '', + author: projectPackageJson.author || '', + license: projectPackageJson.license || '', + dependencies: projectPackageJson.dependencies, +}; + +const distFolderPath = path.resolve(__dirname, '../../dist'); +const isDistFolderExisting = fs.existsSync(distFolderPath); +if (!isDistFolderExisting) { + fs.mkdirSync(distFolderPath); +} + +const productionPackageJsonString = JSON.stringify(productionPackageJson, null, 2); + +fs.writeFileSync(path.join(distFolderPath, 'package.json'), productionPackageJsonString, { encoding: "utf-8" }) diff --git a/starters/express-typeorm-postgres/tsconfig.build.json b/starters/express-typeorm-postgres/tsconfig.build.json new file mode 100644 index 000000000..64f86c6bd --- /dev/null +++ b/starters/express-typeorm-postgres/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] +} diff --git a/starters/express-typeorm-postgres/tsconfig.json b/starters/express-typeorm-postgres/tsconfig.json new file mode 100644 index 000000000..e058f1211 --- /dev/null +++ b/starters/express-typeorm-postgres/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "module": "commonjs", + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "target": "es2017", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": "./", + "incremental": true, + "skipLibCheck": true, + "strictNullChecks": false, + "noImplicitAny": false, + "strictBindCallApply": false, + "forceConsistentCasingInFileNames": false, + "noFallthroughCasesInSwitch": false + } +}