main 117f6647c5aa cached
76 files
66.8 KB
19.3k tokens
24 symbols
1 requests
Download .txt
Repository: mkosir/typeorm-express-typescript
Branch: main
Commit: 117f6647c5aa
Files: 76
Total size: 66.8 KB

Directory structure:
gitextract_whv901lw/

├── .commitlintrc.ts
├── .czrc
├── .dockerignore
├── .eslintrc.js
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.yml
│   │   └── config.yml
│   └── workflows/
│       └── main.yml
├── .gitignore
├── .husky/
│   ├── commit-msg
│   ├── pre-commit
│   └── pre-push
├── .lintstagedrc
├── .prettierignore
├── .prettierrc
├── Dockerfile
├── LICENSE
├── README.md
├── config/
│   ├── prod.env
│   └── stage.env
├── database/
│   └── .gitkeep
├── docker-compose.dev.yml
├── docker-compose.prod.yml
├── docker-compose.stage.yml
├── docker-compose.yml
├── log/
│   └── .gitkeep
├── package.json
├── postman/
│   ├── RESTful_API_Boilerplate.postman_collection.json
│   └── RESTful_API_Boilerplate.postman_environment.json
├── renovate.json
├── scripts/
│   ├── be-node-dev.sh
│   └── wait-for-it.sh
├── src/
│   ├── consts/
│   │   └── ConstsUser.ts
│   ├── controllers/
│   │   ├── auth/
│   │   │   ├── changePassword.ts
│   │   │   ├── index.ts
│   │   │   ├── login.test.ts
│   │   │   ├── login.ts
│   │   │   ├── register.test.ts
│   │   │   └── register.ts
│   │   └── users/
│   │       ├── destroy.ts
│   │       ├── edit.ts
│   │       ├── index.test.ts
│   │       ├── index.ts
│   │       ├── list.ts
│   │       └── show.ts
│   ├── index.ts
│   ├── middleware/
│   │   ├── checkJwt.ts
│   │   ├── checkRole.ts
│   │   ├── errorHandler.ts
│   │   ├── getLanguage.ts
│   │   └── validation/
│   │       ├── auth/
│   │       │   ├── index.ts
│   │       │   ├── validatorChangePassword.ts
│   │       │   ├── validatorLogin.ts
│   │       │   └── validatorRegister.ts
│   │       └── users/
│   │           ├── index.ts
│   │           └── validatorEdit.ts
│   ├── orm/
│   │   ├── config/
│   │   │   ├── ormconfig-seed.ts
│   │   │   └── ormconfig.ts
│   │   ├── dbCreateConnection.ts
│   │   ├── entities/
│   │   │   └── users/
│   │   │       ├── User.ts
│   │   │       └── types.ts
│   │   ├── migrations/
│   │   │   └── 1590521920166-CreateUsers.ts
│   │   └── seeds/
│   │       └── 1590519635401-SeedUsers.ts
│   ├── routes/
│   │   ├── index.ts
│   │   ├── pages/
│   │   │   ├── 404.ts
│   │   │   └── root.ts
│   │   └── v1/
│   │       ├── auth.ts
│   │       ├── index.ts
│   │       └── users.ts
│   ├── types/
│   │   ├── JwtPayload.ts
│   │   ├── ProcessEnv.d.ts
│   │   └── express/
│   │       └── index.d.ts
│   └── utils/
│       ├── createJwtToken.ts
│       └── response/
│           ├── custom-error/
│           │   ├── CustomError.ts
│           │   └── types.ts
│           └── customSuccess.ts
└── tsconfig.json

================================================
FILE CONTENTS
================================================

================================================
FILE: .commitlintrc.ts
================================================
import type { UserConfig } from '@commitlint/types';

const commitlintConfig: UserConfig = {
  extends: ['@commitlint/config-conventional'],
};

module.exports = commitlintConfig;


================================================
FILE: .czrc
================================================
{
  "path": "node_modules/cz-conventional-changelog"
}


================================================
FILE: .dockerignore
================================================
config
database
dist
node_modules
scripts
.git
.env

================================================
FILE: .eslintrc.js
================================================
module.exports = {
  parser: '@typescript-eslint/parser',

  extends: [
    'plugin:@typescript-eslint/recommended',
    'prettier',
    'plugin:prettier/recommended',
    'plugin:no-array-reduce/recommended',
  ],

  plugins: ['@typescript-eslint', 'import'],

  rules: {
    // General
    '@typescript-eslint/no-unused-vars': 0,
    '@typescript-eslint/explicit-module-boundary-types': 0,
    '@typescript-eslint/no-explicit-any': 0,
    '@typescript-eslint/no-non-null-assertion': 0,
    '@typescript-eslint/ban-ts-comment': 0,
    '@typescript-eslint/no-empty-interface': 0,

    // Import
    'import/order': [
      'error',
      {
        groups: ['builtin', 'external', 'internal', 'parent', 'sibling'],
        'newlines-between': 'always',
        alphabetize: {
          order: 'asc',
          caseInsensitive: true,
        },
      },
    ],
  },

  parserOptions: {
    ecmaVersion: 2018,
    sourceType: 'module',
  },

  settings: {
    'import/resolver': {
      node: {
        extensions: ['.js', '.jsx', '.ts', '.tsx'],
        moduleDirectory: ['node_modules', 'src/'],
      },
    },
  },
};


================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.yml
================================================
name: 'Bug Report'
description: 'File a bug report'
body:
  - type: 'textarea'
    id: 'description'
    attributes:
      label: 'Description'
      description: 'A clear and concise description of what the bug is.'
      placeholder: |
        Bug description
    validations:
      required: true
  - type: 'textarea'
    id: 'additional-information'
    attributes:
      label: 'Additional Information'
      description: |
        Add any other context about the problem here.


================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: false
contact_links:
  - name: Ask a question
    url: https://github.com/mkosir/express-typescript-typeorm-boilerplate/discussions
    about: Ask questions and discuss topics with other community members


================================================
FILE: .github/workflows/main.yml
================================================
name: CI

on: push

jobs:
  validate:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout 🛎️
        uses: actions/checkout@v2
      - uses: actions/setup-node@v2
        with:
          node-version: '16.x'

      - name: Cache dependencies ⚡
        id: cache
        uses: actions/cache@v2
        with:
          path: node_modules
          key: node-modules-${{ hashFiles('package-lock.json') }}

      - name: Install dependencies 🔧
        if: steps.cache.outputs.cache-hit != 'true'
        run: npm ci

      - name: Lint ✅
        run: npm run lint

      - name: Build 🏗️
        run: npm run build


================================================
FILE: .gitignore
================================================
.idea/
.vscode/
.DS_Store

node_modules/
build/
dist/
tmp/
temp/

database/*
!database/.gitkeep 

log/*
!log/.gitkeep 
*.log

# keep config folder with env files ONLY for demo purposes
# config/
# .env

================================================
FILE: .husky/commit-msg
================================================
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

npx --no-install commitlint --edit "$1"

================================================
FILE: .husky/pre-commit
================================================
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

npm run lint-staged-husky

================================================
FILE: .husky/pre-push
================================================
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

npm run build


================================================
FILE: .lintstagedrc
================================================
{
  "*": [
    "pretty-quick --staged"
  ],
  "*.{js,jsx,ts,tsx}": [
    "eslint --max-warnings 0",
    "tsc-files --noEmit"
  ]
}


================================================
FILE: .prettierignore
================================================
dist

================================================
FILE: .prettierrc
================================================
{
  "printWidth": 120,
  "semi": true,
  "singleQuote": true,
  "trailingComma": "all"
}


================================================
FILE: Dockerfile
================================================
FROM node:16.14.0-alpine

WORKDIR /app

COPY ./package.json .
COPY ./package-lock.json .

RUN npm install && npm cache clean --force

COPY . .

RUN npm run build

EXPOSE 4000

CMD [ "npm", "start" ]

================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2020 mkosir

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
#  TypeORM / Express / TypeScript RESTful API boilerplate

[![CI][build-badge]][build-url]
[![TypeScript][typescript-badge]][typescript-url]
[![prettier][prettier-badge]][prettier-url]
![Heisenberg](misc/heisenberg.png)

Boilerplate with focus on best practices and painless developer experience:

- Minimal setup that can be extended 🔧
- Spin it up with single command 🌀
- TypeScript first
- RESTful APIs
- JWT authentication with role based authorization

## Requirements

- [Node v16+](https://nodejs.org/)
- [Docker](https://www.docker.com/)

## Running

_Easily set up a local development environment with single command!_

- clone the repo
- `npm run docker:dev` 🚀

Visit [localhost:4000](http://localhost:4000/) or if using Postman grab [config](/postman).

### _What happened_ 💥

Containers created:

- Postgres database container seeded with 💊 Breaking Bad characters in `Users` table (default credentials `user=walter`, `password=white` in [.env file](./.env))
- Node (v16 Alpine) container with running boilerplate RESTful API service
- and one Node container instance to run tests locally or in CI

## Features:

- [Express](https://github.com/expressjs/express) framework
- [TypeScript v4](https://github.com/microsoft/TypeScript) codebase
- [TypeORM](https://typeorm.io/) using Data Mapper pattern
- [Docker](https://www.docker.com/) environment:
  - Easily start local development using [Docker Compose](https://docs.docker.com/compose/) with single command `npm run docker:dev`
  - Connect to different staging or production environments `npm run docker:[stage|prod]`
  - Ready for **microservices** development and deployment.  
    Once API changes are made, just build and push new docker image with your favourite CI/CD tool  
    `docker build -t <username>/api-boilerplate:latest .`  
    `docker push <username>/api-boilerplate:latest`
  - Run unit, integration (or setup with your frontend E2E) tests as `docker exec -ti be_boilerplate_test sh` and `npm run test`
- Contract first REST API design:
  - never break API again with HTTP responses and requests payloads using [type definitions](./src/types/express/index.d.ts)
  - Consistent schema error [response](./src/utils/response/custom-error/types.ts). Your frontend will always know how to handle errors thrown in `try...catch` statements 💪
- JWT authentication and role based authorization using custom middleware
- Set local, stage or production [environmental variables](./config) with [type definitions](./src/types/ProcessEnv.d.ts)
- Logging with [morgan](https://github.com/expressjs/morgan)
- Unit and integration tests with [Mocha](https://mochajs.org/) and [Chai](https://www.chaijs.com/)
- Linting with [ESLint](https://eslint.org/)
- [Prettier](https://prettier.io/) code formatter
- Git hooks with [Husky](https://github.com/typicode/husky) and [lint-staged](https://github.com/okonet/lint-staged)
- Automated npm & Docker dependency updates with [Renovate](https://github.com/renovatebot/renovate) (set to patch version only)
- Commit messages must meet [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) format.  
  After staging changes just run `npm run commit` and get instant feedback on your commit message formatting and be prompted for required fields by [Commitizen](https://github.com/commitizen/cz-cli)

## Other awesome boilerplates:

Each boilerplate comes with it's own flavor of libraries and setup, check out others:

- [Express and TypeORM with TypeScript](https://github.com/typeorm/typescript-express-example)
- [Node.js, Express.js & TypeScript Boilerplate for Web Apps](https://github.com/jverhoelen/node-express-typescript-boilerplate)
- [Express boilerplate for building RESTful APIs](https://github.com/danielfsousa/express-rest-es2017-boilerplate)
- [A delightful way to building a RESTful API with NodeJs & TypeScript by @w3tecch](https://github.com/w3tecch/express-typescript-boilerplate)

[build-badge]: https://github.com/mkosir/express-typescript-typeorm-boilerplate/actions/workflows/main.yml/badge.svg
[build-url]: https://github.com/mkosir/express-typescript-typeorm-boilerplate/actions/workflows/main.yml
[typescript-badge]: https://badges.frapsoft.com/typescript/code/typescript.svg?v=101
[typescript-url]: https://github.com/microsoft/TypeScript
[prettier-badge]: https://img.shields.io/badge/code_style-prettier-ff69b4.svg
[prettier-url]: https://github.com/prettier/prettier

## Contributing

All contributions are welcome!


================================================
FILE: config/prod.env
================================================
NODE_ENV=production

PORT=4000

### Database - Postgres
PG_HOST=host.production.com
PG_PORT=5432
POSTGRES_USER=walter
POSTGRES_PASSWORD=white
POSTGRES_DB=production_boilerplate_db

### JWT
JWT_SECRET=4353dhf8gset8h523sfreh6qedn37dfposefsawd56n381jsd
JWT_EXPIRATION=15m

================================================
FILE: config/stage.env
================================================
NODE_ENV=stage

PORT=4000

### Database - Postgres
PG_HOST=host.stage.com
PG_PORT=5432
POSTGRES_USER=walter
POSTGRES_PASSWORD=white
POSTGRES_DB=stage_boilerplate_db

### JWT
JWT_SECRET=ert423dhf8gh523reh6qedn37dfposdgawdn381js25w
JWT_EXPIRATION=15m

================================================
FILE: database/.gitkeep
================================================


================================================
FILE: docker-compose.dev.yml
================================================
version: '3'

services:
  db_postgres:
    container_name: 'db_boilerplate'
    image: 'postgres:14.2-alpine'
    restart: always
    env_file:
      - .env
    ports:
      - '5432:5432'
    volumes:
      - ./database/boilerplate:/var/lib/postgresql/data/

  be_boilerplate:
    entrypoint: /bin/sh './scripts/be-node-dev.sh'
    env_file:
      - .env
    ports:
      - '4000:4000'
    depends_on:
      - db_postgres
    links:
      - db_postgres

  be_boilerplate_test:
    container_name: 'be_boilerplate_test'
    command: sh -c "echo 'Test container ready' && tail -f /dev/null"
    build: .
    stdin_open: true
    tty: true
    depends_on:
      - db_postgres
    links:
      - db_postgres
    env_file:
      - .env
    volumes:
      - .:/app/
      - /app/node_modules


================================================
FILE: docker-compose.prod.yml
================================================
version: '3'

services:
  be_boilerplate:
    deploy:
      resources:
        limits:
          cpus: '0.90'
          # memory: 400M
    command: /bin/sh -c "echo 'Running API on production!' && npm run build && npm start"
    ports:
      - '4000:4000'
    env_file:
      - ./config/prod.env


================================================
FILE: docker-compose.stage.yml
================================================
version: '3'

services:
  be_boilerplate:
    command: /bin/sh -c "echo 'Running API on production!' && npm run build && npm start"
    ports:
      - '4000:4000'
    env_file:
      - ./config/stage.env


================================================
FILE: docker-compose.yml
================================================
version: '3'

services:
  be_boilerplate:
    container_name: 'be_boilerplate'
    build: .
    restart: always
    volumes:
      - .:/app/
      - /app/node_modules


================================================
FILE: log/.gitkeep
================================================


================================================
FILE: package.json
================================================
{
  "name": "express-typescript-typeorm-boilerplate",
  "version": "1.0.0",
  "description": "RESTful API boilerplate with Typescript, TypeORM, ExpressJs & Mocha.",
  "scripts": {
    "dev": "debug=* NODE_PATH=./src ts-node-dev --respawn ./src/index.ts",
    "build": "rimraf dist && tsc",
    "start": "NODE_PATH=./dist node ./dist/index.js",
    "prepare": "husky install",
    "lint": "eslint --max-warnings 0 . && npm run tsc",
    "lint-fix": "eslint --fix .",
    "lint-staged-husky": "lint-staged",
    "tsc": "tsc --noEmit",
    "format-lint": "prettier --config .prettierrc -l --ignore-unknown .",
    "format-fix": "prettier --config .prettierrc --write --ignore-unknown .",
    "test": "NODE_PATH=./src mocha --require ts-node/register 'src/**/*.test.ts' --timeout 20000 --exit",
    "commit": "git-cz",
    "docker:dev": "docker-compose --file docker-compose.yml --file docker-compose.dev.yml --compatibility up --build",
    "docker:stage": "docker-compose --file docker-compose.yml --file docker-compose.stage.yml --compatibility up --build",
    "docker:prod": "docker-compose --file docker-compose.yml --file docker-compose.prod.yml --compatibility up --build",
    "migration:run": "NODE_PATH=./src ts-node ./node_modules/typeorm/cli.js migration:run --config ./src/orm/config/ormconfig.ts",
    "migration:run:dev": "PG_HOST=localhost NODE_PATH=./src ts-node ./node_modules/typeorm/cli.js migration:run --config ./src/orm/config/ormconfig.ts",
    "migration:revert": "PG_HOST=localhost NODE_PATH=./src ts-node ./node_modules/typeorm/cli.js migration:revert --config ./src/orm/config/ormconfig.ts",
    "migration:generate": "PG_HOST=localhost NODE_PATH=./src ts-node ./node_modules/.bin/typeorm migration:generate --pretty --config ./src/orm/config/ormconfig.ts -n ",
    "migration:create": "PG_HOST=localhost NODE_PATH=./src ts-node ./node_modules/.bin/typeorm migration:create --config ./src/orm/config/ormconfig.ts -n ",
    "seed:run": "NODE_PATH=./src ts-node ./node_modules/.bin/typeorm migration:run --config ./src/orm/config/ormconfig-seed.ts",
    "seed:run:dev": "PG_HOST=localhost NODE_PATH=./src ts-node ./node_modules/.bin/typeorm migration:run --config ./src/orm/config/ormconfig-seed.ts",
    "seed:create": "PG_HOST=localhost NODE_PATH=./src ts-node ./node_modules/.bin/typeorm migration:create --config ./src/orm/config/ormconfig-seed.ts -n "
  },
  "dependencies": {
    "bcryptjs": "^2.4.3",
    "cors": "^2.8.5",
    "express": "^4.17.3",
    "helmet": "^5.0.2",
    "jsonwebtoken": "^8.5.1",
    "morgan": "^1.10.0",
    "pg": "^8.7.3",
    "reflect-metadata": "^0.1.13",
    "typeorm": "^0.2.45",
    "typeorm-naming-strategies": "^4.0.0",
    "validator": "^13.7.0"
  },
  "devDependencies": {
    "@commitlint/cli": "^16.2.4",
    "@commitlint/config-conventional": "^16.2.4",
    "@types/bcryptjs": "^2.4.2",
    "@types/body-parser": "^1.19.2",
    "@types/chai": "^4.3.1",
    "@types/cors": "^2.8.12",
    "@types/express": "^4.17.13",
    "@types/jsonwebtoken": "^8.5.8",
    "@types/mocha": "^9.1.1",
    "@types/morgan": "^1.9.3",
    "@types/node": "^17.0.33",
    "@types/supertest": "^2.0.12",
    "@types/validator": "^13.7.2",
    "@typescript-eslint/eslint-plugin": "^5.23.0",
    "@typescript-eslint/parser": "^5.23.0",
    "chai": "^4.3.6",
    "commitizen": "^4.2.4",
    "dotenv": "^16.0.1",
    "eslint": "^8.15.0",
    "eslint-config-prettier": "^8.5.0",
    "eslint-plugin-import": "^2.26.0",
    "eslint-plugin-no-array-reduce": "^1.0.58",
    "eslint-plugin-prettier": "^4.0.0",
    "husky": "^8.0.1",
    "lint-staged": "^12.4.1",
    "mocha": "^9.2.2",
    "nyc": "^15.1.0",
    "prettier": "^2.6.2",
    "pretty-quick": "^3.1.3",
    "supertest": "^6.2.2",
    "ts-node": "10.7.0",
    "ts-node-dev": "^1.1.8",
    "tsc-files": "^1.1.3",
    "typescript": "4.6.4"
  }
}


================================================
FILE: postman/RESTful_API_Boilerplate.postman_collection.json
================================================
{
  "info": {
    "_postman_id": "da0fe49f-e424-4b96-8f55-49f7db0583dc",
    "name": "RESTful API Boilerplate",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "item": [
    {
      "name": "/auth",
      "item": [
        {
          "name": "/login",
          "event": [
            {
              "listen": "test",
              "script": {
                "id": "81ebd7f4-cc11-473a-a6b3-85b003f7222b",
                "exec": [
                  "var jsonData = JSON.parse(responseBody);",
                  "",
                  "postman.setEnvironmentVariable(\"token\", jsonData.data);"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "name": "Content-Type",
                "type": "text",
                "value": "application/x-www-form-urlencoded"
              },
              {
                "key": "Accept-Language",
                "value": "{{language}}",
                "type": "text"
              }
            ],
            "body": {
              "mode": "urlencoded",
              "urlencoded": [
                {
                  "key": "email",
                  "value": "admin@admin.com",
                  "type": "text"
                },
                {
                  "key": "password",
                  "value": "pass1",
                  "type": "text"
                }
              ]
            },
            "url": {
              "raw": "{{baseUrl}}/login",
              "host": ["{{baseUrl}}"],
              "path": ["login"]
            }
          },
          "response": []
        },
        {
          "name": "/register",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "name": "Content-Type",
                "value": "application/x-www-form-urlencoded",
                "type": "text"
              },
              {
                "key": "Accept-Language",
                "value": "{{language}}",
                "type": "text"
              }
            ],
            "body": {
              "mode": "urlencoded",
              "urlencoded": [
                {
                  "key": "email",
                  "value": "admin1@admin.com",
                  "type": "text"
                },
                {
                  "key": "password",
                  "value": "admin",
                  "type": "text"
                },
                {
                  "key": "passwordConfirm",
                  "value": "admin",
                  "type": "text"
                }
              ]
            },
            "url": {
              "raw": "{{baseUrl}}/register",
              "host": ["{{baseUrl}}"],
              "path": ["register"]
            }
          },
          "response": []
        },
        {
          "name": "/change-password",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "name": "Content-Type",
                "type": "text",
                "value": "application/x-www-form-urlencoded"
              },
              {
                "key": "Authorization",
                "value": "{{token}}",
                "type": "text"
              },
              {
                "key": "Accept-Language",
                "value": "{{language}}",
                "type": "text"
              }
            ],
            "body": {
              "mode": "urlencoded",
              "urlencoded": [
                {
                  "key": "password",
                  "value": "admin",
                  "type": "text"
                },
                {
                  "key": "passwordNew",
                  "value": "admin",
                  "type": "text"
                },
                {
                  "key": "passwordConfirm",
                  "value": "admin",
                  "type": "text"
                }
              ]
            },
            "url": {
              "raw": "{{baseUrl}}/change-password",
              "host": ["{{baseUrl}}"],
              "path": ["change-password"]
            }
          },
          "response": []
        }
      ],
      "protocolProfileBehavior": {}
    },
    {
      "name": "/misc",
      "item": [
        {
          "name": "/change-language",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "name": "Content-Type",
                "type": "text",
                "value": "application/x-www-form-urlencoded"
              },
              {
                "key": "Accept-Language",
                "type": "text",
                "value": "sl-SI"
              }
            ],
            "body": {
              "mode": "urlencoded",
              "urlencoded": [
                {
                  "key": "language",
                  "value": "sl-SI",
                  "type": "text"
                }
              ]
            },
            "url": {
              "raw": "{{baseUrl}}/misc/change-language",
              "host": ["{{baseUrl}}"],
              "path": ["misc", "change-language"]
            }
          },
          "response": []
        }
      ],
      "protocolProfileBehavior": {}
    },
    {
      "name": "/users",
      "item": [
        {
          "name": "/",
          "protocolProfileBehavior": {
            "disableBodyPruning": true
          },
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Content-Type",
                "name": "Content-Type",
                "type": "text",
                "value": "application/x-www-form-urlencoded"
              },
              {
                "key": "Accept-Language",
                "type": "text",
                "value": "sl-SI"
              },
              {
                "key": "Authorization",
                "value": "{{token}}",
                "type": "text"
              }
            ],
            "body": {
              "mode": "urlencoded",
              "urlencoded": []
            },
            "url": {
              "raw": "{{baseUrl}}/users",
              "host": ["{{baseUrl}}"],
              "path": ["users"]
            }
          },
          "response": []
        },
        {
          "name": "/:id",
          "protocolProfileBehavior": {
            "disableBodyPruning": true
          },
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Content-Type",
                "name": "Content-Type",
                "type": "text",
                "value": "application/x-www-form-urlencoded"
              },
              {
                "key": "Accept-Language",
                "type": "text",
                "value": "sl-SI"
              },
              {
                "key": "Authorization",
                "type": "text",
                "value": "{{token}}"
              }
            ],
            "body": {
              "mode": "urlencoded",
              "urlencoded": []
            },
            "url": {
              "raw": "{{baseUrl}}/users/3",
              "host": ["{{baseUrl}}"],
              "path": ["users", "3"]
            }
          },
          "response": []
        },
        {
          "name": "/:id",
          "request": {
            "method": "DELETE",
            "header": [
              {
                "key": "Content-Type",
                "name": "Content-Type",
                "type": "text",
                "value": "application/x-www-form-urlencoded"
              },
              {
                "key": "Accept-Language",
                "type": "text",
                "value": "sl-SI"
              },
              {
                "key": "Authorization",
                "type": "text",
                "value": "{{token}}"
              }
            ],
            "body": {
              "mode": "urlencoded",
              "urlencoded": []
            },
            "url": {
              "raw": "{{baseUrl}}/users/9",
              "host": ["{{baseUrl}}"],
              "path": ["users", "9"]
            }
          },
          "response": []
        },
        {
          "name": "/:id",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "name": "Content-Type",
                "type": "text",
                "value": "application/x-www-form-urlencoded"
              },
              {
                "key": "Accept-Language",
                "type": "text",
                "value": "sl-SI"
              },
              {
                "key": "Authorization",
                "type": "text",
                "value": "{{token}}"
              }
            ],
            "body": {
              "mode": "urlencoded",
              "urlencoded": [
                {
                  "key": "username",
                  "value": "Tyrion1",
                  "type": "text"
                },
                {
                  "key": "name",
                  "value": "test name",
                  "type": "text"
                }
              ]
            },
            "url": {
              "raw": "{{baseUrl}}/users/5",
              "host": ["{{baseUrl}}"],
              "path": ["users", "5"]
            }
          },
          "response": []
        }
      ],
      "protocolProfileBehavior": {}
    }
  ],
  "protocolProfileBehavior": {}
}


================================================
FILE: postman/RESTful_API_Boilerplate.postman_environment.json
================================================
{
  "id": "a8df1c58-9c26-4452-a61e-0f0d2e865ee1",
  "name": "RESTful API Boilerplate",
  "values": [
    {
      "key": "host",
      "value": "http://localhost:4000",
      "enabled": true
    },
    {
      "key": "baseUrl",
      "value": "{{host}}",
      "enabled": true
    },
    {
      "key": "language",
      "value": "en",
      "enabled": true
    },
    {
      "key": "token",
      "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwibmFtZSI6IldhbHRlciBXaGl0ZSIsImVtYWlsIjoiYWRtaW5AYWRtaW4uY29tIiwicm9sZSI6IkFETUlOSVNUUkFUT1IiLCJjcmVhdGVkX2F0IjoiMjAyMC0wNi0xM1QwODoyNjoxNS41MjZaIiwiaWF0IjoxNTkyMTYxMjcwLCJleHAiOjE1OTIxNjIxNzB9.Fi-jszKGqt2plCqJj9jCM5x5l0KSQVkuQu9-qqElMug",
      "enabled": true
    }
  ],
  "_postman_variable_scope": "environment",
  "_postman_exported_at": "2020-06-14T19:04:35.409Z",
  "_postman_exported_using": "Postman/7.26.0"
}


================================================
FILE: renovate.json
================================================
{
  "extends": ["config:base", ":disableDependencyDashboard"],
  "separateMinorPatch": true,
  "assignees": ["mkosir"],
  "assignAutomerge": true,
  "requiredStatusChecks": null,
  "rangeStrategy": "bump",
  "enabled": false,
  "packageRules": [
    {
      "matchUpdateTypes": ["minor", "major"],
      "enabled": false
    },
    {
      "automerge": true,
      "labels": ["automerge", "dependencies", "patch"],
      "groupName": "group:dependencies",
      "matchDepTypes": ["dependencies"]
    },
    {
      "automerge": true,
      "labels": ["automerge", "devDependencies", "patch"],
      "groupName": "group:devDependencies",
      "matchDepTypes": ["devDependencies"]
    }
  ]
}


================================================
FILE: scripts/be-node-dev.sh
================================================
#!/bin/sh

echo "Install bash and execute 'wait-for-it.sh' script"
apk add --update bash
./scripts/wait-for-it.sh $PG_HOST:5432 --timeout=30 --strict -- echo "postgres up and running"

npm run migration:run
npm run seed:run
npm run dev

================================================
FILE: scripts/wait-for-it.sh
================================================
#!/usr/bin/env bash

WAITFORIT_cmdname=${0##*/}

echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi }

usage()
{
    cat << USAGE >&2
Usage:
    $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args]
    -h HOST | --host=HOST       Host or IP under test
    -p PORT | --port=PORT       TCP port under test
                                Alternatively, you specify the host and port as host:port
    -s | --strict               Only execute subcommand if the test succeeds
    -q | --quiet                Don't output any status messages
    -t TIMEOUT | --timeout=TIMEOUT
                                Timeout in seconds, zero for no timeout
    -- COMMAND ARGS             Execute command with args after the test finishes
USAGE
    exit 1
}

wait_for()
{
    if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then
        echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT"
    else
        echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout"
    fi
    WAITFORIT_start_ts=$(date +%s)
    while :
    do
        if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then
            nc -z $WAITFORIT_HOST $WAITFORIT_PORT
            WAITFORIT_result=$?
        else
            (echo > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1
            WAITFORIT_result=$?
        fi
        if [[ $WAITFORIT_result -eq 0 ]]; then
            WAITFORIT_end_ts=$(date +%s)
            echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds"
            break
        fi
        sleep 1
    done
    return $WAITFORIT_result
}

wait_for_wrapper()
{
    if [[ $WAITFORIT_QUIET -eq 1 ]]; then
        timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT &
    else
        timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT &
    fi
    WAITFORIT_PID=$!
    trap "kill -INT -$WAITFORIT_PID" INT
    wait $WAITFORIT_PID
    WAITFORIT_RESULT=$?
    if [[ $WAITFORIT_RESULT -ne 0 ]]; then
        echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT"
    fi
    return $WAITFORIT_RESULT
}

# process arguments
while [[ $# -gt 0 ]]
do
    case "$1" in
        *:* )
        WAITFORIT_hostport=(${1//:/ })
        WAITFORIT_HOST=${WAITFORIT_hostport[0]}
        WAITFORIT_PORT=${WAITFORIT_hostport[1]}
        shift 1
        ;;
        --child)
        WAITFORIT_CHILD=1
        shift 1
        ;;
        -q | --quiet)
        WAITFORIT_QUIET=1
        shift 1
        ;;
        -s | --strict)
        WAITFORIT_STRICT=1
        shift 1
        ;;
        -h)
        WAITFORIT_HOST="$2"
        if [[ $WAITFORIT_HOST == "" ]]; then break; fi
        shift 2
        ;;
        --host=*)
        WAITFORIT_HOST="${1#*=}"
        shift 1
        ;;
        -p)
        WAITFORIT_PORT="$2"
        if [[ $WAITFORIT_PORT == "" ]]; then break; fi
        shift 2
        ;;
        --port=*)
        WAITFORIT_PORT="${1#*=}"
        shift 1
        ;;
        -t)
        WAITFORIT_TIMEOUT="$2"
        if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi
        shift 2
        ;;
        --timeout=*)
        WAITFORIT_TIMEOUT="${1#*=}"
        shift 1
        ;;
        --)
        shift
        WAITFORIT_CLI=("$@")
        break
        ;;
        --help)
        usage
        ;;
        *)
        echoerr "Unknown argument: $1"
        usage
        ;;
    esac
done

if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then
    echoerr "Error: you need to provide a host and port to test."
    usage
fi

WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15}
WAITFORIT_STRICT=${WAITFORIT_STRICT:-0}
WAITFORIT_CHILD=${WAITFORIT_CHILD:-0}
WAITFORIT_QUIET=${WAITFORIT_QUIET:-0}

# Check to see if timeout is from busybox?
WAITFORIT_TIMEOUT_PATH=$(type -p timeout)
WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH)

WAITFORIT_BUSYTIMEFLAG=""
if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then
    WAITFORIT_ISBUSY=1
    # Check if busybox timeout uses -t flag
    # (recent Alpine versions don't support -t anymore)
    if timeout &>/dev/stdout | grep -q -e '-t '; then
        WAITFORIT_BUSYTIMEFLAG="-t"
    fi
else
    WAITFORIT_ISBUSY=0
fi

if [[ $WAITFORIT_CHILD -gt 0 ]]; then
    wait_for
    WAITFORIT_RESULT=$?
    exit $WAITFORIT_RESULT
else
    if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then
        wait_for_wrapper
        WAITFORIT_RESULT=$?
    else
        wait_for
        WAITFORIT_RESULT=$?
    fi
fi

if [[ $WAITFORIT_CLI != "" ]]; then
    if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then
        echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess"
        exit $WAITFORIT_RESULT
    fi
    exec "${WAITFORIT_CLI[@]}"
else
    exit $WAITFORIT_RESULT
fi

================================================
FILE: src/consts/ConstsUser.ts
================================================
export enum ConstsUser {
  PASSWORD_MIN_CHAR = 4,
}


================================================
FILE: src/controllers/auth/changePassword.ts
================================================
import { Request, Response, NextFunction } from 'express';
import { getRepository } from 'typeorm';

import { User } from 'orm/entities/users/User';
import { CustomError } from 'utils/response/custom-error/CustomError';

export const changePassword = async (req: Request, res: Response, next: NextFunction) => {
  const { password, passwordNew } = req.body;
  const { id, name } = req.jwtPayload;

  const userRepository = getRepository(User);
  try {
    const user = await userRepository.findOne({ where: { id } });

    if (!user) {
      const customError = new CustomError(404, 'General', 'Not Found', [`User ${name} not found.`]);
      return next(customError);
    }

    if (!user.checkIfPasswordMatch(password)) {
      const customError = new CustomError(400, 'General', 'Not Found', ['Incorrect password']);
      return next(customError);
    }

    user.password = passwordNew;
    user.hashPassword();
    userRepository.save(user);

    res.customSuccess(200, 'Password successfully changed.');
  } catch (err) {
    const customError = new CustomError(400, 'Raw', 'Error', null, err);
    return next(customError);
  }
};


================================================
FILE: src/controllers/auth/index.ts
================================================
export * from './changePassword';
export * from './login';
export * from './register';


================================================
FILE: src/controllers/auth/login.test.ts
================================================
import 'mocha';
import { expect } from 'chai';
import { agent as request } from 'supertest';
import { getRepository, Connection, Repository } from 'typeorm';

import { dbCreateConnection } from 'orm/dbCreateConnection';
import { Role } from 'orm/entities/users/types';
import { User } from 'orm/entities/users/User';

import { app } from '../../';

describe('Login', () => {
  let dbConnection: Connection;
  let userRepository: Repository<User>;

  const userPassword = 'pass1';
  const user = new User();
  user.username = 'Badger';
  user.name = 'Brandon Mayhew';
  user.email = 'brandon.mayhew@test.com';
  user.password = userPassword;
  user.hashPassword();
  user.role = 'ADMINISTRATOR' as Role;

  before(async () => {
    dbConnection = await dbCreateConnection();
    userRepository = getRepository(User);
  });

  beforeEach(async () => {
    await userRepository.save(user);
  });

  afterEach(async () => {
    await userRepository.delete(user.id);
  });

  it('should return a JWT token', async () => {
    const res = await request(app).post('/v1/auth/login').send({ email: user.email, password: userPassword });
    expect(res.status).to.equal(200);
    expect(res.body.message).to.equal('Token successfully created.');
    expect(res.body.data).not.to.be.empty;
    expect(res.body.data).to.be.an('string');
  });

  it("should report error when email and password don't match", async () => {
    const res = await request(app).post('/v1/auth/login').send({ email: user.email, password: 'wrong_password' });
    expect(res.status).to.equal(404);
    expect(res.body.errorType).to.equal('General');
    expect(res.body.errors).to.eql(['Incorrect email or password']);
    expect(res.body.errorRaw).to.an('null');
    expect(res.body.errorsValidation).to.an('null');
  });

  it('should report error when the email provided is not valid', async () => {
    const res = await request(app).post('/v1/auth/login').send({ email: 'not_valid_email', password: userPassword });
    expect(res.status).to.equal(400);
    expect(res.body.errorType).to.equal('Validation');
    expect(res.body.errorMessage).to.equal('Login validation error');
    expect(res.body.errors).to.an('null');
    expect(res.body.errorRaw).to.an('null');
    expect(res.body.errorsValidation).to.eql([
      {
        email: 'Email is invalid',
      },
    ]);
  });
});


================================================
FILE: src/controllers/auth/login.ts
================================================
import { Request, Response, NextFunction } from 'express';
import { getRepository } from 'typeorm';

import { Role } from 'orm/entities/users/types';
import { User } from 'orm/entities/users/User';
import { JwtPayload } from 'types/JwtPayload';
import { createJwtToken } from 'utils/createJwtToken';
import { CustomError } from 'utils/response/custom-error/CustomError';

export const login = async (req: Request, res: Response, next: NextFunction) => {
  const { email, password } = req.body;

  const userRepository = getRepository(User);
  try {
    const user = await userRepository.findOne({ where: { email } });

    if (!user) {
      const customError = new CustomError(404, 'General', 'Not Found', ['Incorrect email or password']);
      return next(customError);
    }

    if (!user.checkIfPasswordMatch(password)) {
      const customError = new CustomError(404, 'General', 'Not Found', ['Incorrect email or password']);
      return next(customError);
    }

    const jwtPayload: JwtPayload = {
      id: user.id,
      name: user.name,
      email: user.email,
      role: user.role as Role,
      created_at: user.created_at,
    };

    try {
      const token = createJwtToken(jwtPayload);
      res.customSuccess(200, 'Token successfully created.', `Bearer ${token}`);
    } catch (err) {
      const customError = new CustomError(400, 'Raw', "Token can't be created", null, err);
      return next(customError);
    }
  } catch (err) {
    const customError = new CustomError(400, 'Raw', 'Error', null, err);
    return next(customError);
  }
};


================================================
FILE: src/controllers/auth/register.test.ts
================================================
import 'mocha';
import { expect } from 'chai';
import { agent as request } from 'supertest';
import { getRepository, Connection, Repository } from 'typeorm';

import { dbCreateConnection } from 'orm/dbCreateConnection';
import { User } from 'orm/entities/users/User';

import { app } from '../../';

describe('Register', () => {
  let dbConnection: Connection;
  let userRepository: Repository<User>;

  const userPassword = 'pass1';
  const user = new User();
  user.email = 'brandon.mayhew@test.com';
  user.password = userPassword;
  user.hashPassword();

  before(async () => {
    dbConnection = await dbCreateConnection();
    userRepository = getRepository(User);
  });

  it('should register a new user', async () => {
    const res = await request(app)
      .post('/v1/auth/register')
      .send({ email: user.email, password: userPassword, passwordConfirm: userPassword });
    expect(res.status).to.equal(200);
    expect(res.body.message).to.equal('User successfully created.');
    expect(res.body.data).to.be.an('null');
    await userRepository.delete({ email: user.email });
  });

  it('should report error when email already exists', async () => {
    let res = await request(app)
      .post('/v1/auth/register')
      .send({ email: user.email, password: userPassword, passwordConfirm: userPassword });
    res = await request(app)
      .post('/v1/auth/register')
      .send({ email: user.email, password: userPassword, passwordConfirm: userPassword });
    expect(res.status).to.equal(400);
    expect(res.body.errorType).to.equal('General');
    expect(res.body.errorMessage).to.equal('User already exists');
    expect(res.body.errors).to.eql([`Email '${user.email}' already exists`]);
    expect(res.body.errorRaw).to.an('null');
    expect(res.body.errorsValidation).to.an('null');
    await userRepository.delete({ email: user.email });
  });
});


================================================
FILE: src/controllers/auth/register.ts
================================================
import { Request, Response, NextFunction } from 'express';
import { getRepository } from 'typeorm';

import { User } from 'orm/entities/users/User';
import { CustomError } from 'utils/response/custom-error/CustomError';

export const register = async (req: Request, res: Response, next: NextFunction) => {
  const { email, password } = req.body;

  const userRepository = getRepository(User);
  try {
    const user = await userRepository.findOne({ where: { email } });

    if (user) {
      const customError = new CustomError(400, 'General', 'User already exists', [
        `Email '${user.email}' already exists`,
      ]);
      return next(customError);
    }

    try {
      const newUser = new User();
      newUser.email = email;
      newUser.password = password;
      newUser.hashPassword();
      await userRepository.save(newUser);

      res.customSuccess(200, 'User successfully created.');
    } catch (err) {
      const customError = new CustomError(400, 'Raw', `User '${email}' can't be created`, null, err);
      return next(customError);
    }
  } catch (err) {
    const customError = new CustomError(400, 'Raw', 'Error', null, err);
    return next(customError);
  }
};


================================================
FILE: src/controllers/users/destroy.ts
================================================
import { Request, Response, NextFunction } from 'express';
import { getRepository } from 'typeorm';

import { User } from 'orm/entities/users/User';
import { CustomError } from 'utils/response/custom-error/CustomError';

export const destroy = async (req: Request, res: Response, next: NextFunction) => {
  const id = req.params.id;

  const userRepository = getRepository(User);
  try {
    const user = await userRepository.findOne({ where: { id } });

    if (!user) {
      const customError = new CustomError(404, 'General', 'Not Found', [`User with id:${id} doesn't exists.`]);
      return next(customError);
    }
    userRepository.delete(id);

    res.customSuccess(200, 'User successfully deleted.', { id: user.id, name: user.name, email: user.email });
  } catch (err) {
    const customError = new CustomError(400, 'Raw', 'Error', null, err);
    return next(customError);
  }
};


================================================
FILE: src/controllers/users/edit.ts
================================================
import { Request, Response, NextFunction } from 'express';
import { getRepository } from 'typeorm';

import { User } from 'orm/entities/users/User';
import { CustomError } from 'utils/response/custom-error/CustomError';

export const edit = async (req: Request, res: Response, next: NextFunction) => {
  const id = req.params.id;
  const { username, name } = req.body;

  const userRepository = getRepository(User);
  try {
    const user = await userRepository.findOne({ where: { id } });

    if (!user) {
      const customError = new CustomError(404, 'General', `User with id:${id} not found.`, ['User not found.']);
      return next(customError);
    }

    user.username = username;
    user.name = name;

    try {
      await userRepository.save(user);
      res.customSuccess(200, 'User successfully saved.');
    } catch (err) {
      const customError = new CustomError(409, 'Raw', `User '${user.email}' can't be saved.`, null, err);
      return next(customError);
    }
  } catch (err) {
    const customError = new CustomError(400, 'Raw', 'Error', null, err);
    return next(customError);
  }
};


================================================
FILE: src/controllers/users/index.test.ts
================================================
import 'mocha';
import { expect } from 'chai';
import { agent as request } from 'supertest';
import { getRepository, Connection, Repository } from 'typeorm';

import { dbCreateConnection } from 'orm/dbCreateConnection';
import { Role } from 'orm/entities/users/types';
import { User } from 'orm/entities/users/User';

import { app } from '../../';

describe('Users', () => {
  let dbConnection: Connection;
  let userRepository: Repository<User>;

  const userPassword = 'pass1';
  let adminUserToken = null;
  const adminUser = new User();
  adminUser.username = 'Badger';
  adminUser.name = 'Brandon Mayhew';
  adminUser.email = 'brandon.mayhew@test.com';
  adminUser.password = userPassword;
  adminUser.hashPassword();
  adminUser.role = 'ADMINISTRATOR' as Role;

  let standardUserToken = null;
  const standardUser = new User();
  standardUser.username = 'Toddy';
  standardUser.name = 'Todd Alquist';
  standardUser.email = 'todd.alquist@test.com';
  standardUser.password = userPassword;
  standardUser.hashPassword();
  standardUser.role = 'STANDARD' as Role;

  before(async () => {
    dbConnection = await dbCreateConnection();
    userRepository = getRepository(User);
  });

  beforeEach(async () => {
    await userRepository.save([adminUser, standardUser]);
    let res = await request(app).post('/v1/auth/login').send({ email: adminUser.email, password: userPassword });
    adminUserToken = res.body.data;
    res = await request(app).post('/v1/auth/login').send({ email: standardUser.email, password: userPassword });
    standardUserToken = res.body.data;
  });

  afterEach(async () => {
    await userRepository.delete([adminUser.id, standardUser.id]);
  });

  describe('GET /v1/auth/users', () => {
    it('should get all users', async () => {
      const res = await request(app).get('/v1/users').set('Authorization', adminUserToken);
      expect(res.status).to.equal(200);
      expect(res.body.message).to.equal('List of users.');
      expect(res.body.data[3].email).to.eql('hank.schrader@test.com');
    });

    it('should report error of unauthorized user', async () => {
      const res = await request(app).get('/v1/users').set('Authorization', standardUserToken);
      expect(res.status).to.equal(401);
      expect(res.body.errorType).to.equal('Unauthorized');
      expect(res.body.errorMessage).to.equal('Unauthorized - Insufficient user rights');
      expect(res.body.errors).to.eql([
        'Unauthorized - Insufficient user rights',
        'Current role: STANDARD. Required role: ADMINISTRATOR',
      ]);
      expect(res.body.errorRaw).to.an('null');
      expect(res.body.errorsValidation).to.an('null');
    });
  });

  describe('GET /v1/auth/users//:id([0-9]+)', () => {
    it('should get user', async () => {
      const user = await userRepository.findOne({ email: adminUser.email });
      const res = await request(app).get(`/v1/users/${user.id}`).set('Authorization', adminUserToken);
      expect(res.status).to.equal(200);
      expect(res.body.message).to.equal('User found');
      expect(res.body.data.email).to.eql(adminUser.email);
    });
  });
});


================================================
FILE: src/controllers/users/index.ts
================================================
export * from './destroy';
export * from './edit';
export * from './list';
export * from './show';


================================================
FILE: src/controllers/users/list.ts
================================================
import { Request, Response, NextFunction } from 'express';
import { getRepository } from 'typeorm';

import { User } from 'orm/entities/users/User';
import { CustomError } from 'utils/response/custom-error/CustomError';

export const list = async (req: Request, res: Response, next: NextFunction) => {
  const userRepository = getRepository(User);
  try {
    const users = await userRepository.find({
      select: ['id', 'username', 'name', 'email', 'role', 'language', 'created_at', 'updated_at'],
    });
    res.customSuccess(200, 'List of users.', users);
  } catch (err) {
    const customError = new CustomError(400, 'Raw', `Can't retrieve list of users.`, null, err);
    return next(customError);
  }
};


================================================
FILE: src/controllers/users/show.ts
================================================
import { Request, Response, NextFunction } from 'express';
import { getRepository } from 'typeorm';

import { User } from 'orm/entities/users/User';
import { CustomError } from 'utils/response/custom-error/CustomError';

export const show = async (req: Request, res: Response, next: NextFunction) => {
  const id = req.params.id;

  const userRepository = getRepository(User);
  try {
    const user = await userRepository.findOne(id, {
      select: ['id', 'username', 'name', 'email', 'role', 'language', 'created_at', 'updated_at'],
    });

    if (!user) {
      const customError = new CustomError(404, 'General', `User with id:${id} not found.`, ['User not found.']);
      return next(customError);
    }
    res.customSuccess(200, 'User found', user);
  } catch (err) {
    const customError = new CustomError(400, 'Raw', 'Error', null, err);
    return next(customError);
  }
};


================================================
FILE: src/index.ts
================================================
import 'dotenv/config';
import 'reflect-metadata';
import fs from 'fs';
import path from 'path';

import bodyParser from 'body-parser';
import cors from 'cors';
import express from 'express';
import helmet from 'helmet';
import morgan from 'morgan';

import './utils/response/customSuccess';
import { errorHandler } from './middleware/errorHandler';
import { getLanguage } from './middleware/getLanguage';
import { dbCreateConnection } from './orm/dbCreateConnection';
import routes from './routes';

export const app = express();
app.use(cors());
app.use(helmet());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(getLanguage);

try {
  const accessLogStream = fs.createWriteStream(path.join(__dirname, '../log/access.log'), {
    flags: 'a',
  });
  app.use(morgan('combined', { stream: accessLogStream }));
} catch (err) {
  console.log(err);
}
app.use(morgan('combined'));

app.use('/', routes);

app.use(errorHandler);

const port = process.env.PORT || 4000;
app.listen(port, () => {
  console.log(`Server running on port ${port}`);
});

(async () => {
  await dbCreateConnection();
})();


================================================
FILE: src/middleware/checkJwt.ts
================================================
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';

import { JwtPayload } from '../types/JwtPayload';
import { createJwtToken } from '../utils/createJwtToken';
import { CustomError } from '../utils/response/custom-error/CustomError';

export const checkJwt = (req: Request, res: Response, next: NextFunction) => {
  const authHeader = req.get('Authorization');
  if (!authHeader) {
    const customError = new CustomError(400, 'General', 'Authorization header not provided');
    return next(customError);
  }

  const token = authHeader.split(' ')[1];
  let jwtPayload: { [key: string]: any };
  try {
    jwtPayload = jwt.verify(token, process.env.JWT_SECRET as string) as { [key: string]: any };
    ['iat', 'exp'].forEach((keyToRemove) => delete jwtPayload[keyToRemove]);
    req.jwtPayload = jwtPayload as JwtPayload;
  } catch (err) {
    const customError = new CustomError(401, 'Raw', 'JWT error', null, err);
    return next(customError);
  }

  try {
    // Refresh and send a new token on every request
    const newToken = createJwtToken(jwtPayload as JwtPayload);
    res.setHeader('token', `Bearer ${newToken}`);
    return next();
  } catch (err) {
    const customError = new CustomError(400, 'Raw', "Token can't be created", null, err);
    return next(customError);
  }
};


================================================
FILE: src/middleware/checkRole.ts
================================================
import { Request, Response, NextFunction } from 'express';

import { Role } from '../orm/entities/users/types';
import { CustomError } from '../utils/response/custom-error/CustomError';

export const checkRole = (roles: Role[], isSelfAllowed = false) => {
  return async (req: Request, res: Response, next: NextFunction) => {
    const { id, role } = req.jwtPayload;
    const { id: requestId } = req.params;

    let errorSelfAllowed: string | null = null;
    if (isSelfAllowed) {
      if (id === parseInt(requestId)) {
        return next();
      }
      errorSelfAllowed = 'Self allowed action.';
    }

    if (roles.indexOf(role) === -1) {
      const errors = [
        'Unauthorized - Insufficient user rights',
        `Current role: ${role}. Required role: ${roles.toString()}`,
      ];
      if (errorSelfAllowed) {
        errors.push(errorSelfAllowed);
      }
      const customError = new CustomError(401, 'Unauthorized', 'Unauthorized - Insufficient user rights', errors);
      return next(customError);
    }
    return next();
  };
};


================================================
FILE: src/middleware/errorHandler.ts
================================================
import { Request, Response, NextFunction } from 'express';

import { CustomError } from '../utils/response/custom-error/CustomError';

export const errorHandler = (err: CustomError, req: Request, res: Response, next: NextFunction) => {
  return res.status(err.HttpStatusCode).json(err.JSON);
};


================================================
FILE: src/middleware/getLanguage.ts
================================================
import { Request, Response, NextFunction } from 'express';

import { Language } from '../orm/entities/users/types';

export const getLanguage = (req: Request, res: Response, next: NextFunction) => {
  const acceptLanguageHeader = req.get('Accept-Language') as Language | null;
  if (!acceptLanguageHeader) {
    req.language = 'en-US';
    return next();
  }
  req.language = acceptLanguageHeader;
  return next();
};


================================================
FILE: src/middleware/validation/auth/index.ts
================================================
export * from './validatorChangePassword';
export * from './validatorLogin';
export * from './validatorRegister';


================================================
FILE: src/middleware/validation/auth/validatorChangePassword.ts
================================================
import { Request, Response, NextFunction } from 'express';
import validator from 'validator';

import { ConstsUser } from 'consts/ConstsUser';
import { CustomError } from 'utils/response/custom-error/CustomError';
import { ErrorValidation } from 'utils/response/custom-error/types';

export const validatorChangePassword = (req: Request, res: Response, next: NextFunction) => {
  let { password, passwordNew, passwordConfirm } = req.body;
  const errorsValidation: ErrorValidation[] = [];

  password = !password ? '' : password;
  passwordNew = !passwordNew ? '' : passwordNew;
  passwordConfirm = !passwordConfirm ? '' : passwordConfirm;

  if (validator.isEmpty(password)) {
    errorsValidation.push({ password: 'Password is required' });
  }

  if (validator.isEmpty(passwordNew)) {
    errorsValidation.push({ passwordNew: 'New password is required' });
  }

  if (validator.isEmpty(passwordConfirm)) {
    errorsValidation.push({ passwordConfirm: 'Password confirm is required' });
  }

  if (!validator.isLength(passwordNew, { min: ConstsUser.PASSWORD_MIN_CHAR })) {
    errorsValidation.push({
      passwordNew: `Password must be at least ${ConstsUser.PASSWORD_MIN_CHAR} characters`,
    });
  }

  if (!validator.equals(passwordNew, passwordConfirm)) {
    errorsValidation.push({ passwordConfirm: 'Passwords must match' });
  }

  if (errorsValidation.length !== 0) {
    const customError = new CustomError(
      400,
      'Validation',
      'Change password validation error',
      null,
      null,
      errorsValidation,
    );
    return next(customError);
  }
  return next();
};


================================================
FILE: src/middleware/validation/auth/validatorLogin.ts
================================================
import { Request, Response, NextFunction } from 'express';
import validator from 'validator';

import { CustomError } from 'utils/response/custom-error/CustomError';
import { ErrorValidation } from 'utils/response/custom-error/types';

export const validatorLogin = (req: Request, res: Response, next: NextFunction) => {
  let { email, password } = req.body;
  const errorsValidation: ErrorValidation[] = [];

  email = !email ? '' : email;
  password = !password ? '' : password;

  if (!validator.isEmail(email)) {
    errorsValidation.push({ email: 'Email is invalid' });
  }

  if (validator.isEmpty(email)) {
    errorsValidation.push({ email: 'Email field is required' });
  }

  if (validator.isEmpty(password)) {
    errorsValidation.push({ password: 'Password field is required' });
  }

  if (errorsValidation.length !== 0) {
    const customError = new CustomError(400, 'Validation', 'Login validation error', null, null, errorsValidation);
    return next(customError);
  }
  return next();
};


================================================
FILE: src/middleware/validation/auth/validatorRegister.ts
================================================
import { Request, Response, NextFunction } from 'express';
import validator from 'validator';

import { ConstsUser } from 'consts/ConstsUser';
import { CustomError } from 'utils/response/custom-error/CustomError';
import { ErrorValidation } from 'utils/response/custom-error/types';

export const validatorRegister = (req: Request, res: Response, next: NextFunction) => {
  let { email, password, passwordConfirm } = req.body;
  const errorsValidation: ErrorValidation[] = [];

  email = !email ? '' : email;
  password = !password ? '' : password;
  passwordConfirm = !passwordConfirm ? '' : passwordConfirm;

  if (!validator.isEmail(email)) {
    errorsValidation.push({ email: 'Email is invalid' });
  }

  if (validator.isEmpty(email)) {
    errorsValidation.push({ email: 'Email is required' });
  }

  if (validator.isEmpty(password)) {
    errorsValidation.push({ password: 'Password is required' });
  }

  if (!validator.isLength(password, { min: ConstsUser.PASSWORD_MIN_CHAR })) {
    errorsValidation.push({
      password: `Password must be at least ${ConstsUser.PASSWORD_MIN_CHAR} characters`,
    });
  }

  if (validator.isEmpty(passwordConfirm)) {
    errorsValidation.push({ passwordConfirm: 'Confirm password is required' });
  }

  if (!validator.equals(password, passwordConfirm)) {
    errorsValidation.push({ passwordConfirm: 'Passwords must match' });
  }

  if (errorsValidation.length !== 0) {
    const customError = new CustomError(400, 'Validation', 'Register validation error', null, null, errorsValidation);
    return next(customError);
  }
  return next();
};


================================================
FILE: src/middleware/validation/users/index.ts
================================================
export * from './validatorEdit';


================================================
FILE: src/middleware/validation/users/validatorEdit.ts
================================================
import { Request, Response, NextFunction } from 'express';
import { getRepository } from 'typeorm';

import { User } from 'orm/entities/users/User';
import { CustomError } from 'utils/response/custom-error/CustomError';
import { ErrorValidation } from 'utils/response/custom-error/types';

export const validatorEdit = async (req: Request, res: Response, next: NextFunction) => {
  let { username, name } = req.body;
  const errorsValidation: ErrorValidation[] = [];
  const userRepository = getRepository(User);

  username = !username ? '' : username;
  name = !name ? '' : name;

  const user = await userRepository.findOne({ username });
  if (user) {
    errorsValidation.push({ username: `Username '${username}' already exists` });
  }

  if (errorsValidation.length !== 0) {
    const customError = new CustomError(400, 'Validation', 'Edit user validation error', null, null, errorsValidation);
    return next(customError);
  }
  return next();
};


================================================
FILE: src/orm/config/ormconfig-seed.ts
================================================
import { ConnectionOptions } from 'typeorm';
import { SnakeNamingStrategy } from 'typeorm-naming-strategies';

const configSeed: ConnectionOptions = {
  type: 'postgres',
  host: process.env.PG_HOST,
  port: Number(process.env.PG_PORT),
  username: process.env.POSTGRES_USER,
  password: process.env.POSTGRES_PASSWORD,
  database: process.env.POSTGRES_DB,
  synchronize: false,
  logging: false,
  entities: ['src/orm/entities/**/*.ts'],
  migrations: ['src/orm/seeds/**/*.ts'],
  cli: {
    migrationsDir: 'src/orm/seeds',
  },
  namingStrategy: new SnakeNamingStrategy(),
};

export = configSeed;


================================================
FILE: src/orm/config/ormconfig.ts
================================================
import { ConnectionOptions } from 'typeorm';
import { SnakeNamingStrategy } from 'typeorm-naming-strategies';

const config: ConnectionOptions = {
  type: 'postgres',
  name: 'default',
  host: process.env.PG_HOST,
  port: Number(process.env.PG_PORT),
  username: process.env.POSTGRES_USER,
  password: process.env.POSTGRES_PASSWORD,
  database: process.env.POSTGRES_DB,
  synchronize: false,
  logging: false,
  entities: ['src/orm/entities/**/*.ts'],
  migrations: ['src/orm/migrations/**/*.ts'],
  subscribers: ['src/orm/subscriber/**/*.ts'],
  cli: {
    entitiesDir: 'src/orm/entities',
    migrationsDir: 'src/orm/migrations',
    subscribersDir: 'src/orm/subscriber',
  },
  namingStrategy: new SnakeNamingStrategy(),
};

export = config;


================================================
FILE: src/orm/dbCreateConnection.ts
================================================
import { Connection, createConnection, getConnectionManager } from 'typeorm';

import config from './config/ormconfig';

export const dbCreateConnection = async (): Promise<Connection | null> => {
  try {
    const conn = await createConnection(config);
    console.log(`Database connection success. Connection name: '${conn.name}' Database: '${conn.options.database}'`);
  } catch (err) {
    if (err.name === 'AlreadyHasActiveConnectionError') {
      const activeConnection = getConnectionManager().get(config.name);
      return activeConnection;
    }
    console.log(err);
  }
  return null;
};


================================================
FILE: src/orm/entities/users/User.ts
================================================
import bcrypt from 'bcryptjs';
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';

import { Role, Language } from './types';

@Entity('users')
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({
    unique: true,
  })
  email: string;

  @Column()
  password: string;

  @Column({
    nullable: true,
    unique: true,
  })
  username: string;

  @Column({
    nullable: true,
  })
  name: string;

  @Column({
    default: 'STANDARD' as Role,
    length: 30,
  })
  role: string;

  @Column({
    default: 'en-US' as Language,
    length: 15,
  })
  language: string;

  @Column()
  @CreateDateColumn()
  created_at: Date;

  @Column()
  @UpdateDateColumn()
  updated_at: Date;

  setLanguage(language: Language) {
    this.language = language;
  }

  hashPassword() {
    this.password = bcrypt.hashSync(this.password, 8);
  }

  checkIfPasswordMatch(unencryptedPassword: string) {
    return bcrypt.compareSync(unencryptedPassword, this.password);
  }
}


================================================
FILE: src/orm/entities/users/types.ts
================================================
export type Role = 'ADMINISTRATOR' | 'STANDARD';
export type Language = 'en-US' | 'sl-SI';


================================================
FILE: src/orm/migrations/1590521920166-CreateUsers.ts
================================================
import { MigrationInterface, QueryRunner } from 'typeorm';

export class CreateUsers1590521920166 implements MigrationInterface {
  name = 'CreateUsers1590521920166';

  public async up(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(
      `CREATE TABLE "users" ("id" SERIAL NOT NULL, "username" character varying(40), "name" character varying(40), "email" character varying(100) NOT NULL, "password" character varying NOT NULL, "role" character varying(30) NOT NULL DEFAULT 'STANDARD', "language" character varying(15) NOT NULL DEFAULT 'en-US', "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "UQ_fe0bb3f6520ee0469504521e710" UNIQUE ("username"), CONSTRAINT "UQ_97672ac88f789774dd47f7c8be3" UNIQUE ("email"), CONSTRAINT "PK_a3ffb1c0c8416b9fc6f907b7433" PRIMARY KEY ("id"))`,
      undefined,
    );
  }

  public async down(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(`DROP TABLE "users"`, undefined);
  }
}


================================================
FILE: src/orm/seeds/1590519635401-SeedUsers.ts
================================================
import { MigrationInterface, QueryRunner, getRepository } from 'typeorm';

import { Role } from '../entities/users/types';
import { User } from '../entities/users/User';

export class SeedUsers1590519635401 implements MigrationInterface {
  public async up(queryRunner: QueryRunner): Promise<any> {
    let user = new User();
    const userRepository = getRepository(User);

    user.username = 'Heisenberg';
    user.name = 'Walter White';
    user.email = 'admin@admin.com';
    user.password = 'pass1';
    user.hashPassword();
    user.role = 'ADMINISTRATOR' as Role;
    await userRepository.save(user);

    user = new User();
    user.username = 'Jesse';
    user.name = 'Jesse Pinkman';
    user.email = 'standard@standard.com';
    user.password = 'pass1';
    user.hashPassword();
    user.role = 'STANDARD' as Role;
    await userRepository.save(user);

    user = new User();
    user.username = 'Sky';
    user.name = 'Skyler White';
    user.email = 'skyler.white@test.com';
    user.password = 'pass1';
    user.hashPassword();
    await userRepository.save(user);

    user = new User();
    user.username = 'Hank';
    user.name = 'Hank Schrader';
    user.email = 'hank.schrader@test.com';
    user.password = 'pass1';
    user.hashPassword();
    await userRepository.save(user);

    user = new User();
    user.username = 'Marie';
    user.name = 'Marie Schrader';
    user.email = 'marie.schrader@test.com';
    user.password = 'pass1';
    user.hashPassword();
    await userRepository.save(user);

    user = new User();
    user.username = 'The Lawyer';
    user.name = 'Saul Goodman';
    user.email = 'saul.goodman@test.com';
    user.password = 'pass1';
    user.hashPassword();
    await userRepository.save(user);

    user = new User();
    user.username = 'Gus';
    user.name = 'Gustavo Fring';
    user.email = 'gustavo.fring@test.com';
    user.password = 'pass1';
    user.hashPassword();
    await userRepository.save(user);

    user = new User();
    user.username = 'Mike';
    user.name = 'Michael Ehrmantraut';
    user.email = 'michael.ehrmantraut@test.com';
    user.password = 'pass1';
    user.hashPassword();
    await userRepository.save(user);

    user = new User();
    user.username = 'Tio';
    user.name = 'Hector Salamanca';
    user.email = 'hector.salamanca@test.com';
    user.password = 'pass1';
    user.hashPassword();
    await userRepository.save(user);

    user = new User();
    user.username = 'Tuco';
    user.name = 'Alberto Salamanca';
    user.email = 'alberto.salamanca@test.com';
    user.password = 'pass1';
    user.hashPassword();
    await userRepository.save(user);
  }

  public async down(queryRunner: QueryRunner): Promise<any> {
    console.log('Not implemented');
  }
}


================================================
FILE: src/routes/index.ts
================================================
import { Router } from 'express';

import page404 from './pages/404';
import pageRoot from './pages/root';
import v1 from './v1/';

const router = Router();

router.use(`/v1`, v1);

router.use(pageRoot);
router.use(page404);

export default router;


================================================
FILE: src/routes/pages/404.ts
================================================
import { Router } from 'express';

const router = Router();

router.get('*', (req, res, next) => {
  return res.status(404).json('404 Not Found');
});

export default router;


================================================
FILE: src/routes/pages/root.ts
================================================
import { Router } from 'express';

const router = Router();

router.get('/', (req, res, next) => {
  res.status(200).header('Content-Type', 'text/html').send(`<h4>💊 RESTful API boilerplate</h4>`);
});

export default router;


================================================
FILE: src/routes/v1/auth.ts
================================================
import { Router } from 'express';

import { login, register, changePassword } from 'controllers/auth';
import { checkJwt } from 'middleware/checkJwt';
import { validatorLogin, validatorRegister, validatorChangePassword } from 'middleware/validation/auth';

const router = Router();

router.post('/login', [validatorLogin], login);
router.post('/register', [validatorRegister], register);
router.post('/change-password', [checkJwt, validatorChangePassword], changePassword);

export default router;


================================================
FILE: src/routes/v1/index.ts
================================================
import { Router } from 'express';

import auth from './auth';
import users from './users';

const router = Router();

router.use('/auth', auth);
router.use('/users', users);

export default router;


================================================
FILE: src/routes/v1/users.ts
================================================
import { Router } from 'express';

import { list, show, edit, destroy } from 'controllers/users';
import { checkJwt } from 'middleware/checkJwt';
import { checkRole } from 'middleware/checkRole';
import { validatorEdit } from 'middleware/validation/users';

const router = Router();

router.get('/', [checkJwt, checkRole(['ADMINISTRATOR'])], list);

router.get('/:id([0-9]+)', [checkJwt, checkRole(['ADMINISTRATOR'], true)], show);

router.patch('/:id([0-9]+)', [checkJwt, checkRole(['ADMINISTRATOR'], true), validatorEdit], edit);

router.delete('/:id([0-9]+)', [checkJwt, checkRole(['ADMINISTRATOR'], true)], destroy);

export default router;


================================================
FILE: src/types/JwtPayload.ts
================================================
import { Role } from '../orm/entities/users/types';

export type JwtPayload = {
  id: number;
  name: string;
  email: string;
  role: Role;
  created_at: Date;
};


================================================
FILE: src/types/ProcessEnv.d.ts
================================================
declare namespace NodeJS {
  export interface ProcessEnv {
    PORT: string;
    NODE_ENV: string;
    PG_HOST: string;
    PG_PORT: string;
    POSTGRES_USER: string;
    POSTGRES_PASSWORD: string;
    POSTGRES_DB: string;
    JWT_SECRET: string;
    JWT_EXPIRATION: string;
  }
}


================================================
FILE: src/types/express/index.d.ts
================================================
import { Language } from 'orm/entities/users/types';

import { JwtPayload } from '../JwtPayload';

declare global {
  namespace Express {
    export interface Request {
      jwtPayload: JwtPayload;
      language: Language;
    }
    export interface Response {
      customSuccess(httpStatusCode: number, message: string, data?: any): Response;
    }
  }
}


================================================
FILE: src/utils/createJwtToken.ts
================================================
import jwt from 'jsonwebtoken';

import { JwtPayload } from '../types/JwtPayload';

export const createJwtToken = (payload: JwtPayload): string => {
  return jwt.sign(payload, process.env.JWT_SECRET!, {
    expiresIn: process.env.JWT_EXPIRATION,
  });
};


================================================
FILE: src/utils/response/custom-error/CustomError.ts
================================================
import { ErrorType, ErrorValidation, ErrorResponse } from './types';

export class CustomError extends Error {
  private httpStatusCode: number;
  private errorType: ErrorType;
  private errors: string[] | null;
  private errorRaw: any;
  private errorsValidation: ErrorValidation[] | null;

  constructor(
    httpStatusCode: number,
    errorType: ErrorType,
    message: string,
    errors: string[] | null = null,
    errorRaw: any = null,
    errorsValidation: ErrorValidation[] | null = null,
  ) {
    super(message);

    this.name = this.constructor.name;

    this.httpStatusCode = httpStatusCode;
    this.errorType = errorType;
    this.errors = errors;
    this.errorRaw = errorRaw;
    this.errorsValidation = errorsValidation;
  }

  get HttpStatusCode() {
    return this.httpStatusCode;
  }

  get JSON(): ErrorResponse {
    return {
      errorType: this.errorType,
      errorMessage: this.message,
      errors: this.errors,
      errorRaw: this.errorRaw,
      errorsValidation: this.errorsValidation,
      stack: this.stack,
    };
  }
}


================================================
FILE: src/utils/response/custom-error/types.ts
================================================
export type ErrorResponse = {
  errorType: ErrorType;
  errorMessage: string;
  errors: string[] | null;
  errorRaw: any;
  errorsValidation: ErrorValidation[] | null;
  stack?: string;
};

export type ErrorType = 'General' | 'Raw' | 'Validation' | 'Unauthorized';

export type ErrorValidation = { [key: string]: string };


================================================
FILE: src/utils/response/customSuccess.ts
================================================
import { response, Response } from 'express';

response.customSuccess = function (httpStatusCode: number, message: string, data: any = null): Response {
  return this.status(httpStatusCode).json({ message, data });
};


================================================
FILE: tsconfig.json
================================================
{
  "compilerOptions": {
    "target": "es6",
    "module": "commonjs",
    "lib": ["dom", "es6", "es2017", "esnext.asynciterable"],
    "sourceMap": true,
    "outDir": "./dist",
    "moduleResolution": "node",
    "removeComments": true,
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "resolveJsonModule": true,
    "typeRoots": ["./src/types", "./node_modules/@types"],
    "baseUrl": "src/",
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true
  },
  "include": ["./src/**/*.tsx", "./src/**/*.ts"],
  "exclude": ["node_modules", "test/**/*.ts"]
}
Download .txt
gitextract_whv901lw/

├── .commitlintrc.ts
├── .czrc
├── .dockerignore
├── .eslintrc.js
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.yml
│   │   └── config.yml
│   └── workflows/
│       └── main.yml
├── .gitignore
├── .husky/
│   ├── commit-msg
│   ├── pre-commit
│   └── pre-push
├── .lintstagedrc
├── .prettierignore
├── .prettierrc
├── Dockerfile
├── LICENSE
├── README.md
├── config/
│   ├── prod.env
│   └── stage.env
├── database/
│   └── .gitkeep
├── docker-compose.dev.yml
├── docker-compose.prod.yml
├── docker-compose.stage.yml
├── docker-compose.yml
├── log/
│   └── .gitkeep
├── package.json
├── postman/
│   ├── RESTful_API_Boilerplate.postman_collection.json
│   └── RESTful_API_Boilerplate.postman_environment.json
├── renovate.json
├── scripts/
│   ├── be-node-dev.sh
│   └── wait-for-it.sh
├── src/
│   ├── consts/
│   │   └── ConstsUser.ts
│   ├── controllers/
│   │   ├── auth/
│   │   │   ├── changePassword.ts
│   │   │   ├── index.ts
│   │   │   ├── login.test.ts
│   │   │   ├── login.ts
│   │   │   ├── register.test.ts
│   │   │   └── register.ts
│   │   └── users/
│   │       ├── destroy.ts
│   │       ├── edit.ts
│   │       ├── index.test.ts
│   │       ├── index.ts
│   │       ├── list.ts
│   │       └── show.ts
│   ├── index.ts
│   ├── middleware/
│   │   ├── checkJwt.ts
│   │   ├── checkRole.ts
│   │   ├── errorHandler.ts
│   │   ├── getLanguage.ts
│   │   └── validation/
│   │       ├── auth/
│   │       │   ├── index.ts
│   │       │   ├── validatorChangePassword.ts
│   │       │   ├── validatorLogin.ts
│   │       │   └── validatorRegister.ts
│   │       └── users/
│   │           ├── index.ts
│   │           └── validatorEdit.ts
│   ├── orm/
│   │   ├── config/
│   │   │   ├── ormconfig-seed.ts
│   │   │   └── ormconfig.ts
│   │   ├── dbCreateConnection.ts
│   │   ├── entities/
│   │   │   └── users/
│   │   │       ├── User.ts
│   │   │       └── types.ts
│   │   ├── migrations/
│   │   │   └── 1590521920166-CreateUsers.ts
│   │   └── seeds/
│   │       └── 1590519635401-SeedUsers.ts
│   ├── routes/
│   │   ├── index.ts
│   │   ├── pages/
│   │   │   ├── 404.ts
│   │   │   └── root.ts
│   │   └── v1/
│   │       ├── auth.ts
│   │       ├── index.ts
│   │       └── users.ts
│   ├── types/
│   │   ├── JwtPayload.ts
│   │   ├── ProcessEnv.d.ts
│   │   └── express/
│   │       └── index.d.ts
│   └── utils/
│       ├── createJwtToken.ts
│       └── response/
│           ├── custom-error/
│           │   ├── CustomError.ts
│           │   └── types.ts
│           └── customSuccess.ts
└── tsconfig.json
Download .txt
SYMBOL INDEX (24 symbols across 10 files)

FILE: src/consts/ConstsUser.ts
  type ConstsUser (line 1) | enum ConstsUser {

FILE: src/orm/entities/users/User.ts
  class User (line 7) | class User {
    method setLanguage (line 50) | setLanguage(language: Language) {
    method hashPassword (line 54) | hashPassword() {
    method checkIfPasswordMatch (line 58) | checkIfPasswordMatch(unencryptedPassword: string) {

FILE: src/orm/entities/users/types.ts
  type Role (line 1) | type Role = 'ADMINISTRATOR' | 'STANDARD';
  type Language (line 2) | type Language = 'en-US' | 'sl-SI';

FILE: src/orm/migrations/1590521920166-CreateUsers.ts
  class CreateUsers1590521920166 (line 3) | class CreateUsers1590521920166 implements MigrationInterface {
    method up (line 6) | public async up(queryRunner: QueryRunner): Promise<void> {
    method down (line 13) | public async down(queryRunner: QueryRunner): Promise<void> {

FILE: src/orm/seeds/1590519635401-SeedUsers.ts
  class SeedUsers1590519635401 (line 6) | class SeedUsers1590519635401 implements MigrationInterface {
    method up (line 7) | public async up(queryRunner: QueryRunner): Promise<any> {
    method down (line 93) | public async down(queryRunner: QueryRunner): Promise<any> {

FILE: src/types/JwtPayload.ts
  type JwtPayload (line 3) | type JwtPayload = {

FILE: src/types/ProcessEnv.d.ts
  type ProcessEnv (line 2) | interface ProcessEnv {

FILE: src/types/express/index.d.ts
  type Request (line 7) | interface Request {
  type Response (line 11) | interface Response {

FILE: src/utils/response/custom-error/CustomError.ts
  class CustomError (line 3) | class CustomError extends Error {
    method constructor (line 10) | constructor(
    method HttpStatusCode (line 29) | get HttpStatusCode() {
    method JSON (line 33) | get JSON(): ErrorResponse {

FILE: src/utils/response/custom-error/types.ts
  type ErrorResponse (line 1) | type ErrorResponse = {
  type ErrorType (line 10) | type ErrorType = 'General' | 'Raw' | 'Validation' | 'Unauthorized';
  type ErrorValidation (line 12) | type ErrorValidation = { [key: string]: string };
Condensed preview — 76 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (76K chars).
[
  {
    "path": ".commitlintrc.ts",
    "chars": 180,
    "preview": "import type { UserConfig } from '@commitlint/types';\n\nconst commitlintConfig: UserConfig = {\n  extends: ['@commitlint/co"
  },
  {
    "path": ".czrc",
    "chars": 55,
    "preview": "{\n  \"path\": \"node_modules/cz-conventional-changelog\"\n}\n"
  },
  {
    "path": ".dockerignore",
    "chars": 51,
    "preview": "config\ndatabase\ndist\nnode_modules\nscripts\n.git\n.env"
  },
  {
    "path": ".eslintrc.js",
    "chars": 1119,
    "preview": "module.exports = {\n  parser: '@typescript-eslint/parser',\n\n  extends: [\n    'plugin:@typescript-eslint/recommended',\n   "
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "chars": 483,
    "preview": "name: 'Bug Report'\ndescription: 'File a bug report'\nbody:\n  - type: 'textarea'\n    id: 'description'\n    attributes:\n   "
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "chars": 227,
    "preview": "blank_issues_enabled: false\ncontact_links:\n  - name: Ask a question\n    url: https://github.com/mkosir/express-typescrip"
  },
  {
    "path": ".github/workflows/main.yml",
    "chars": 621,
    "preview": "name: CI\n\non: push\n\njobs:\n  validate:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout 🛎️\n        uses: act"
  },
  {
    "path": ".gitignore",
    "chars": 201,
    "preview": ".idea/\n.vscode/\n.DS_Store\n\nnode_modules/\nbuild/\ndist/\ntmp/\ntemp/\n\ndatabase/*\n!database/.gitkeep \n\nlog/*\n!log/.gitkeep \n*"
  },
  {
    "path": ".husky/commit-msg",
    "chars": 81,
    "preview": "#!/bin/sh\n. \"$(dirname \"$0\")/_/husky.sh\"\n\nnpx --no-install commitlint --edit \"$1\""
  },
  {
    "path": ".husky/pre-commit",
    "chars": 67,
    "preview": "#!/bin/sh\n. \"$(dirname \"$0\")/_/husky.sh\"\n\nnpm run lint-staged-husky"
  },
  {
    "path": ".husky/pre-push",
    "chars": 56,
    "preview": "#!/bin/sh\n. \"$(dirname \"$0\")/_/husky.sh\"\n\nnpm run build\n"
  },
  {
    "path": ".lintstagedrc",
    "chars": 131,
    "preview": "{\n  \"*\": [\n    \"pretty-quick --staged\"\n  ],\n  \"*.{js,jsx,ts,tsx}\": [\n    \"eslint --max-warnings 0\",\n    \"tsc-files --noE"
  },
  {
    "path": ".prettierignore",
    "chars": 4,
    "preview": "dist"
  },
  {
    "path": ".prettierrc",
    "chars": 89,
    "preview": "{\n  \"printWidth\": 120,\n  \"semi\": true,\n  \"singleQuote\": true,\n  \"trailingComma\": \"all\"\n}\n"
  },
  {
    "path": "Dockerfile",
    "chars": 198,
    "preview": "FROM node:16.14.0-alpine\n\nWORKDIR /app\n\nCOPY ./package.json .\nCOPY ./package-lock.json .\n\nRUN npm install && npm cache c"
  },
  {
    "path": "LICENSE",
    "chars": 1063,
    "preview": "MIT License\n\nCopyright (c) 2020 mkosir\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof "
  },
  {
    "path": "README.md",
    "chars": 4474,
    "preview": "#  TypeORM / Express / TypeScript RESTful API boilerplate\n\n[![CI][build-badge]][build-url]\n[![TypeScript][typescript-bad"
  },
  {
    "path": "config/prod.env",
    "chars": 268,
    "preview": "NODE_ENV=production\n\nPORT=4000\n\n### Database - Postgres\nPG_HOST=host.production.com\nPG_PORT=5432\nPOSTGRES_USER=walter\nPO"
  },
  {
    "path": "config/stage.env",
    "chars": 248,
    "preview": "NODE_ENV=stage\n\nPORT=4000\n\n### Database - Postgres\nPG_HOST=host.stage.com\nPG_PORT=5432\nPOSTGRES_USER=walter\nPOSTGRES_PAS"
  },
  {
    "path": "database/.gitkeep",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "docker-compose.dev.yml",
    "chars": 786,
    "preview": "version: '3'\n\nservices:\n  db_postgres:\n    container_name: 'db_boilerplate'\n    image: 'postgres:14.2-alpine'\n    restar"
  },
  {
    "path": "docker-compose.prod.yml",
    "chars": 296,
    "preview": "version: '3'\n\nservices:\n  be_boilerplate:\n    deploy:\n      resources:\n        limits:\n          cpus: '0.90'\n          "
  },
  {
    "path": "docker-compose.stage.yml",
    "chars": 204,
    "preview": "version: '3'\n\nservices:\n  be_boilerplate:\n    command: /bin/sh -c \"echo 'Running API on production!' && npm run build &&"
  },
  {
    "path": "docker-compose.yml",
    "chars": 167,
    "preview": "version: '3'\n\nservices:\n  be_boilerplate:\n    container_name: 'be_boilerplate'\n    build: .\n    restart: always\n    volu"
  },
  {
    "path": "log/.gitkeep",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "package.json",
    "chars": 3839,
    "preview": "{\n  \"name\": \"express-typescript-typeorm-boilerplate\",\n  \"version\": \"1.0.0\",\n  \"description\": \"RESTful API boilerplate wi"
  },
  {
    "path": "postman/RESTful_API_Boilerplate.postman_collection.json",
    "chars": 9871,
    "preview": "{\n  \"info\": {\n    \"_postman_id\": \"da0fe49f-e424-4b96-8f55-49f7db0583dc\",\n    \"name\": \"RESTful API Boilerplate\",\n    \"sch"
  },
  {
    "path": "postman/RESTful_API_Boilerplate.postman_environment.json",
    "chars": 882,
    "preview": "{\n  \"id\": \"a8df1c58-9c26-4452-a61e-0f0d2e865ee1\",\n  \"name\": \"RESTful API Boilerplate\",\n  \"values\": [\n    {\n      \"key\": "
  },
  {
    "path": "renovate.json",
    "chars": 692,
    "preview": "{\n  \"extends\": [\"config:base\", \":disableDependencyDashboard\"],\n  \"separateMinorPatch\": true,\n  \"assignees\": [\"mkosir\"],\n"
  },
  {
    "path": "scripts/be-node-dev.sh",
    "chars": 235,
    "preview": "#!/bin/sh\n\necho \"Install bash and execute 'wait-for-it.sh' script\"\napk add --update bash\n./scripts/wait-for-it.sh $PG_HO"
  },
  {
    "path": "scripts/wait-for-it.sh",
    "chars": 5071,
    "preview": "#!/usr/bin/env bash\n\nWAITFORIT_cmdname=${0##*/}\n\nechoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo \"$@\" 1>&2; fi }\n"
  },
  {
    "path": "src/consts/ConstsUser.ts",
    "chars": 52,
    "preview": "export enum ConstsUser {\n  PASSWORD_MIN_CHAR = 4,\n}\n"
  },
  {
    "path": "src/controllers/auth/changePassword.ts",
    "chars": 1139,
    "preview": "import { Request, Response, NextFunction } from 'express';\nimport { getRepository } from 'typeorm';\n\nimport { User } fro"
  },
  {
    "path": "src/controllers/auth/index.ts",
    "chars": 87,
    "preview": "export * from './changePassword';\nexport * from './login';\nexport * from './register';\n"
  },
  {
    "path": "src/controllers/auth/login.test.ts",
    "chars": 2354,
    "preview": "import 'mocha';\nimport { expect } from 'chai';\nimport { agent as request } from 'supertest';\nimport { getRepository, Con"
  },
  {
    "path": "src/controllers/auth/login.ts",
    "chars": 1566,
    "preview": "import { Request, Response, NextFunction } from 'express';\nimport { getRepository } from 'typeorm';\n\nimport { Role } fro"
  },
  {
    "path": "src/controllers/auth/register.test.ts",
    "chars": 1877,
    "preview": "import 'mocha';\nimport { expect } from 'chai';\nimport { agent as request } from 'supertest';\nimport { getRepository, Con"
  },
  {
    "path": "src/controllers/auth/register.ts",
    "chars": 1196,
    "preview": "import { Request, Response, NextFunction } from 'express';\nimport { getRepository } from 'typeorm';\n\nimport { User } fro"
  },
  {
    "path": "src/controllers/users/destroy.ts",
    "chars": 893,
    "preview": "import { Request, Response, NextFunction } from 'express';\nimport { getRepository } from 'typeorm';\n\nimport { User } fro"
  },
  {
    "path": "src/controllers/users/edit.ts",
    "chars": 1112,
    "preview": "import { Request, Response, NextFunction } from 'express';\nimport { getRepository } from 'typeorm';\n\nimport { User } fro"
  },
  {
    "path": "src/controllers/users/index.test.ts",
    "chars": 3114,
    "preview": "import 'mocha';\nimport { expect } from 'chai';\nimport { agent as request } from 'supertest';\nimport { getRepository, Con"
  },
  {
    "path": "src/controllers/users/index.ts",
    "chars": 99,
    "preview": "export * from './destroy';\nexport * from './edit';\nexport * from './list';\nexport * from './show';\n"
  },
  {
    "path": "src/controllers/users/list.ts",
    "chars": 714,
    "preview": "import { Request, Response, NextFunction } from 'express';\nimport { getRepository } from 'typeorm';\n\nimport { User } fro"
  },
  {
    "path": "src/controllers/users/show.ts",
    "chars": 889,
    "preview": "import { Request, Response, NextFunction } from 'express';\nimport { getRepository } from 'typeorm';\n\nimport { User } fro"
  },
  {
    "path": "src/index.ts",
    "chars": 1135,
    "preview": "import 'dotenv/config';\nimport 'reflect-metadata';\nimport fs from 'fs';\nimport path from 'path';\n\nimport bodyParser from"
  },
  {
    "path": "src/middleware/checkJwt.ts",
    "chars": 1331,
    "preview": "import { Request, Response, NextFunction } from 'express';\nimport jwt from 'jsonwebtoken';\n\nimport { JwtPayload } from '"
  },
  {
    "path": "src/middleware/checkRole.ts",
    "chars": 1057,
    "preview": "import { Request, Response, NextFunction } from 'express';\n\nimport { Role } from '../orm/entities/users/types';\nimport {"
  },
  {
    "path": "src/middleware/errorHandler.ts",
    "chars": 295,
    "preview": "import { Request, Response, NextFunction } from 'express';\n\nimport { CustomError } from '../utils/response/custom-error/"
  },
  {
    "path": "src/middleware/getLanguage.ts",
    "chars": 418,
    "preview": "import { Request, Response, NextFunction } from 'express';\n\nimport { Language } from '../orm/entities/users/types';\n\nexp"
  },
  {
    "path": "src/middleware/validation/auth/index.ts",
    "chars": 114,
    "preview": "export * from './validatorChangePassword';\nexport * from './validatorLogin';\nexport * from './validatorRegister';\n"
  },
  {
    "path": "src/middleware/validation/auth/validatorChangePassword.ts",
    "chars": 1603,
    "preview": "import { Request, Response, NextFunction } from 'express';\nimport validator from 'validator';\n\nimport { ConstsUser } fro"
  },
  {
    "path": "src/middleware/validation/auth/validatorLogin.ts",
    "chars": 1006,
    "preview": "import { Request, Response, NextFunction } from 'express';\nimport validator from 'validator';\n\nimport { CustomError } fr"
  },
  {
    "path": "src/middleware/validation/auth/validatorRegister.ts",
    "chars": 1593,
    "preview": "import { Request, Response, NextFunction } from 'express';\nimport validator from 'validator';\n\nimport { ConstsUser } fro"
  },
  {
    "path": "src/middleware/validation/users/index.ts",
    "chars": 33,
    "preview": "export * from './validatorEdit';\n"
  },
  {
    "path": "src/middleware/validation/users/validatorEdit.ts",
    "chars": 956,
    "preview": "import { Request, Response, NextFunction } from 'express';\nimport { getRepository } from 'typeorm';\n\nimport { User } fro"
  },
  {
    "path": "src/orm/config/ormconfig-seed.ts",
    "chars": 599,
    "preview": "import { ConnectionOptions } from 'typeorm';\nimport { SnakeNamingStrategy } from 'typeorm-naming-strategies';\n\nconst con"
  },
  {
    "path": "src/orm/config/ormconfig.ts",
    "chars": 746,
    "preview": "import { ConnectionOptions } from 'typeorm';\nimport { SnakeNamingStrategy } from 'typeorm-naming-strategies';\n\nconst con"
  },
  {
    "path": "src/orm/dbCreateConnection.ts",
    "chars": 601,
    "preview": "import { Connection, createConnection, getConnectionManager } from 'typeorm';\n\nimport config from './config/ormconfig';\n"
  },
  {
    "path": "src/orm/entities/users/User.ts",
    "chars": 1031,
    "preview": "import bcrypt from 'bcryptjs';\nimport { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } fro"
  },
  {
    "path": "src/orm/entities/users/types.ts",
    "chars": 91,
    "preview": "export type Role = 'ADMINISTRATOR' | 'STANDARD';\nexport type Language = 'en-US' | 'sl-SI';\n"
  },
  {
    "path": "src/orm/migrations/1590521920166-CreateUsers.ts",
    "chars": 1017,
    "preview": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class CreateUsers1590521920166 implements MigrationIn"
  },
  {
    "path": "src/orm/seeds/1590519635401-SeedUsers.ts",
    "chars": 2753,
    "preview": "import { MigrationInterface, QueryRunner, getRepository } from 'typeorm';\n\nimport { Role } from '../entities/users/types"
  },
  {
    "path": "src/routes/index.ts",
    "chars": 249,
    "preview": "import { Router } from 'express';\n\nimport page404 from './pages/404';\nimport pageRoot from './pages/root';\nimport v1 fro"
  },
  {
    "path": "src/routes/pages/404.ts",
    "chars": 175,
    "preview": "import { Router } from 'express';\n\nconst router = Router();\n\nrouter.get('*', (req, res, next) => {\n  return res.status(4"
  },
  {
    "path": "src/routes/pages/root.ts",
    "chars": 225,
    "preview": "import { Router } from 'express';\n\nconst router = Router();\n\nrouter.get('/', (req, res, next) => {\n  res.status(200).hea"
  },
  {
    "path": "src/routes/v1/auth.ts",
    "chars": 498,
    "preview": "import { Router } from 'express';\n\nimport { login, register, changePassword } from 'controllers/auth';\nimport { checkJwt"
  },
  {
    "path": "src/routes/v1/index.ts",
    "chars": 198,
    "preview": "import { Router } from 'express';\n\nimport auth from './auth';\nimport users from './users';\n\nconst router = Router();\n\nro"
  },
  {
    "path": "src/routes/v1/users.ts",
    "chars": 645,
    "preview": "import { Router } from 'express';\n\nimport { list, show, edit, destroy } from 'controllers/users';\nimport { checkJwt } fr"
  },
  {
    "path": "src/types/JwtPayload.ts",
    "chars": 164,
    "preview": "import { Role } from '../orm/entities/users/types';\n\nexport type JwtPayload = {\n  id: number;\n  name: string;\n  email: s"
  },
  {
    "path": "src/types/ProcessEnv.d.ts",
    "chars": 282,
    "preview": "declare namespace NodeJS {\n  export interface ProcessEnv {\n    PORT: string;\n    NODE_ENV: string;\n    PG_HOST: string;\n"
  },
  {
    "path": "src/types/express/index.d.ts",
    "chars": 359,
    "preview": "import { Language } from 'orm/entities/users/types';\n\nimport { JwtPayload } from '../JwtPayload';\n\ndeclare global {\n  na"
  },
  {
    "path": "src/utils/createJwtToken.ts",
    "chars": 255,
    "preview": "import jwt from 'jsonwebtoken';\n\nimport { JwtPayload } from '../types/JwtPayload';\n\nexport const createJwtToken = (paylo"
  },
  {
    "path": "src/utils/response/custom-error/CustomError.ts",
    "chars": 1062,
    "preview": "import { ErrorType, ErrorValidation, ErrorResponse } from './types';\n\nexport class CustomError extends Error {\n  private"
  },
  {
    "path": "src/utils/response/custom-error/types.ts",
    "chars": 323,
    "preview": "export type ErrorResponse = {\n  errorType: ErrorType;\n  errorMessage: string;\n  errors: string[] | null;\n  errorRaw: any"
  },
  {
    "path": "src/utils/response/customSuccess.ts",
    "chars": 218,
    "preview": "import { response, Response } from 'express';\n\nresponse.customSuccess = function (httpStatusCode: number, message: strin"
  },
  {
    "path": "tsconfig.json",
    "chars": 668,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"es6\",\n    \"module\": \"commonjs\",\n    \"lib\": [\"dom\", \"es6\", \"es2017\", \"esnext.asyn"
  }
]

About this extraction

This page contains the full source code of the mkosir/typeorm-express-typescript GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 76 files (66.8 KB), approximately 19.3k tokens, and a symbol index with 24 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!