├── .dockerignore
├── .env.dist
├── .env.test
├── .github
└── workflows
│ ├── build.yml
│ ├── delivery.yml
│ └── release.yml
├── .gitignore
├── .lefthook
└── commit-msg
│ └── commitlint.sh
├── .prettierrc
├── CHANGELOG.md
├── Dockerfile
├── LICENSE
├── README.md
├── docker-compose.yml
├── eslint.config.mjs
├── lefthook.yml
├── nest-cli.json
├── package-lock.json
├── package.json
├── release.config.cjs
├── renovate.json
├── src
├── app.module.ts
├── config
│ ├── database
│ │ ├── migrations
│ │ │ └── create-admin-credentials.migration.ts
│ │ └── mikro-orm.config.ts
│ └── env
│ │ └── configuration.constant.ts
├── libs
│ ├── api
│ │ ├── api-role.enum.ts
│ │ ├── graphql
│ │ │ └── paginated.type.ts
│ │ └── rest
│ │ │ ├── collection.interface.ts
│ │ │ ├── paginated-query-params.dto.ts
│ │ │ ├── paginated.response.dto.ts
│ │ │ └── sorting-type.enum.ts
│ ├── database
│ │ └── base.entity.ts
│ ├── ddd
│ │ ├── application-service.interface.ts
│ │ ├── base-entity.interface.ts
│ │ ├── domain-event.abstract.ts
│ │ ├── domain-service.interface.ts
│ │ ├── mapper.interface.ts
│ │ ├── mikro-orm-repository.abstract.ts
│ │ ├── repository.interface.ts
│ │ ├── use-case.interface.ts
│ │ └── value-object.abstract.ts
│ ├── decorator
│ │ ├── auth.decorator.ts
│ │ └── query-params.decorator.ts
│ ├── exceptions
│ │ ├── custom-bad-request.exception.ts
│ │ ├── custom-conflict.exception.ts
│ │ ├── custom-not-found.exception.ts
│ │ └── exception.filter.ts
│ ├── pipe
│ │ └── query-params-validation.pipe.ts
│ └── util
│ │ └── config.util.ts
├── main.ts
└── modules
│ ├── auth
│ ├── api
│ │ ├── guard
│ │ │ └── auth.guard.ts
│ │ └── rest
│ │ │ ├── controller
│ │ │ └── auth.controller.ts
│ │ │ └── presentation
│ │ │ ├── body
│ │ │ ├── login.body.ts
│ │ │ ├── refresh-token.body.ts
│ │ │ └── signup.body.ts
│ │ │ └── dto
│ │ │ ├── auth-user.dto.ts
│ │ │ └── jwt-user.dto.ts
│ ├── application
│ │ ├── command
│ │ │ └── register-user.command.ts
│ │ ├── query
│ │ │ ├── check-auth-user-by-id.query.ts
│ │ │ └── get-auth-user-by-email.query.ts
│ │ ├── service
│ │ │ └── jwt-auth-service.interface.ts
│ │ └── use-case
│ │ │ ├── login.use-case.ts
│ │ │ └── signup.use-case.ts
│ ├── auth.module.ts
│ ├── auth.tokens.ts
│ └── infrastructure
│ │ └── jwt
│ │ └── jwt.service.ts
│ ├── communication
│ ├── application
│ │ ├── handler
│ │ │ └── event
│ │ │ │ └── created-user.handler.ts
│ │ └── service
│ │ │ └── email.service.interface.ts
│ ├── communication.module.ts
│ ├── communication.tokens.ts
│ └── infrastructure
│ │ └── email
│ │ └── email.service.ts
│ ├── health
│ ├── api
│ │ └── rest
│ │ │ └── controller
│ │ │ └── health.controller.ts
│ └── health.module.ts
│ └── user
│ ├── api
│ ├── graphql
│ │ ├── presentation
│ │ │ ├── input
│ │ │ │ └── create-user.input.ts
│ │ │ └── model
│ │ │ │ ├── paginated-user.model.ts
│ │ │ │ └── user.model.ts
│ │ └── resolver
│ │ │ └── user.resolver.ts
│ └── rest
│ │ ├── controller
│ │ └── user.controller.ts
│ │ └── presentation
│ │ ├── body
│ │ └── create-user.body.ts
│ │ ├── dto
│ │ └── user.dto.ts
│ │ └── params
│ │ ├── user-filter.params.ts
│ │ ├── user-sort.params.ts
│ │ └── user.params.ts
│ ├── application
│ ├── command
│ │ └── create-user.command.ts
│ ├── handler
│ │ ├── command
│ │ │ ├── create-user.handler.ts
│ │ │ └── register-user.handler.ts
│ │ └── query
│ │ │ ├── check-auth-user-by-id.handler.ts
│ │ │ ├── get-all-users.handler.ts
│ │ │ ├── get-auth-user-by-email.handler.ts
│ │ │ └── get-user-by-id.handler.ts
│ ├── query
│ │ ├── get-all-users.query.ts
│ │ ├── get-user-by-email.query.ts
│ │ └── get-user-by-id.query.ts
│ └── use-case
│ │ └── create-user.use-case.ts
│ ├── domain
│ ├── entity
│ │ └── user.entity.ts
│ ├── event
│ │ └── created-user.event.ts
│ ├── repository
│ │ └── user.repository.interface.ts
│ └── value-object
│ │ ├── user-role.enum.ts
│ │ └── user-state.enum.ts
│ ├── infrastructure
│ └── database
│ │ ├── entity
│ │ └── user.entity.ts
│ │ ├── mapper
│ │ └── user.mapper.ts
│ │ └── repository
│ │ └── user.repository.ts
│ ├── user.module.ts
│ └── user.tokens.ts
├── test
├── config
│ ├── jest-e2e.config.ts
│ ├── jest-unit.config.ts
│ ├── jest.config.base.ts
│ └── jest.setup.ts
└── e2e
│ ├── auth
│ └── auth.e2e-spec.ts
│ ├── health
│ └── health.e2e-spec.ts
│ ├── user
│ └── user.e2e-spec.ts
│ └── util
│ └── setup-e2e-test.util.ts
├── tsconfig.build.json
└── tsconfig.json
/.dockerignore:
--------------------------------------------------------------------------------
1 | postgresql_volume
2 | Dockerfile
3 | .dockerignore
4 | npm-debug.log
5 | node_modules
6 | dist
7 | *.log
8 | *.md
9 | .git
10 |
--------------------------------------------------------------------------------
/.env.dist:
--------------------------------------------------------------------------------
1 | DATABASE_NAME=db-test
2 | DATABASE_HOST=127.0.0.1
3 | DATABASE_PORT=15432
4 | DATABASE_USER=postgres
5 | DATABASE_PASSWORD=postgres
6 |
7 | JWT_SECRET=nestjs-ddd-devops
8 | JWT_REFRESH_SECRET=nestjs-ddd-devops-refresh
9 | JWT_EXPIRES_IN=3600
10 | JET_REFRESH_EXPIRES_IN=360000
11 |
12 | PORT=3000
--------------------------------------------------------------------------------
/.env.test:
--------------------------------------------------------------------------------
1 | DATABASE_NAME=db-test-e2e
2 | DATABASE_HOST=127.0.0.1
3 | DATABASE_PORT=25432
4 | DATABASE_USER=postgres
5 | DATABASE_PASSWORD=postgres
6 |
7 | JWT_SECRET=nestjs-ddd-devops
8 | JWT_REFRESH_SECRET=nestjs-ddd-devops-refresh
9 | JWT_EXPIRES_IN=3600
10 | JWT_REFRESH_EXPIRES_IN=360000
11 |
12 | PORT=3000
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on:
4 | push:
5 | paths-ignore:
6 | - '.gitignore'
7 | - 'CHANGELOG.md'
8 | - 'LICENSE'
9 | - 'README.md'
10 | - 'renovate.json'
11 | pull_request:
12 |
13 | jobs:
14 | Build:
15 | strategy:
16 | matrix:
17 | node-version: ['20.x', '22.x' ]
18 | runs-on: ubuntu-latest
19 | services:
20 | postgres:
21 | image: postgres:latest
22 | env:
23 | POSTGRES_DB: db-test
24 | POSTGRES_USER: postgres
25 | POSTGRES_PASSWORD: postgres
26 | options: >-
27 | --health-cmd pg_isready
28 | --health-interval 10s
29 | --health-timeout 5s
30 | --health-retries 5
31 | ports:
32 | - 5432:5432
33 | steps:
34 | - uses: actions/checkout@v4
35 | with:
36 | submodules: recursive
37 | fetch-depth: 0
38 | - name: Use Node.js ${{ matrix.node-version }}
39 | uses: actions/setup-node@v4
40 | with:
41 | node-version: ${{ matrix.node-version }}
42 | - name: Install dependencies
43 | run: npm ci
44 | - name: Check style
45 | run: npm run lint
46 | - name: Build project
47 | run: npm run build
48 | - name: Unit Tests
49 | run: npm run test
50 | - name: E2E Tests
51 | run: |
52 | npm run schema:update
53 | npm run migrate:up
54 | npm run test:e2e
55 | env:
56 | DATABASE_NAME: db-test
57 | DATABASE_HOST: 127.0.0.1
58 | DATABASE_PORT: 5432
59 | DATABASE_USER: postgres
60 | DATABASE_PASSWORD: postgres
--------------------------------------------------------------------------------
/.github/workflows/delivery.yml:
--------------------------------------------------------------------------------
1 | name: Delivery
2 | on:
3 | workflow_run:
4 | workflows: [ "Release" ]
5 | branches: [ main ]
6 | types:
7 | - completed
8 | jobs:
9 | Delivery:
10 | if: ${{ github.event.workflow_run.conclusion == 'success' }}
11 | name: Delivery
12 | runs-on: ubuntu-latest
13 | permissions:
14 | packages: write
15 | steps:
16 | - name: Checkout
17 | uses: actions/checkout@v4
18 | with:
19 | fetch-depth: 0
20 | - name: Log in to GitHub Container Registry (GHCR)
21 | run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
22 | - name: Extract Release Version
23 | id: get_release_version
24 | run: |
25 | VERSION=$(git describe --tags --abbrev=0)
26 | if [ -z "$VERSION" ]; then
27 | echo "Error: No tags found in the repository!" && exit 1
28 | fi
29 | echo "RELEASE_VERSION=$VERSION" >> $GITHUB_ENV
30 | - name: Build Docker Image
31 | run: |
32 | IMAGE_NAME=ghcr.io/${{ github.repository }}
33 | docker build -t $IMAGE_NAME:${{ env.RELEASE_VERSION }} .
34 | docker tag $IMAGE_NAME:${{ env.RELEASE_VERSION }} $IMAGE_NAME:latest
35 | - name: Push Docker Image to GitHub Container Registry (GHCR)
36 | run: |
37 | IMAGE_NAME=ghcr.io/${{ github.repository }}
38 | docker push $IMAGE_NAME:${{ env.RELEASE_VERSION }}
39 | docker push $IMAGE_NAME:latest
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 | on:
3 | workflow_run:
4 | workflows: [ "Build" ]
5 | branches: [ main ]
6 | types:
7 | - completed
8 | jobs:
9 | Release:
10 | if: ${{ github.event.workflow_run.conclusion == 'success' }}
11 | name: Release
12 | runs-on: ubuntu-latest
13 | permissions:
14 | contents: write
15 | issues: write
16 | pull-requests: write
17 | steps:
18 | - name: Checkout
19 | uses: actions/checkout@v4
20 | with:
21 | fetch-depth: 0
22 | - name: Setup Node.js
23 | uses: actions/setup-node@v4
24 | with:
25 | node-version: "lts/*"
26 | - name: Install dependencies
27 | run: npm clean-install --ignore-scripts
28 | - name: Verify the integrity of provenance attestations and registry signatures for installed dependencies
29 | run: npm audit signatures
30 | - name: Release
31 | env:
32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
33 | run: npx semantic-release
34 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 | .pnpm-debug.log*
9 |
10 | # Diagnostic reports (https://nodejs.org/api/report.html)
11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
12 |
13 | # Runtime data
14 | pids
15 | *.pid
16 | *.seed
17 | *.pid.lock
18 |
19 | # Directory for instrumented libs generated by jscoverage/JSCover
20 | lib-cov
21 |
22 | # Coverage directory used by tools like istanbul
23 | coverage
24 | *.lcov
25 |
26 | # nyc test coverage
27 | .nyc_output
28 |
29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
30 | .grunt
31 |
32 | # Bower dependency directory (https://bower.io/)
33 | bower_components
34 |
35 | # node-waf configuration
36 | .lock-wscript
37 |
38 | # Compiled binary addons (https://nodejs.org/api/addons.html)
39 | build/Release
40 |
41 | # Dependency directories
42 | node_modules/
43 | jspm_packages/
44 |
45 | # Snowpack dependency directory (https://snowpack.dev/)
46 | web_modules/
47 |
48 | # TypeScript cache
49 | *.tsbuildinfo
50 |
51 | # Optional npm cache directory
52 | .npm
53 |
54 | # Optional eslint cache
55 | .eslintcache
56 |
57 | # Optional stylelint cache
58 | .stylelintcache
59 |
60 | # Microbundle cache
61 | .rpt2_cache/
62 | .rts2_cache_cjs/
63 | .rts2_cache_es/
64 | .rts2_cache_umd/
65 |
66 | # Optional REPL history
67 | .node_repl_history
68 |
69 | # Output of 'npm pack'
70 | *.tgz
71 |
72 | # Yarn Integrity file
73 | .yarn-integrity
74 |
75 | # dotenv environment variable files
76 | .env
77 | .env.development.local
78 | .env.test.local
79 | .env.production.local
80 | .env.local
81 |
82 | # parcel-bundler cache (https://parceljs.org/)
83 | .cache
84 | .parcel-cache
85 |
86 | # Next.js build output
87 | .next
88 | out
89 |
90 | # Nuxt.js build / generate output
91 | .nuxt
92 | dist
93 |
94 | # Gatsby files
95 | .cache/
96 | # Comment in the public line in if your project uses Gatsby and not Next.js
97 | # https://nextjs.org/blog/next-9-1#public-directory-support
98 | # public
99 |
100 | # vuepress build output
101 | .vuepress/dist
102 |
103 | # vuepress v2.x temp and cache directory
104 | .temp
105 |
106 | # Docusaurus cache and generated files
107 | .docusaurus
108 |
109 | # Serverless directories
110 | .serverless/
111 |
112 | # FuseBox cache
113 | .fusebox/
114 |
115 | # DynamoDB Local files
116 | .dynamodb/
117 |
118 | # TernJS port file
119 | .tern-port
120 |
121 | # Stores VSCode versions used for testing VSCode extensions
122 | .vscode-test
123 |
124 | # yarn v2
125 | .yarn/cache
126 | .yarn/unplugged
127 | .yarn/build-state.yml
128 | .yarn/install-state.gz
129 | .pnp.*
130 |
131 |
132 | .idea/
133 |
134 | postgresql_volume
--------------------------------------------------------------------------------
/.lefthook/commit-msg/commitlint.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # list of Conventional Commits types
4 | types=(
5 | fix feat chore docs perf revert test ci build style refactor
6 | )
7 |
8 | # the commit message file is the only argument
9 | msg_file="$1"
10 |
11 | # join types with | to form regex ORs
12 | r_types="($(IFS='|'; echo "${types[*]}"))"
13 | # optional (scope)
14 | r_scope="(\([[:alnum:] \/-]+\))?"
15 | # optional breaking change indicator and colon delimiter
16 | r_delim='!?:'
17 | # subject line, body, footer
18 | r_subject=" [[:alnum:]].+"
19 | # the full regex pattern
20 | pattern="^$r_types$r_scope$r_delim$r_subject$"
21 |
22 | # Check if commit is conventional commit
23 | if grep -Eq "$pattern" "$msg_file"; then
24 | exit 0
25 | fi
26 |
27 |
28 | echo "[ERROR]: Invalid commit message!"
29 | exit 1
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "all",
4 | "semi": true
5 | }
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## [1.3.0](https://github.com/andrea-acampora/nestjs-ddd-devops/compare/1.2.0...1.3.0) (2025-03-27)
2 |
3 | ### Features
4 |
5 | * create aggregate root and update dispatch event logic ([99789a9](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/99789a97abfae6fea5f59932bfa8eed2e1663b1b))
6 |
7 | ### Dependency updates
8 |
9 | * **deps:** update dependency effect to v3.13.11 ([#122](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/122)) ([fd7a211](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/fd7a2116d75dd2acb71b6ddef98dcca38e8cbf9a))
10 | * **deps:** update dependency effect to v3.13.12 ([#125](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/125)) ([6ae779c](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/6ae779c654bb4455e0c25be737dcd1ed7e071ec4))
11 | * **deps:** update mikro-orm monorepo to v6.4.10 ([#123](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/123)) ([7ea276a](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/7ea276a60219995a0399414a70da79c308ac4bfe))
12 |
13 | ### General maintenance
14 |
15 | * add get users graphql query and create user mutation ([8829990](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/882999068296c1eb34cca1319a3ddd8fe631a35b))
16 | * add graphql dependencies and create user resolver ([28e8db9](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/28e8db98eb51022fcccb4edf4b5b4f95bb552696))
17 | * add graphql documentation ([d1d46c1](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/d1d46c1bea7b1f1a782b12697cd67ad4d79bc1e2))
18 |
19 | ## [1.2.0](https://github.com/andrea-acampora/nestjs-ddd-devops/compare/1.1.0...1.2.0) (2025-03-14)
20 |
21 | ### Features
22 |
23 | * create mapper interface ([4f517ff](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/4f517ffae6b5b11d8f5c7632ab8e0f5d9540ed68))
24 | * create pure domain entity ([9804c25](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/9804c250e10322e8fd6d1aae2affaff33d1a7906))
25 | * create user mapper and refactor code to use new domain entity ([e71e350](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/e71e3505747f47eae229ce2e3b21f9c6ea5fc9fe))
26 |
27 | ### Dependency updates
28 |
29 | * **deps:** update dependency @types/bcryptjs to v3 ([#119](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/119)) ([75f05e2](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/75f05e2640225d8569606990a6ed77cc87482bb8))
30 | * **deps:** update dependency @types/node to v22.13.10 ([#113](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/113)) ([d16d71a](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/d16d71a19f2c0a5c687c8ad5e6c77abe747dc2b9))
31 | * **deps:** update dependency @types/node to v22.13.9 ([#101](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/101)) ([7e69aad](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/7e69aadd2ed05065dccc23c7108a199c0834336f))
32 | * **deps:** update dependency effect to v3.13.10 ([#118](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/118)) ([12f6179](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/12f6179c58d510e4ec485adec586623531e8ba85))
33 | * **deps:** update dependency effect to v3.13.6 ([#102](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/102)) ([00016ec](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/00016ec000daeaac75737780e0f6beb1e678ce64))
34 | * **deps:** update dependency effect to v3.13.7 ([#107](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/107)) ([cf5d0eb](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/cf5d0eb4b2d06edd76e1f11eaca8b9e3d1ff5c9a))
35 | * **deps:** update dependency effect to v3.13.8 ([#115](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/115)) ([52ff535](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/52ff535ad6910c7a334f05d845e991abe360731b))
36 | * **deps:** update dependency effect to v3.13.9 ([#117](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/117)) ([d37d622](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/d37d622d53784c973b81256538421f67d24519d5))
37 | * **deps:** update dependency eslint to v9.22.0 ([#112](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/112)) ([217b1d3](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/217b1d3f3997ce97cbca5cba6f50d88192abf7b2))
38 | * **deps:** update dependency eslint-config-prettier to v10.1.1 ([#111](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/111)) ([3156dc2](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/3156dc219cebcae555eaf65fbc73fa1e8a807f8b))
39 | * **deps:** update dependency lefthook to v1.11.3 ([#109](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/109)) ([076c558](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/076c5581029f25a77b28459434ba896838afdbd1))
40 | * **deps:** update dependency typescript to v5.8.2 ([#105](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/105)) ([42e6d6a](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/42e6d6a3425a49eba2f2b898fb7c1ab20f876c7c))
41 | * **deps:** update mikro-orm monorepo to v6.4.8 ([#106](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/106)) ([c7116f8](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/c7116f83f83bcc156dbeb97d1b83dd607133d2d6))
42 | * **deps:** update mikro-orm monorepo to v6.4.9 ([#110](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/110)) ([5cb369e](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/5cb369eaeae7eed795ce32ca8d1cd18c263a58ed))
43 | * **deps:** update nest monorepo ([#100](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/100)) ([1916dd1](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/1916dd1f9e1efedcde7420030cd121fd63a094f5))
44 | * **deps:** update typescript-eslint monorepo to v8.26.0 ([#103](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/103)) ([028a936](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/028a936ddeb1e2c0625154d17175772cbfda0cc6))
45 | * **deps:** update typescript-eslint monorepo to v8.26.1 ([#116](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/116)) ([2ceb2c5](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/2ceb2c51ca35385d9824914e0ebff4d10814e914))
46 |
47 | ### General maintenance
48 |
49 | * add communication module and database migrations ([2e19bab](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/2e19babd402eb1aeba461c26e074b653abf5c1a7))
50 | * add refresh token endpoint ([df1f4f6](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/df1f4f690c8efdf441259245d74e96316da32c53))
51 | * create handlers ([88884f0](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/88884f03241e702974e2d0131cdcd6fff45291f6))
52 | * disable healthcheck memory check ([397bad2](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/397bad26b1b19c778cece9e72d49e6db7ecf870e))
53 | * implement api for retrieving users ([5dad820](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/5dad8208cbdaa12e1b93a777474ed235974520b7))
54 | * implement create user use case ([81a6459](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/81a64596c39918e139e89d8fee3a8f27edd5e5ba))
55 | * **test:** add user tests ([6efb8a1](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/6efb8a1807619ca8c20805cf6a5f104cf1c69dac))
56 |
57 | ### Refactoring
58 |
59 | * update auth e2e tests ([b00cdca](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/b00cdcaf28989ce580275b55278ef04f4904b528))
60 |
61 | ## [1.1.0](https://github.com/andrea-acampora/nestjs-ddd-devops/compare/1.0.0...1.1.0) (2025-03-03)
62 |
63 | ### Features
64 |
65 | * **auth:** implement login and signup use case and add e2e tests ([d029ed8](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/d029ed8be4ba61192521a1a23c4f55720e7b2264))
66 |
67 | ### Dependency updates
68 |
69 | * **deps:** update dependency @eslint/eslintrc to v3.3.0 ([a53200d](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/a53200d1bcb0152c60623cf9c5d0bdcd985b6795))
70 | * **deps:** update dependency @fastify/static to v8.1.0 ([#45](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/45)) ([ab2820c](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/ab2820c1099c747f7443d2c9f5ca99467dc6520c))
71 | * **deps:** update dependency @fastify/static to v8.1.1 ([#70](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/70)) ([27afc45](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/27afc451f7197369bbf688383b0e154168cbd061))
72 | * **deps:** update dependency @mikro-orm/nestjs to v6.1.1 ([#69](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/69)) ([a5d8f43](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/a5d8f4341e3f328001d0050b526bce964d54c6a0))
73 | * **deps:** update dependency @nestjs/cli to v11.0.4 ([#74](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/74)) ([2b00a60](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/2b00a60e44893fb3a1acb2852d448fc341fce00e))
74 | * **deps:** update dependency @nestjs/cli to v11.0.5 ([#92](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/92)) ([33d49cb](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/33d49cb0dbe9056b66f04abc4c6dde1db11682df))
75 | * **deps:** update dependency @nestjs/event-emitter to v3.0.1 ([#90](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/90)) ([8a2d0b7](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/8a2d0b7275263f406d9ba7e68ac27f5a2730465d))
76 | * **deps:** update dependency @types/jsonwebtoken to v9.0.9 ([#86](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/86)) ([33fcf8b](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/33fcf8b416675d581991fbf13a96a8dd20a592b5))
77 | * **deps:** update dependency @types/node to v22.13.0 ([#44](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/44)) ([daab7a0](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/daab7a057e1cb20f7a641ca182c9a46aa279fc5a))
78 | * **deps:** update dependency @types/node to v22.13.1 ([#47](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/47)) ([b1ef6e5](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/b1ef6e5bc67c98098b9e82b218552e51bb6d7d13))
79 | * **deps:** update dependency @types/node to v22.13.2 ([#62](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/62)) ([aaaab03](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/aaaab0352c68e0e463c2811ac98279ec87a74fa0))
80 | * **deps:** update dependency @types/node to v22.13.4 ([#65](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/65)) ([6d13150](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/6d131506f5ed631e0d294c83b454c22a2e190e77))
81 | * **deps:** update dependency @types/node to v22.13.5 ([#80](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/80)) ([6216278](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/621627892fa5bb4c12071dad5375d2d3ea3f676c))
82 | * **deps:** update dependency @types/node to v22.13.7 ([#97](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/97)) ([6fc0750](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/6fc075082b5b7edb9c32bf6e40e3ec4be2dec946))
83 | * **deps:** update dependency @types/node to v22.13.8 ([#98](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/98)) ([fd12c0f](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/fd12c0fd2c6d590f4295518e5d2a3d254e168dbc))
84 | * **deps:** update dependency bcryptjs to v3 ([#64](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/64)) ([16cda09](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/16cda09b646e164297bdd6efebae4269478b3c67))
85 | * **deps:** update dependency bcryptjs to v3.0.1 ([#72](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/72)) ([cd90e37](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/cd90e371ef4ce022b764cdb942cdb2da9966b878))
86 | * **deps:** update dependency bcryptjs to v3.0.2 ([#76](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/76)) ([f65b1db](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/f65b1db012852018192b3857648ebb26b54469a9))
87 | * **deps:** update dependency effect to v3.12.10 ([#52](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/52)) ([001cea6](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/001cea668c52ba1acbcb9729b04a9e593290deec))
88 | * **deps:** update dependency effect to v3.12.11 ([#57](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/57)) ([0457c77](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/0457c7746cd03e2f4a16223a28ec887c01259be3))
89 | * **deps:** update dependency effect to v3.12.12 ([#66](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/66)) ([179c7a7](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/179c7a7d1176ad223371f0891a5d32864968bd78))
90 | * **deps:** update dependency effect to v3.12.9 ([#49](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/49)) ([e55b731](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/e55b731a7f614f4948cda747ae96b488346c988d))
91 | * **deps:** update dependency effect to v3.13.1 ([#68](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/68)) ([c0327c7](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/c0327c780bbb1cd2fe17d36fba5c950ddefb8942))
92 | * **deps:** update dependency effect to v3.13.2 ([#75](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/75)) ([14ce61d](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/14ce61dc186fccb81c3e222f5e2f6c9f76c24742))
93 | * **deps:** update dependency effect to v3.13.3 ([#94](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/94)) ([fb9c9e2](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/fb9c9e28dc80c2864576b8b0cb7674d3f73a052e))
94 | * **deps:** update dependency effect to v3.13.4 ([9d62524](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/9d62524735dd2caf031290776863611da8520193))
95 | * **deps:** update dependency eslint to v9.20.0 ([#53](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/53)) ([0ddb351](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/0ddb35197a771a230ce12ad6669c89c9cdd134fd))
96 | * **deps:** update dependency eslint to v9.20.1 ([#59](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/59)) ([ffd73ca](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/ffd73ca301533f060ee52bb963c169c0342af53c))
97 | * **deps:** update dependency eslint to v9.21.0 ([618cdc7](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/618cdc7b92d7791e6ee27ba9764fde3e9fd497f4))
98 | * **deps:** update dependency eslint-config-prettier to v10.0.2 ([4c5f223](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/4c5f2231074cf00f6b4ca12244c306736631a716))
99 | * **deps:** update dependency lefthook to v1.11.2 ([0aff2c6](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/0aff2c6ae1323e615b6d147eaf735ddcd580b508))
100 | * **deps:** update dependency prettier to v3.5.0 ([#54](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/54)) ([44b006e](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/44b006e8ff8517fb7f34f1960638ffc64c991a7b))
101 | * **deps:** update dependency prettier to v3.5.1 ([#63](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/63)) ([c085425](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/c0854251d01d9f0ce1f56f1bcd08df3ceac2a950))
102 | * **deps:** update dependency prettier to v3.5.2 ([aee7dca](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/aee7dcab1ea78e2c691576eec17cf321bf0bd9b8))
103 | * **deps:** update dependency prettier to v3.5.3 ([#99](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/99)) ([add0486](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/add0486cb7b50403a37b1225587b36de7afc373b))
104 | * **deps:** update dependency rxjs to v7.8.2 ([c1d71ee](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/c1d71eeb8c6a90f63cda4259fb4df54e2ceb13ae))
105 | * **deps:** update dependency semantic-release-preconfigured-conventional-commits to v1.1.121 ([#48](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/48)) ([d9674a9](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/d9674a910de40d0202623cd559471ae4c3b1a1c5))
106 | * **deps:** update dependency semantic-release-preconfigured-conventional-commits to v1.1.122 ([#51](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/51)) ([6d609a1](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/6d609a1052f2822ac5d9f8606cd2a20889c9e9d2))
107 | * **deps:** update dependency semantic-release-preconfigured-conventional-commits to v1.1.123 ([#61](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/61)) ([04b2684](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/04b2684dc9c588868a41320f9e9fc427d5bdc0cf))
108 | * **deps:** update dependency semantic-release-preconfigured-conventional-commits to v1.1.124 ([#67](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/67)) ([259d123](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/259d123d35913b35e65642faa1949da0d18119c0))
109 | * **deps:** update dependency semantic-release-preconfigured-conventional-commits to v1.1.125 ([#77](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/77)) ([adecda7](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/adecda779f9926e5605f13adeda04479c7b7322d))
110 | * **deps:** update dependency ts-jest to v29.2.6 ([b08bb65](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/b08bb65e94788861c1a585a7c77245293a088529))
111 | * **deps:** update dependency uuid to v11.1.0 ([#78](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/78)) ([6365bb6](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/6365bb6ddc284b9842de480203776698a38bc5ee))
112 | * **deps:** update mikro-orm monorepo to v6.4.5 ([#42](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/42)) ([a72e2a2](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/a72e2a2761413b648b961b47b00303ed67f5ed9b))
113 | * **deps:** update mikro-orm monorepo to v6.4.6 ([#60](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/60)) ([75e7cdd](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/75e7cdd592985e72a5de524d2985852bf9390d90))
114 | * **deps:** update mikro-orm monorepo to v6.4.7 ([9ae1de6](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/9ae1de6c849f596612d9e485427951f65e1347e3))
115 | * **deps:** update nest monorepo to v11 ([cde7883](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/cde78830cbedb3f573e48897d7a7b4b39cb8fa65))
116 | * **deps:** update nest monorepo to v11.0.11 ([af90c15](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/af90c154ae78b1a8b2a54231d7e7e4d0f1ddeafe))
117 | * **deps:** update nest monorepo to v11.0.3 ([#71](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/71)) ([9dd0996](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/9dd0996d027b98a8f5f322ea1f20e4d77363c45c))
118 | * **deps:** update nest monorepo to v11.0.7 ([#43](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/43)) ([1267a4d](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/1267a4d5d62a1e9ef1597ad2a2be490eb48be0d5))
119 | * **deps:** update nest monorepo to v11.0.8 ([#50](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/50)) ([bf355d1](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/bf355d1b16b2a1599ba88d35465d375608538a1c))
120 | * **deps:** update nest monorepo to v11.0.9 ([#55](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/55)) ([ea7eed9](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/ea7eed9b457b4215fea0fe4a73f2c43eb2547a8a))
121 | * **deps:** update typescript-eslint monorepo to v8.23.0 ([#46](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/46)) ([a5cb2ab](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/a5cb2aba029629c4f6a11540c72d283a227c874b))
122 | * **deps:** update typescript-eslint monorepo to v8.24.0 ([#56](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/56)) ([4f2dc1e](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/4f2dc1eb8c48ff787ba1abd31f9427d96ae81631))
123 | * **deps:** update typescript-eslint monorepo to v8.24.1 ([#73](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/73)) ([004d6d6](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/004d6d651d335bc36880f79a9b569876502235bf))
124 | * **deps:** update typescript-eslint monorepo to v8.25.0 ([2b1b6f8](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/2b1b6f8ccb592d7cf584608e821a77623203ff9e))
125 |
126 | ### General maintenance
127 |
128 | * add auth module and jwt service ([1e99946](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/1e999466bfb39ced235033b441c5fbd32bd41c47))
129 | * **auth:** implement login use case ([845eeb8](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/845eeb8bb6bbe057ee5bd8345c2ce09a7eb23195))
130 | * fix dependencies ([514d559](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/514d559b76c66c2f1ab83f4325cab235fe543cd2))
131 | * fix dependencies ([fbed161](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/fbed1611ace38831cacc4766496309cf7aef1fc6))
132 | * **test:** add check for email in login e2e test ([b3dced7](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/b3dced7b4c9f7b3d2d11ca2d7c5cce8c41dcba4e))
133 | * update gitignore ([298ceca](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/298cecad5891190d0135ab235fe22cda7471fed0))
134 | * update jest configuration ([95b99a6](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/95b99a6fe33837942ee9f2fedc4b593fd900afd3))
135 |
136 | ## 1.0.0 (2025-01-29)
137 |
138 | ### Dependency updates
139 |
140 | * **deps:** add some useful deps ([ed9853d](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/ed9853d8eed963b5f6097e98a544eb5ec4d62fcc))
141 | * **deps:** remove helmet and static fastify deps ([7e4c8f0](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/7e4c8f074370f4a3388d11b38e9ade6ea5bf98f6))
142 | * **deps:** remove useless dependency ([7504e9b](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/7504e9bd1e3d4356a2d6dc30ce05009ccfd62abf))
143 | * **deps:** update dependency @fastify/helmet to v12 ([#15](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/15)) ([89161bc](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/89161bc0b893211e410f0b2c0bc08c471b1d7638))
144 | * **deps:** update dependency @fastify/helmet to v13 ([#16](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/16)) ([fb3a7d1](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/fb3a7d1dae8766e334bdb2c855da851035a8af20))
145 | * **deps:** update dependency @mikro-orm/nestjs to v6.1.0 ([#41](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/41)) ([6911fce](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/6911fcee934a464aaaceda4f1ddb3de4c3944449))
146 | * **deps:** update dependency @nestjs/cache-manager to v3 ([#20](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/20)) ([f176f5f](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/f176f5f79ec98fe5b297887815efeaad57d5cebc))
147 | * **deps:** update dependency @nestjs/config to v4 ([#21](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/21)) ([e087c8e](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/e087c8e4851b2f9812178ea059b19609b5eff98d))
148 | * **deps:** update dependency @nestjs/event-emitter to v3 ([#22](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/22)) ([34b12db](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/34b12dbc0539f231013d0bc84cde33855dff1145))
149 | * **deps:** update dependency @nestjs/schedule to v5 ([#23](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/23)) ([640c873](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/640c873ab65cdfd4f62854b52dd8b6b1c599b11e))
150 | * **deps:** update dependency @nestjs/schedule to v5.0.1 ([#33](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/33)) ([63d8a57](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/63d8a576b640bc2b3432c14bb51a579b9434cb50))
151 | * **deps:** update dependency @nestjs/terminus to v10.3.0 ([#34](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/34)) ([abeb850](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/abeb8503dff7552606848638d47a81bcd903c610))
152 | * **deps:** update dependency @nestjs/throttler to v6.4.0 ([#31](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/31)) ([7827637](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/7827637f5692f46a161d5891214dbd518ef8c938))
153 | * **deps:** update dependency @types/express to v5 ([#4](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/4)) ([fbd2ad0](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/fbd2ad0966b7267e03fe5d0406a46d69ed14bcda))
154 | * **deps:** update dependency @types/jsonwebtoken to v9.0.8 ([#37](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/37)) ([37d9bf2](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/37d9bf206bc2f0eb64f2747a18e73eb2003d1937))
155 | * **deps:** update dependency @types/node to v22 ([660c5f0](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/660c5f0781e05ebf1199ae2f68aa84a68c6b6a4f))
156 | * **deps:** update dependency @types/node to v22.10.10 ([#35](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/35)) ([54f8494](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/54f8494e42cc002210c63e60cfef7ad6c83bee5e))
157 | * **deps:** update dependency @types/node to v22.10.9 ([#32](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/32)) ([42cc1cb](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/42cc1cbc5b16f201de5d6069b33bcb74ff7b0241))
158 | * **deps:** update dependency @types/node to v22.12.0 ([#40](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/40)) ([636f00f](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/636f00f7f73025ec64f62ddd11f6d7cdc6f76104))
159 | * **deps:** update dependency effect to v3.12.5 ([#17](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/17)) ([c03f331](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/c03f331a18730cb9f7b612420a541e6a5a086264))
160 | * **deps:** update dependency effect to v3.12.6 ([#29](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/29)) ([5092439](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/50924394a88f1b8857c430c59a2d17653a4b18a3))
161 | * **deps:** update dependency effect to v3.12.7 ([#30](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/30)) ([922cb97](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/922cb97685f04d3cae4b79be234a5c2ea4bbbcc1))
162 | * **deps:** update dependency eslint to v9.19.0 ([#38](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/38)) ([81ec5cc](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/81ec5ccc1aabad54322ae3c6e50eb2ade62b367e))
163 | * **deps:** update dependency eslint-config-prettier to v10 ([2c1ddb5](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/2c1ddb5cbe27ff74ae1aba527e542fc27079d9e9))
164 | * **deps:** update dependency eslint-plugin-prettier to v5.2.3 ([#25](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/25)) ([8ef2934](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/8ef2934a213b205b490cbbbc46b0a2534b94820f))
165 | * **deps:** update dependency lefthook to v1.10.10 ([#28](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/28)) ([93cc614](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/93cc614dc56f1ef213d3f7376f0bc13d3016d7ab))
166 | * **deps:** update dependency lefthook to v1.10.8 ([#18](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/18)) ([661cbf5](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/661cbf5f1eb1a0d99e58a39e47f02bb76e32de22))
167 | * **deps:** update dependency lefthook to v1.10.9 ([#26](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/26)) ([50a4503](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/50a4503e7e6616f3b088251ec5b21bd55ffda461))
168 | * **deps:** update dependency supertest to v7 ([aaf8933](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/aaf89337c0b34c32efd79d898a83367d519ced4a))
169 | * **deps:** update eslint to v9 ([7832bb0](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/7832bb01442dcae5e87fdde9fad0958c5ad6e5a5))
170 | * **deps:** update mikro-orm monorepo to v6.4.4 ([#36](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/36)) ([47f0cc5](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/47f0cc512bb8b30c6aef6577065942cb566c475b))
171 | * **deps:** update nest monorepo to v11 ([9e22778](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/9e227784731ad9ab4eb2b3da3e00a7bc593a7187))
172 | * **deps:** update typescript-eslint monorepo to v8 ([6443f10](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/6443f105d410c5b90a04a8abbd9b2a3db4506dde))
173 | * **deps:** update typescript-eslint monorepo to v8.21.0 ([#27](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/27)) ([6ee47a6](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/6ee47a6fea4dcde508f17ef1ae6375fb06905213))
174 | * **deps:** update typescript-eslint monorepo to v8.22.0 ([#39](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/39)) ([eb6eb21](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/eb6eb2170a5ca0d6379fd0c9e75765c1ce509345))
175 |
176 | ### Bug Fixes
177 |
178 | * update docker image creation ([f61a532](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/f61a532b895cdd5e0c1f1e2c51a3641396379048))
179 |
180 | ### Build and continuous integration
181 |
182 | * add delivery workflow ([bf97d83](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/bf97d83fa28dfa491e565769bf9f5f2ac3f162d4))
183 | * add e2e step ([b2ebcc0](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/b2ebcc0d0efc0dc8ebacbc4455499e8ee6b355d5))
184 | * create build workflow ([1165aaa](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/1165aaafd1aee73e4b63f20559fded0ae3a564a7))
185 | * create empty release workflow ([af0af5a](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/af0af5a5a03f637a68ffc8c357c0a3c4a1ad56dd))
186 | * create release workflow ([9fa185e](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/9fa185eb50c354c7fcc59cf698934f96893d5b0a))
187 | * execute migrations before e2e tests ([ea88e57](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/ea88e574a1a672b30f4becd291fe02b8fa78f742))
188 | * remove unsopported node version ([83aeb33](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/83aeb33e4eefcf6acc463d3a4beac6c9b30ee696))
189 | * run release job if only if build succeed ([730565b](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/730565b14dbe7f565109e14c569a485edc6cf67e))
190 | * temporary remove e2e tests in ci ([2596f85](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/2596f858b4e276426a4df7e195f89f67ca3dc8c8))
191 | * update build workflow ([3bba6e1](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/3bba6e14987588f8fc96dc42d38b1688d9e8bbc7))
192 | * update build workflow ([41ee762](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/41ee7626f6ac1a1eca3457feff25aa1b26d832bf))
193 | * update build.yml ([0e580f1](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/0e580f10e7801749f91dc21d0659fba1b015a0f8))
194 | * update postgres service port ([2f659af](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/2f659af32f3db385b97e67d657128bbae6e4021a))
195 |
196 | ### General maintenance
197 |
198 | * add automatic dependency update chapter ([441dccb](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/441dccb6d1a9d1862505162f5c1a1d5b4521c363))
199 | * add clean architecture chapter ([3928bf7](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/3928bf7b0913abae2fb3b90a925389282e53c3df))
200 | * add clean architecture image ([bb8af71](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/bb8af718bab832bc5dae5f83ed251f13cbd7fce4))
201 | * add continuous delivery chapter ([d7a31af](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/d7a31afad4c02f2c99fcce1c6e3d6e07b422bb73))
202 | * add continuous integration chapter ([afb7f42](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/afb7f42d3d27bf3bb4d63610cd96e61518c781da))
203 | * add data validation chapter ([75cf453](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/75cf45321999b3616e191c7ae8159756d7d743e8))
204 | * add ddd intro ([7d40751](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/7d40751a892d4e6ab07956c05d616b419bf5504b))
205 | * add entities chapter ([9469c36](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/9469c36f117ddea6f8cc136339986551b3e5f93a))
206 | * add fp chapter ([11aaacf](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/11aaacfe5287cd1f511f2437864ede4a6c5db6de))
207 | * add git flow image ([808ed42](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/808ed42674d7ae3440c8e723ccfb8d80c9dc438b))
208 | * add http module to health module ([7105746](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/7105746cc0ff626ca9640e8fea7115b559b4e689))
209 | * add instructions to readme ([c73a7ec](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/c73a7ec767aae053379ecfd5e05f0103d5141b07))
210 | * add link to github repo ([7dbe320](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/7dbe3209f8d5d996bd48f6e4e43404468b3de909))
211 | * add nest logo ([0dc5e7e](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/0dc5e7eac6680996adbd9a23c0ac2a328bbb89c2))
212 | * add package-lock.json ([31085f3](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/31085f3b768224977a146d03087a93fbdbefee66))
213 | * add repo badges ([b8c1e42](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/b8c1e42d7326d1cd6427378b0731c95cd7a264eb))
214 | * add semantic release config file ([f6fb87b](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/f6fb87bfb9f804f34c14c412bbd313b836193917))
215 | * add some interfaces ([4926cc8](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/4926cc8ff4c1279bc0899ca096cde51b0d6d417d))
216 | * add some types ([8f6c854](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/8f6c854a5c5ee5f219b57e689ebb97e036194754))
217 | * add some utility types ([c6714ad](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/c6714adef75289f5e9bc9f63853fc776ae218610))
218 | * add some utility types ([95b53b8](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/95b53b85f1bce047751ecf982fe87c61c1d23837))
219 | * add stack logo ([543ad68](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/543ad68ce3e2d35eef9dd8db78b6adcf15f38a6f))
220 | * add table of contents ([dae69d3](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/dae69d396067d1b982ca622410293d8cec588b42))
221 | * add testing chapter ([8d9da65](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/8d9da654d43f00f55dd967876b869ecd30d2b5a4))
222 | * add workflow organization chapter ([5d47fd7](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/5d47fd744f4338b73ea04f90c646df8a92053519))
223 | * center images ([c6576e8](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/c6576e8e45fe6e42b91482f57ad063de73afefc7))
224 | * change clean architecture image ([ec337c9](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/ec337c97a88dd79bc797b9ab5179f54b2435b459))
225 | * change image size ([e4d17b0](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/e4d17b0827285d472568479f07d6af83ca56f528))
226 | * create .env sample file ([924c487](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/924c487c9e91ba5681c4b44618241b6dd3b8febd))
227 | * create architecture section ([fce89cc](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/fce89cca5b24d4a3a99198e6876bb2cfd9ab6867))
228 | * create docker-compose file for development database ([790596e](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/790596e6b9cc876ae1f9f553e6899ed681937130))
229 | * create health-check module ([59727ea](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/59727ea1c069fbb272bc59912c1af6e54f12b3fd))
230 | * create project and add lefthook ([d3b2912](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/d3b2912f336842515417d8159f39b55de10ea7aa))
231 | * create stack table ([37e4a68](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/37e4a68114751eb75b740a4ef6a18827431a552e))
232 | * create user entity and update dependencies ([8b6479e](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/8b6479ef0ebe785264ea9d4ee4bae3229d3eea9d))
233 | * delete clean-architecture.png ([9685e73](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/9685e73941dfc597badd39081850dfdddf5fcffe))
234 | * delete docs directory ([98b566d](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/98b566dcc4dece894d424562b91e75f8e951e5cf))
235 | * delete nest sample classes ([f6b67df](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/f6b67dff462441aaa14a31dded3be0587c983846))
236 | * finish clean architecture chapter ([ee33f81](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/ee33f8159bc00c1107419877ef09ba72355d4d33))
237 | * fix caption ([0a17466](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/0a1746626229fdc25bb18cce2e335029a230b76f))
238 | * **lib:** add some utility classes ([ba37417](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/ba374176ce9c878247bf3075ecbef22ca85f5429))
239 | * move semantic release config file to root dir ([cc77ab9](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/cc77ab9f6975198158f947ef6e82a431756db45a))
240 | * remove useless axiso dependency ([088cbbb](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/088cbbbd09f39a00ff902578c74ccf21ebc096a3))
241 | * rename semantic release config file ([15a6446](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/15a6446f94e7f7e97c7905158a8a7e5e1fbf7b6e))
242 | * set node project as private ([75ac0f5](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/75ac0f5a7174d86e190109dcf190ea8c4ad53267))
243 | * **tests:** update health test ([80f2c97](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/80f2c97e03d06dc3f6228830bdd6a921cd897667))
244 | * update app.module.ts ([149691b](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/149691be25ba898c8645c5ab690850ddcc8463f2))
245 | * update ddd chapter ([50cef2e](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/50cef2ef2fea439ccee482a738a16a48f19a2be8))
246 | * update doc ([62a80d3](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/62a80d348a0bd8139fe081287ae4646a1b387930))
247 | * update effect code image ([4184859](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/4184859c5d86f2f1bf3460959ec755b79d68ef2c))
248 | * update effect image ([a5a1e29](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/a5a1e29f67b4c73c0f1246cb588ff30ea4767062))
249 | * update gitignore ([972269f](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/972269fbd7027151f5e8b3a7b2e3a9fbe8f11bcb))
250 | * update hook script ([df4ace3](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/df4ace3438bdb45c5c35628ee4bad510065c45ee))
251 | * update image ([63d72e7](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/63d72e750c9c0e156302320c689d29955578218c))
252 | * update image ([a72f7a1](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/a72f7a10c37f3c6f7aab1ae25b4039da72955c0a))
253 | * update image size ([bf676e5](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/bf676e58c0b6095f3be523761909248aea5e59b6))
254 | * update images path ([c7925ca](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/c7925caf81e5e84ef38dd47c00447cb9faeb4a06))
255 | * update main.ts file ([2fd45e0](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/2fd45e03c488951f71c271240d6d15e2f8715814))
256 | * update mikro-orm migration path ([3da6f20](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/3da6f20916f053f08a89b9099d08ac8fb09d3f32))
257 | * update prettier configuration ([cfc0d45](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/cfc0d457cfc6c78a7c40cf2984cbd67eb3676216))
258 | * update project name ([42a1fc6](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/42a1fc6cc5c697d2c07e97fc5744afb09bbe26b6))
259 | * update readme ([0431914](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/0431914070647124098a602949d7af29cf2f6fbf))
260 | * update readme ([88a3bc7](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/88a3bc7c36b699a2458446180ce9712b95468e85))
261 | * update README ([33146f5](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/33146f5a169826a5dd34fdf7211a291a88c23031))
262 | * update README.md ([34ce4c6](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/34ce4c6807fd2d458fa4ccd87ad71e6d3d5a9054))
263 | * update README.md ([228c7e3](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/228c7e3f03251bdfafec4aa77c1cae1726453f17))
264 | * update README.md ([d85b7a5](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/d85b7a54b734a02b5d6980e9525723bcbb996515))
265 | * update README.md ([e8d06c6](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/e8d06c6ca33d40f1e8b8a8ff9cabe0cc96eaf693))
266 | * update README.md ([ff39416](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/ff3941685a42ac63625b1488980a852b380e885a))
267 | * update README.md ([79186c2](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/79186c2e1ecc9b38755a5a20a7e456192f5ca2ae))
268 | * update README.md ([4716ca4](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/4716ca4647d39a376b1bd260b9e61d9b747a70c7))
269 | * update README.md ([dc1ad64](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/dc1ad64d776de9bd83b5dd082c7a93c03a11ba88))
270 | * update README.md ([ab7da88](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/ab7da8868e3ad39aeb25a65cad35b800032fc017))
271 | * update README.md ([f7d5a04](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/f7d5a04a7c6808fc72a841de7ee993e55e28505e))
272 | * update README.md ([11db044](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/11db044d4e16f69341bf7aa3d020c7d3269b6771))
273 | * update README.md ([64a8ac5](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/64a8ac5d13f1334760c0701ac91f6321eb28649f))
274 | * update README.md ([ea21486](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/ea214864ff01292d5325ce3acebc5feaeecd6800))
275 | * update README.md ([d8d6322](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/d8d632268074ffa53f80ec9a824bc21a18d2cd8d))
276 | * update README.md ([24b1e28](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/24b1e28a60e964b3f8b7cf0ceccd6d9e9f71129e))
277 | * update README.md ([fae8631](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/fae86311f27fd2f8abf917234729b4d0913675b0))
278 | * update README.md ([1f4039f](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/1f4039f06ae55f4ec7851a9f8b49c38aa7e5d524))
279 | * update README.md ([b9d6204](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/b9d6204135bea8679ec6f4dac91dfc89b4ae42c2))
280 | * update release.yml ([d490c69](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/d490c699a67cd3947dbb34f1799669a42a40618b))
281 | * update renovate config file ([ea4bec5](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/ea4bec540ab44b2883a6c2ec778bb7d6108d8253))
282 | * update renovate.json ([ee23db6](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/ee23db6c69ef5aa0957b5a02694c6b90349282b2))
283 | * update renovate.json ([66c4f8f](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/66c4f8f8ca94ee91faab0d5f6baaee2c11b853df))
284 | * update renovate.json ([0414dba](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/0414dbae6ec547faf2d7cebbc85b00cc76c5d812))
285 | * update renovate.json ([8c26eb0](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/8c26eb0659c35df9d98da342618a33ee2b5ab965))
286 | * update renovate.json ([77558dd](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/77558dd06c4cb3895e5f40d6cd5a14541d4ede5f))
287 | * update renovate.json ([dd40f95](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/dd40f953761da8fcebf219b224fdd340197faf98))
288 | * update renovate.json ([7279976](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/727997686c0e6083148bda5e1bc25c7e319fa9c5))
289 | * update renovate.json ([eeeec37](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/eeeec37aab8269731d4e035e8d6a2322d36157be))
290 | * update renovate.json ([6eb642c](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/6eb642c2a055915270b3fe467c716099ce8bdf52))
291 | * update renovate.json ([173849d](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/173849d1cd63007c120ad4447b33840c2a60145e))
292 | * update renovate.json ([6677e22](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/6677e22a00f6f330142560cd28f9455a003c752c))
293 | * upload continuous integration image ([6afef18](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/6afef18e358dcd6471eaa27cbd7c2a79d274d1d2))
294 | * upload effect code image ([b49532a](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/b49532aceca906218253cf6a6d2c4f0cf931f954))
295 | * upload image ([021799f](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/021799f3918f2a5443f92e7866c26aa7e030cf9b))
296 | * use image absolute path ([1e58c1b](https://github.com/andrea-acampora/nestjs-ddd-devops/commit/1e58c1bb79268c85e1ab5f9b54d94ca98e64c501))
297 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:22-alpine
2 |
3 | WORKDIR /app
4 |
5 | COPY package*.json ./
6 |
7 | RUN npm ci --ignore-scripts
8 |
9 | COPY . .
10 |
11 | ENV NODE_ENV=production
12 |
13 | RUN npm run build
14 |
15 | EXPOSE 3000
16 |
17 | CMD ["sh", "-c", "npm run schema:update && npm run migrate:up && npm run start:prod"]
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Andrea Acampora
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # NestJS-DDD-DevOps
2 |
3 | [](https://github.com/andrea-acampora/nestjs-ddd-devops/actions/workflows/build.yml)
4 | [](https://github.com/andrea-acampora/nestjs-ddd-devops/actions/workflows/release.yml)
5 | [](https://github.com/andrea-acampora/nestjs-ddd-devops/actions/workflows/delivery.yml)
6 |
7 | [](https://github.com/andrea-acampora/nestjs-ddd-devops/actions/workflows/pages/pages-build-deployment)
8 | 
9 |
10 | [](https://github.com/andrea-acampora/nestjs-ddd-devops/blob/main/LICENSE)
11 | [](https://github.com/semantic-release/semantic-release/tree/master)
12 | [](https://conventionalcommits.org)
13 |
14 | []()
15 | [](https://github.com/andrea-acampora/nestjs-ddd-devops/issues)
16 | [](https://github.com/andrea-acampora/nestjs-ddd-devops/stargazers)
17 | [](https://github.com/andrea-acampora/nestjs-ddd-devops/graphs/contributors)
18 |
19 | The purpose of this [repository](https://github.com/andrea-acampora/nestjs-ddd-devops) is to create a ready-to-use project following _Domain-Driven Design_, _Clean
20 | Architecture_ and _Functional Programming_ best practices combined with some _DevOps_ techniques such as _Continuous
21 | Integration_, _Continuous Delivery_ and _Quality Assurance_.
22 |
23 | **Key Features**:
24 | - **Modular Monolith Architecture** with clear domain boundaries
25 | - **Test-Driven Development** with _Jest_ and _Supertest_
26 | - **Automated CI/CD** via _GitHub Actions_
27 | - **Semantic Versioning** & **Conventional Commits**
28 | - **Rate Limiting**, **Caching**, **Data Validation** and **API Versioning**
29 | - **Dockerized environment** with _PostgreSQL_
30 |
31 | The project is completely open source under the **MIT** license, feel free to contribute by opening
32 | an [issue](https://github.com/andrea-acampora/nestjs-ddd-devops/issues/new/choose),
33 | a [pull request](https://github.com/andrea-acampora/nestjs-ddd-devops/compare) or
34 | a [discussion topic](https://github.com/andrea-acampora/nestjs-ddd-devops/discussions/new/choose).
35 |
36 | In the following chapters you will find a description of the main choices, technologies and techniques adopted.
37 |
38 | **DISCLAIMER**: This page is not an article about _Domain-Driven Design_ or _Clean Architecture_: the sole purpose of this page is to explain some of the principles and techniques used in this project. For some of the chapters there is an introduction and a basic explanation to provide all the elements necessary to understand the choices made.
39 |
40 | ## Stack
41 |
42 | | NodeJS | TypeScript | NestJS | PostgreSQL | Mikro-ORM | Docker |
43 | | :---: | :----: | :---: | :---: | :----: | :---: |
44 | | [ ](https://nodejs.org/en) | [ ](https://www.typescriptlang.org) | [ ](https://nestjs.com) | [ ](https://www.postgresql.org) | [ ](https://mikro-orm.io) | [ ](https://www.docker.com) |
45 |
46 | ## Instructions
47 | 1. Fork this repository and use it as ```template``` repository
48 | 2. Install all dependencies
49 | ```bash
50 | npm install
51 | ```
52 | 3. Start the _PostgreSQL_ development database in a local container
53 | ```bash
54 | docker-compose up -d
55 | ```
56 | 4. Provide a ```.env``` and ```.env.test``` files with all required environment variables _(check out ```.env.dist``` example file)_
57 | 5. Create and generate the database schema from your entities' metadata:
58 | ```bash
59 | npm run schema:update
60 | ```
61 | 6. Start to create your modules and entities following all the principles explained in the below chapters!
62 |
63 | ## Table of Contents
64 |
65 | - [Architecture](#architecture)
66 | - [Domain-Driven Design](#domain-driven-design)
67 | - [Strategic Design](#strategic-design)
68 | - [Tactical Design](#tactical-design)
69 | - [Clean Architecture](#clean-architecture)
70 | - [Testing](#testing)
71 | - [GraphQL](#graphql)
72 | - [Functional Programming](#functional-programming)
73 | - [Workflow Organization](#workflow-organization)
74 | - [Semantic Versioning](#semantic-versioning)
75 | - [Continuous Integration](#continuous-integration)
76 | - [Continuous Delivery](#continuous-delivery)
77 | - [Automatic Dependency Update](#automatic-dependency-update)
78 | - [Backend Best Practices](#backend-best-practices)
79 | - [Caching](#caching)
80 | - [Data Validation](#data-validation)
81 | - [Rate Limiting](#rate-limiting)
82 | - [API Versioning](#api-versioning)
83 |
84 | ### Architecture
85 | [NestJS](https://docs.nestjs.com/) provides a modular architecture that allows the creation of loosely coupled and easily testable components. \
86 | Although this framework natively supports the development of microservice or event-driven architectures, they will not
87 | be considered because the purpose of this project is just to create a simple, extensible and ready-to-use application. \
88 | For this reason, we will implement a **Modular Monolith**: an architectural pattern that structures the
89 | application into independent modules or components with well-defined boundaries.
90 |
91 |
92 |
93 | Example of a Modular Monolith Architecture.
94 |
95 |
96 | In addition to simplicity and extensibility, a modular monolith allows us to start the development of the application as
97 | a single repository and deployment unit, with distinct and clear boundaries between business contexts.
98 | By this way, we can gradually refactor our architecture to a microservice architecture rather than implementing it from
99 | the beginning. \
100 | In [NestJS](https://docs.nestjs.com/), applications typically consists of multiple modules, each serving a specific purpose or feature set.
101 | A module is a class annotated with the `@Module()` decorator, and it encapsulates a specific domain or feature of the
102 | application. A module class define providers and inject them into other components leveraging **Dependency Injection**.
103 |
104 | ---
105 |
106 | ### Domain-Driven Design
107 | _Domain-Driven Design (DDD)_, introduced by _Eric Evans_ in his seminal book [Domain-Driven Design: Tackling Complexity in the Heart of Software](https://www.amazon.com/Domain-Driven-Design-Tackling-Complexity-Software/dp/0321125215), is an approach to software development that focuses on modeling software to match the complex realities of the business domain. It emphasizes collaboration between domain experts and software developers to build a shared understanding of the problem domain and to reflect that understanding in the code.
108 |
109 | DDD is structured into two main aspects:
110 | - **Strategic Design**: focuses on the high-level design of the system, defining boundaries and relationships between different parts of the domain.
111 | - **Tactical Design**: deals with patterns and building blocks that guide the implementation within the defined boundaries.
112 |
113 | #### Strategic Design
114 | Strategic design provides a big-picture approach to defining how different subdomains interact and how to partition a system into well-defined parts.
115 | On this page we will not cover the _Problem Space_, which includes, for example, the identification of subdomains, but we will discuss about how to manage and implement the various _Bounded Contexts_ designed.
116 |
117 | A _Bounded Context_ defines the explicit boundaries in which a particular domain model is defined and applied. Each context has its own domain logic, rules, and language, preventing ambiguity and inconsistencies when working with other contexts. It helps in maintaining clarity and separation of concerns within complex systems.
118 |
119 |
120 |
121 | Schema of Bounded Context Anatomy.
122 |
123 |
124 | In a [NestJS](https://docs.nestjs.com/) project, every bounded context can be implemented as a separate module.\
125 | Each module encapsulates its own domain logic, application services, and infrastructure concerns, ensuring clear separation and maintainability. For this reason, each module's name should reflect an important concept from the Domain and have its own folder with a dedicated codebase (`src/modules`). \
126 | This approach ensures [loose coupling](https://en.wikipedia.org/wiki/Loose_coupling): refactoring of a module internals can be done easier because outside world only depends on module's public interface, and if bounded contexts are defined and designed properly each module can be easily separated into a microservice if needed without touching any domain logic or major refactoring.
127 |
128 | To ensure [modularity](https://www.geeksforgeeks.org/modularity-and-its-properties/) and [maintainability](https://en.wikipedia.org/wiki/Maintainability), we have to make each module self-contained and minimize interactions between them. It's very important to treat each module as an independent mini-application that encapsulates a single business context and to avoid direct imports between modules to prevent [tight coupling](https://en.wikipedia.org/wiki/Coupling_(computer_programming)). This practice helps maintain separation of concerns, reduces dependencies, and prevents the code base from becoming a tangled and unmanageable structure.
129 |
130 | One of the most common factors that leads to the creation of `dependencies` and `tight coupling` between different bounded contexts is definitely the way they communicate. \
131 | There are several ways to facilitate communication between modules while maintaining loose coupling:
132 | 1. **Event-Based Communication**: [NestJS](https://docs.nestjs.com/) provides the `@nestjs/event-emitter` package to facilitate communication between modules via [Domain Events](#domain-events).
133 | Modules can publish domain events using the `Event Emitter` class, allowing other modules to subscribe and react to changes asynchronously.
134 | 2. **Dependency Injection**: modules can inject services from other modules by importing them explicitly in the module definition, ensuring proper encapsulation.
135 | ```typescript
136 | @Module({
137 | imports: [UsersModule],
138 | providers: [OrdersService],
139 | })
140 | export class OrdersModule {}
141 | ```
142 | 3. **Shared Service**: a shared module can be created to hold common logic and utilities needed across multiple bounded contexts.
143 | 4. **CQRS Pattern**: using the `@nestjs/cqrs package`, commands and queries can be dispatched to other modules following a clear separation of concern.
144 |
145 | #### Tactical Design
146 | Tactical design is a set of design patterns and building blocks that we can use in the construction of our Domain Model.\
147 | These building blocks are built around the _OOP_ and _FP_ techniques and their role is to help to manage complexity and ensure clarity behavior within the domain model.
148 |
149 | **Entities**
150 |
151 | Entities represent domain objects that have a distinct `identity` and `lifecycle`. Unlike value objects, which are defined solely by their attributes, entities are primarily distinguished by their identity, which remains consistent over time despite changes to their attributes.
152 | Entities should be behavior-oriented, and they should expose expressive methods that communicate domain behaviors instead of exposing state.
153 | [NestJS](https://docs.nestjs.com/) provides support for entities through its integration with _Object-Relational Mapping (ORM)_ tools such as [Mikro-ORM](https://mikro-orm.io/), [TypeORM](https://typeorm.io/) and [Prisma](https://www.prisma.io/). While these tools help with persistence, it is essential to align [NestJS](https://docs.nestjs.com/) entities with DDD principles to ensure a clear separation of concerns.
154 |
155 |
156 | There are two primary strategies to consider when integrating [NestJS](https://docs.nestjs.com/) entities with _DDD_:
157 |
158 | 1. Separating `Persistence Entities` from `Domain Entities`: with this approach, the domain model is kept clean and independent of the persistence layer. Persistence entities are used strictly for database operations, while domain entities encapsulate business logic. The pros of this approach are: a clear separation of concerns, an improved maintainability and testability and finally domain entities remain persistence-agnostic. The main drawback is that it introduces additional complexity due to the need for mapping between domain and persistence entities.
159 |
160 | 2. Using `Persistence Entities` as `Domain Entities`: this approach consolidates domain and persistence concerns into a single model, leveraging persistence entities to contain both database and domain logic. The benefit of this approach is the simplicity of the codebase by reducing duplication while the drawback is the coupling between domain logic and persistence concerns.
161 |
162 | **Value Objects**
163 |
164 | A value object is an immutable type that is defined by its properties rather than a unique identity.
165 | Unlike entities, which are distinguished by their identities, value objects are distinguished by their attributes. Two value objects are considered equal if all their attributes are the same. \
166 | Examples of value objects are things like numbers, dates, monies and strings.
167 |
168 | ```typescript
169 | export abstract class ValueObject {
170 | protected readonly props: T;
171 |
172 | constructor(props: T) {
173 | this.props = Object.freeze(props);
174 | }
175 |
176 | equals(other?: ValueObject): boolean {
177 | if (other === null || other === undefined)
178 | return false;
179 | return JSON.stringify(this.props) === JSON.stringify(other.props);
180 | }
181 | }
182 | ```
183 |
184 | **Repositories**
185 |
186 | The _Repository_ pattern is a design principle that abstracts data access logic behind a set of interfaces, separating the business logic from direct interactions with data sources, such as databases.
187 | It centralizes data access operations, making code more maintainable, testable, and flexible. \
188 | To reduce dependencies and coupling between layers, interfaces are defined in the `domain layer`, but the implementation, which interacts with the database, resides outside in the `infrastructure layer`.
189 |
190 | ```typescript
191 |
192 | export interface Repository {
193 | findById(id: number): Promise>;
194 |
195 | findAll(): Promise>;
196 |
197 | save(entity: T): Promise;
198 |
199 | update(data: Partial): Promise;
200 |
201 | delete(id: number): Promise;
202 | }
203 |
204 | ```
205 |
206 | **Domain Services**
207 |
208 | a _Domain Service_ is a stateless, operation-centric class that encapsulates domain logic or operations that don't naturally fit within the boundaries of an _Entity_ or a _Value Object_.
209 | These services are all about performing actions or providing domain-specific functionality that doesn't belong to a single entity.
210 |
211 | ```typescript
212 |
213 | export interface DomainService {
214 | execute(): Promise;
215 | }
216 |
217 | ```
218 |
219 | **Application Services**
220 |
221 | An _Application Service_ is a service that represents a use case or operation in the application.
222 | It is typically implemented as a class that contains the application-specific business logic for performing a specific operation.
223 |
224 | ```typescript
225 |
226 | export interface ApplicationService {
227 | execute(input: I): Promise;
228 | }
229 |
230 | ```
231 |
232 | While both of `application services` and `domain services` implement the business rules, there are fundamental logical and formal differences.
233 | - `application services` implement the use cases of the application, while `domain services` implement the core domain logic.
234 | - `application services` return *Data Transfer Objects* while `domain services` methods typically get and return the domain objects (entities, value objects).
235 | - `domain services` are typically used by the `application services` or other `domain services`, while `application services` are used by the *Presentation Layer* or *Client Applications*.
236 |
237 |
238 | **Domain Events**
239 |
240 | Domain events are events that occur in a specific area or domain and are important for the business logic of an application.\
241 | In contrast to `integration events`, which can affect the entire application, domain events are closely linked to the specific domain or specialist area of your application.\
242 | Using domain events improves the modularity of an application, as individual components are loosely coupled and can work independently of each other.
243 |
244 | ```typescript
245 |
246 | export abstract class DomainEvent {
247 |
248 | public readonly eventId: string;
249 |
250 | public readonly name: string;
251 |
252 | public readonly timeStamp: Date;
253 |
254 | public readonly payload: T;
255 |
256 | public readonly correlationId?: string;
257 |
258 | public readonly version: number;
259 | }
260 |
261 | ```
262 |
263 | **CQRS**
264 |
265 | *Command Query Responsibility Segregation (CQRS)* is a powerful architectural pattern used to separate the read and write sides of an application.\
266 | In CQRS, we have two distinct models: a `command model` that handles the commands that modify the state, and a `query model` that handles the queries that read the state.\
267 | The `command` model is usually implemented with an event-sourced aggregate, which is an entity that stores its state as a sequence of domain events.\
268 | The `query ` model is usually implemented with a projection, which is a denormalized view of the state that is updated by subscribing to the domain events.\
269 | By using domain events, we can decouple the command and query models, and optimize them for different purposes.
270 |
271 | In this project we'll use the `@nestjs/cqrs` package to implement the CQRS pattern.
272 |
273 | If you want to deep dive and to understand in detail how this library works, please refer to the official [documentation](https://docs.nestjs.com/recipes/cqrs).
274 |
275 |
276 | ### Clean Architecture
277 | Once the various bounded contexts have been identified and designed, it is necessary to proceed with the internal design of each module. \
278 | In this context, we will be helped by the principles of **Clean Architecture**, defined by _Robert C. Martin_ in this [article](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html).
279 |
280 | > Clean architecture is a software design philosophy that separates the elements of a design into ring levels. An important goal of clean architecture is to provide developers with a way to organize code in such a way that it encapsulates the business logic but keeps it separate from the delivery mechanism.
281 |
282 | This architecture attempts to integrate some of the leading modern architecture like [Hexagonal Architecture](https://en.wikipedia.org/wiki/Hexagonal_architecture_(software)), [Onion Architecture](http://jeffreypalermo.com/blog/the-onion-architecture-part-1/), [Screaming Architecture](https://blog.cleancoder.com/uncle-bob/2011/09/30/Screaming-Architecture.html) into one main architecture. \
283 | [NestJS](https://docs.nestjs.com/), with its modular structure and robust features, provides an excellent foundation for applying Clean Architecture principles.
284 | Since each module corresponds to a different _Bounded Context_, we are going to apply these principles within each module of the application.
285 |
286 |
287 |
288 |
289 | Different layers of the Clean Architecture.
290 |
291 |
292 | In this application, we are going to use these Clean Architecture layers:
293 | - **Entity Layer**: contains all domain elements. It is the central, most stable and therefore least volatile layer of any module, and the concepts defined within it are completely independent of anything defined in the external layers, resulting in decoupling from the technologies and libraries used.
294 | - **Use Case Layer**: contains all the use cases of the system. They use only the domain concepts defined in the innermost Entity layer acting as orchestrators of entities encapsulating business policies. They thus allow the details of domain elements to be abstracted behind a coarse-grained API that reflects the system's use cases. This allows unit-testing of system use cases without having dependencies on the infrastructure.
295 | - **Application Layer**: contains the controllers and presenters. The former handle the orchestration of the application flow by managing the interaction between external actors and the business policies defined in the core. They therefore do not represent domain concepts let alone define business rules. The second ones deal with serialization and deserialization, then presentation, of data to the infrastructure layer or use case layer, thus adapting the data to the most convenient format for the layers involved.
296 | - **Infrastructure Layer**: contains all the technological choices of the system. They are confined to the outermost layer because they are more volatile thus allowing everything defined in the innermost layers to remain valid in the face of technological changes, providing more flexibility to the system.
297 |
298 | In cases where inner layers must interact with abstractions defined in upper layers, as defined in the Clean Architecture [article](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html), the principle of **Dependency Inversion** (DIP) is exploited to make dependencies go only inward. Whenever this occurs, an interface is defined in the inner layer that is then implemented in the outer layer. In this way, dependencies remain only inward, without depending on concepts defined in the outer layers.
299 |
300 | Accordingly, each module of the application will have the following directory structure:
301 | ```md
302 | .
303 | └── src
304 | ├── app.module.ts
305 | ├── main.ts
306 | ├── config
307 | ├── lib
308 | └── modules
309 | └── module-x
310 | ├── domain
311 | ├── usecase
312 | ├── application
313 | └── infrastructure
314 | └── module-y
315 | ├── domain
316 | ├── usecase
317 | ├── application
318 | └── infrastructure
319 | ```
320 |
321 | ----
322 |
323 | ### Testing
324 | Testing is an important process in the software development lifecycle. It involves verifying and validating that a software application is free of bugs, meets the technical requirements set by its design and development, and satisfies user requirements efficiently and effectively.
325 |
326 | In this project we will implement two different types of tests:
327 |
328 | - **Unit Tests**: unit tests are very low level and close to the source of an application. They consist in testing individual methods and functions of the classes, components, or modules used by your software. They are generally quite cheap to automate and can run very quickly by a continuous integration server.
329 |
330 | - **End-to-end Tests**: end-to-end testing replicates a user behavior with the software in a complete application environment. It verifies that various user flows work as expected and can be as simple as loading a web page or logging in or much more complex scenarios verifying email notifications, online payments, etc...
331 |
332 | To automate the execution of the tests we will run them inside a _Continuous Integration_ pipeline, which will be explained in the next chapters.
333 |
334 | **Unit Testing**
335 |
336 | In this project, to implement Unit Tests we will use the following packages:
337 |
338 | - `@nestjs/testing`: provides a set of utilities that enable a more robust testing process.
339 | - `jest`: serves as a test-runner and also provides assert functions and test-double utilities that help with mocking, spying, etc.
340 |
341 | In the following code block it's possible to see an example of unit testing with [Jest](https://github.com/facebook/jest) library:
342 |
343 | ```typescript
344 | describe('CatsController', () => {
345 | let catsController: CatsController;
346 | let catsService: CatsService;
347 |
348 | beforeEach(() => {
349 | catsService = new CatsService();
350 | catsController = new CatsController(catsService);
351 | });
352 |
353 | describe('findAll', () => {
354 | it('should return an array of cats', async () => {
355 | const result = ['test'];
356 | jest.spyOn(catsService, 'findAll').mockImplementation(() => result);
357 | expect(await catsController.findAll()).toBe(result);
358 | });
359 | });
360 | });
361 | ```
362 |
363 | _Unit tests_ will be run with this command:
364 |
365 | ```bash
366 |
367 | npm run test
368 |
369 | ```
370 |
371 | **End-to-end Testing**
372 |
373 | In this project, to implement E2E Tests we will use the following packages:
374 |
375 | - `@nestjs/testing`: provides a set of utilities that enable a more robust testing process.
376 | - `supertest`: serves as a test-runner and also provides assert functions and test-double utilities that help with mocking, spying, etc.
377 |
378 | In the following code block it's possible to see an example of end-to-end testing with [Supertest](https://github.com/visionmedia/supertest) library:
379 |
380 | ```typescript
381 | describe('HealthCheck (E2E)', () => {
382 | let app: INestApplication;
383 |
384 | beforeEach(async () => {
385 | const moduleFixture: TestingModule = await Test.createTestingModule({
386 | imports: [AppModule],
387 | }).compile();
388 | app = moduleFixture.createNestApplication();
389 | await app.init();
390 | });
391 |
392 | it('should always return 200', () => {
393 | return request(app.getHttpServer()).get('/health').expect(200);
394 | });
395 | });
396 | ```
397 |
398 | _E2E tests_ will be run with this command:
399 |
400 | ```bash
401 |
402 | npm run test:e2e
403 |
404 | ```
405 |
406 | ---
407 |
408 | ### GraphQL
409 |
410 |
411 | GraphQL is a *query language* for APIs that allows clients to request only the data they need. Unlike REST, which relies on fixed endpoints and multiple requests, GraphQL consolidates requests into a single query, reducing network overhead and improving efficiency.
412 |
413 | When integrating GraphQL with NestJS, it is possible to define schemas using two approaches:
414 | 1. **Schema-First** (SDL-Based) \
415 | The Schema Definition Language (SDL) approach follows a traditional GraphQL method where the schema is written in a `.graphql` file and then mapped to resolvers in the application.
416 | 2. **Code-First** (Decorator-Based) \
417 | The Code-First approach uses TypeScript decorators to define GraphQL types and resolvers, which NestJS automatically converts into a GraphQL schema.
418 |
419 | In this project we will use the **Code-First** approach:
420 |
421 | ```typescript
422 | @ObjectType()
423 | export class UserModel {
424 | @Field(() => String)
425 | readonly id!: string;
426 |
427 | @Field({ nullable: true })
428 | readonly firstName?: string;
429 |
430 | @Field({ nullable: true })
431 | readonly lastName?: string;
432 |
433 | @Field(() => String)
434 | readonly email!: string;
435 |
436 | @Field(() => Date, { nullable: true })
437 | readonly createdAt?: Date;
438 | }
439 | ```
440 |
441 | At this point, we've defined the objects (type definitions) that can exist in our data graph, but clients don't yet have a way to interact with those objects.
442 | To address that, we need to create a resolver class. In the code first method, a resolver class both defines resolver functions and generates the Query type.
443 |
444 | ```typescript
445 | @Resolver(() => UserModel)
446 | export class UserResolver {
447 | constructor(
448 | private readonly queryBus: QueryBus,
449 | private readonly commandBus: CommandBus,
450 | ) {
451 | }
452 |
453 | @AuthRoles(ApiRole.ADMIN)
454 | @Query(() => UserModel, {})
455 | async getUser(
456 | @Args('id', { type: () => String }) id: string,
457 | ): Promise {
458 | return getOrThrowWith(
459 | map(await this.queryBus.execute(new GetUserByIdQuery(id)), toUserModel),
460 | () =>
461 | new GraphQLException('User Not Found', {
462 | extensions: {
463 | http: {
464 | status: HttpStatus.NOT_FOUND,
465 | },
466 | },
467 | }),
468 | );
469 | }
470 |
471 | }
472 |
473 | ```
474 |
475 | If you want to deep dive and to understand in detail how GraphQL works, please refer to the official [documentation](https://docs.nestjs.com/graphql/quick-start).
476 |
477 |
478 | ---
479 |
480 | ### Functional Programming
481 | In this section we are going to discuss and to explore some technical choices used in the development of this project related to functional programming.
482 |
483 | > Functional programming is a programming paradigm where programs are constructed by applying and composing functions. It is a declarative programming paradigm in which function definitions are trees of expressions that map values to other values, rather than a sequence of imperative statements which update the running state of the program.
484 |
485 | Since this is a web server using _Node.js_ and _TypeScript_, the project will not be fully functional like _Scala_ or _Haskell_ applications. \
486 | Instead, we will try to apply the following principles belonging to functional programming with the aim of improving the quality of our code:
487 |
488 | - **Immutability**: one of the biggest headaches in _JavaScript_ is dealing with state changes and unexpected side effects. With FP principles like immutability, we avoid accidental data mutations. Instead of modifying objects directly, we create new ones. This approach makes our app more predictable and easier to debug. _TypeScript_'s type system helps enforce immutability with tools like readonly and utility types, ensuring your data stay consistent.
489 |
490 | - **Pure Functions**: pure functions always return the same output for the same input and don't mess with the outside world. This predictability makes our code easier to test and reason about. With _TypeScript_, we get an added layer of security by defining precise input and output types.
491 |
492 | - **Higher-Order Functions**: higher-order functions (HOFs) let us write reusable and composable code. They can be used to create reusable abstractions that can simplify complex code and make it easier to understand.
493 |
494 | - **Type Safety**: with _TypeScript_, you catch mistakes before they become runtime issues. FP concepts align perfectly with _TypeScript_'s static typing, reducing the chances of passing around undefined or null by accident.
495 |
496 | - **Declarativity**: functional programming encourages writing code that focuses on what should happen rather than how it happens. This leads to cleaner, more readable code, which is easier for us to maintain.
497 |
498 | To implement and follow all of these FP principles we are going to use the [Effect-TS](https://github.com/Effect-TS/effect) library, which belongs to the [Effect](https://effect.website/) ecosystem. \
499 | The `effect-ts` library is a powerful tool for managing functional programming paradigms in a _Node.js_ and _TypeScript_ project. It provides a comprehensive set of utilities for handling side effects, asynchronous operations, and error management in a purely functional and type-safe manner.
500 | Its core abstractions, such as `Effect`, `Option`, and `Either`, allow developers to build complex applications while maintaining clarity and scalability. Whether handling HTTP requests, database interactions, or background tasks, `effect-ts` simplifies the process of structuring the logic in a way that is predictable, testable, and resilient to failure.
501 |
502 | In the following code snippet you can find an example of `effect-ts` library usage.
503 | ```typescript
504 | import { Option } from "effect"
505 |
506 | const computation = (): Option =>
507 | Math.random() < 0.5 ? some(10) : none()
508 |
509 | const alternativeComputation = (): Option =>
510 | Math.random() < 0.5 ? some(20) : none()
511 |
512 | const program = computation().pipe(
513 | Option.orElse(() => alternativeComputation())
514 | )
515 |
516 | const result = Option.match(program, {
517 | onNone: () => "Both computations resulted in None",
518 | onSome: (value) => `Computed value: ${value}`
519 | })
520 | ```
521 |
522 | ---
523 |
524 | ### Workflow Organization
525 | In order to make the best use of _DevOps_ practices, it is necessary to adopt an appropriate **Workflow Organization**. \
526 | In this project we are going to use a custom version of the [Gitflow Workflow]( https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow). \
527 | Instead of a single `main` branch, this workflow uses two branches to record the history of the project. The `main` branch stores the official release history, and the `develop` branch serves as an integration branch for features. It's also convenient to tag all commits in the main branch with a version number. Each new feature should reside in its own branch, which can be pushed to the central repository for backup/collaboration. But, instead of branching off of main, feature branches use develop as their parent branch. When a feature is complete, it gets merged back into develop. Features should never interact directly with main.
528 |
529 |
530 |
531 |
532 | Gitflow branch structure.
533 |
534 |
535 | The overall flow of **Gitflow** is:
536 |
537 | 1. A develop branch is created from main
538 | 2. Feature branches are created from develop
539 | 3. When a feature is complete it is merged into the develop branch
540 | 4. When we want to trigger a release the develop branch is merged into main
541 | 5. If an issue in main is detected a hotfix branch is created from main
542 | 6. Once the hotfix is complete it is merged to both develop and main
543 |
544 | In this project, we are also going to adopt a `rebase` policy instead of a `merge` policy to keep a cleaner and linear project history. \
545 | In addition, in order to make the meaning of commits more explicit, we are going to adopt the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification, which will simplify the use of automatic tools for application versioning. \
546 | In order to check the correct use of the _Conventional Commits_ specification and compliance with quality standards on the produced code, we are going to use the following git hooks:
547 |
548 | - `pre-commit`: verifies the compliance with quality standards on code using the project linter and running unit tests
549 | - `commit-msg`: verifies compliance with the _Conventional Commit_ specification
550 |
551 | To define and configure the hooks we are going to use the tool [Lefthook](https://lefthook.dev/), which will install the hooks during the `prepare` step of the node project.
552 |
553 | ---
554 |
555 | ### Semantic Versioning
556 | > Software versioning is the process of assigning either unique version names or unique version numbers to unique states of computer software. Within a given version number category (e.g., major or minor), these numbers are generally assigned in increasing order and correspond to new developments in the software.
557 |
558 | Regarding the versioning process, in this project we are going to follow the [Semantic Versioning](https://semver.org/lang/it/) specification. \
559 | According to _Semantic Versioning_, a version consists of three numbers: **Major**, **Minor**, and **Patch**. Each change to the source code results in an increase of one of these numbers based on the importance of the changes made. \
560 | Using the _Conventional Commit_ specification described earlier, it was possible to use the semantics of commits to understand when to make a new release and the importance of it.
561 | Accordingly, we are going to use the [Semantic-release-bot](https://github.com/semantic-release/semantic-release), which follows the _Semantic Versioning_ specification, to automate the software release process and changelog generation by analyzing commits in order to identify the correct version increment. For the type of release to be associated with each commit, we are going to use the `semantic-release-preconfigured-conventional-commits` configuration. The bot is triggered upon the push of a commit on the main branch, and if, upon analyzing the commits, a new release needs to be executed, the bot will take care of executing a new release on **GitHub Release**.
562 |
563 | ---
564 |
565 | ### Continuous Integration
566 | One of the fundamental practices of DevOps is _Continuous Integration_. It aims to continuously integrate code with the main line of development so that integration problems are detected early and software quality is improved by enabling a faster and more reliable development process.
567 |
568 |
569 |
570 | Pipeline of Continuous Integration and Delivery.
571 |
572 |
573 | In this project we are going to use [GitHub Actions](https://github.com/features/actions) to create and execute our CI workflows:
574 |
575 | - **Build**
576 | - **Release**
577 |
578 | The [**Build**](https://github.com/andrea-acampora/nestjs-ddd-devops/blob/main/.github/workflows/build.yml) workflow consists of running tests and code quality checks on all combinations of operating system and different versions of _Node.js_ in order to ensure proper performance on all platforms of interest.
579 | First, we are going to execute the `unit` tests and then, we are going to emulate a real scenario executing a _PostgreSQL_ database instance and running `end-to-end` tests to check the integrity of the application and to prevent regression errors. \
580 | The workflow is configured to run on pushes or pull request creation. In this way, it is possible to run the tests and provides the results of each test in the pull request, so you can see whether the change in your branch introduces an error. When all CI tests in the `build` workflow pass, the changes we pushed are ready to be reviewed by a team member or merged. When a test fails, one of our changes may have caused the failure.
581 |
582 | The [**Release**](https://github.com/andrea-acampora/nestjs-ddd-devops/blob/main/.github/workflows/release.yml) workflow is responsible for running the `semantic release bot` to manage the automatic release of new versions of the software. This workflow is executed only if you are on the `main` branch and if a build workflow has previously completed successfully.
583 |
584 | ---
585 |
586 | ### Continuous Delivery
587 | Continuous Delivery (CD) is a software development practice that enables teams to release new features, updates, and bug fixes to production environments rapidly, reliably, and sustainably. The primary goal of CD is to minimize the time between writing code and delivering it to users, while ensuring high quality and stability. \
588 | In this project, the **Continuous Delivery** workflow is built using **GitHub Actions** and **Docker**, and it runs on a _Continuous Integration_ environment. \
589 | The [workflow](https://github.com/andrea-acampora/nestjs-ddd-devops/blob/main/.github/workflows/delivery.yml) is realized in the following way:
590 |
591 | 1. **Automated Workflow with GitHub Actions**: the workflow is triggered automatically when a successful `Release` job is completed, ensuring only tested and verified code gets delivered. We use conditional execution to ensure that deployment only happens if the previous workflow (Release) succeeds.
592 | 2. **Versioning**: we extract version tags using `git describe --tags --abbrev=0`, making sure each _Docker_ image is tagged correctly. This approach makes rollback, tracking, and auditing deployments very easy.
593 | 3. **Docker Containerization**: we build the _Docker_ image of the application using a custom `Dockerfile`. The Dockerfile follows best practices by installing dependencies, running the build, and handling migrations and database schema creation on startup.
594 | 4. **Deployment to GitHub Container Registry (GHCR)**: we securely log in to GHCR using secrets, ensuring that credentials stay protected. Then we tag both `versioned` and `latest` container images to allows flexibility and rollback strategies.
595 |
596 | At the end of the workflow, if all the steps are successful, we can find the docker image of the application on [GitHub Packages](https://github.com/andrea-acampora?tab=packages&repo_name=nestjs-ddd-devops). \
597 | So, you can download it and run it in this way:
598 |
599 | ```bash
600 | docker run -p 3000:3000 --env-file .env ghcr.io/andrea-acampora/nestjs-ddd-devops:latest
601 | ```
602 |
603 | Remember that you need to provide a `.env` file with all database connection variables. Alternatively, you can create a `docker-compose` file with a _PostgreSQL_ service and a service containing the image you just created so that the app and database can communicate via an internal network.
604 |
605 | ---
606 |
607 | ### Automatic Dependency Update
608 | Keeping dependencies current is one of the most effective security methods available, since it prevents vulnerabilities from entering the code base at the outset. Updating dependencies is a complex task that takes time and often introduces technical debt.
609 | Especially in a complex dependency tree, it’s difficult to even know what libraries or packages are out of date. Manually looking for updates is time-consuming and unrewarding work. Moreover, updates may not always be compatible with existing code, and without total confidence in merging an update, developers worry that an update will break their app.
610 |
611 | In order to automate the update of the project `dependencies`, we are going to use the [Renovate](https://www.mend.io/renovate/) bot. \
612 | This bot will reduce risk, improve code quality, and cut technical debt by automatically ensuring all dependencies are kept up to date. To do this, the bot will open a new `pull request` on a dedicated `branch` every time it detects a dependency update. This will trigger the running of all Unit and E2E tests in Continuous Integration and if everything is fine then the PR will be automatically merged into the base branch.
613 |
614 | ---
615 |
616 | ### Backend Best Practices
617 | In this section we will discuss some common backend best practices that we will use in this project. Most of them are directly supported by [NestJS](https://docs.nestjs.com/) while others will need a custom implementation.
618 |
619 | ### Caching
620 | As reported in the offical [NestJS](https://docs.nestjs.com/) documentation, _Caching_ is a powerful and straightforward technique for enhancing application's performance. By acting as a temporary storage layer, it allows for quicker access to frequently used data, reducing the need to repeatedly fetch or compute the same information. This results in faster response times and improved overall efficiency. \
621 | In this project, we will use the `@nestjs/cache-manager` package along with the `cache-manager` package. By default, with these packages use a `in-memory` strategy so everything is stored in the memory of the application.
622 | In this way, if the project grows, it will be possible to use an advanced solution and a dedicated database such as [Redis](https://redis.io/) as it is fully supported by the `@nestjs/cache-manager`. \
623 | If you want to deep dive and to understand in detail how this tool works, please refer to the official [documentation](https://docs.nestjs.com/techniques/caching#caching).
624 |
625 | ### Data Validation
626 | Data validation is one of the most crucial steps in building a robust backend system ensuring data flowing through the system is accurate, consistent, and adheres to predefined formats. By introducing data validation invalid or malicious data gets filtered out before it can impact your system.\
627 | In this project, to implement data validation we will use `class-validator` and `class-transformer` packages.
628 |
629 | We start by binding `ValidationPipe` at the application level, thus ensuring all endpoints are protected from receiving incorrect data.
630 | ```typescript
631 | app.useGlobalPipes(
632 | new ValidationPipe({
633 | transform: true,
634 | whitelist: true,
635 | forbidNonWhitelisted: true,
636 | }),
637 | );
638 | ```
639 |
640 | The `whitelist` property is set to true to ensure that the validator will strip validated object of any properties that do not have any decorators. In this case, we can whitelist the acceptable properties, and any property not included in the whitelist is automatically stripped from the resulting object.\
641 | Alternatively, if we want to stop the request from processing when non-whitelisted properties are present, we have to set the `forbidNonWhitelisted` option property to true, in combination with setting whitelist to true.
642 | To enable auto-transformation of payloads to typed objects according to their DTO classes, we have to se the `transform` option property to true.
643 | Since _TypeScript_ does not store metadata about generics or interfaces, when you use them in your DTOs, ValidationPipe may not be able to properly validate incoming data. For this reason, consider using concrete classes in your DTOs.
644 |
645 | Once the _Validation Pipe_ is registered globally, we can start to add some validation rules to our dtos.
646 | ```typescript
647 | import { IsEmail, IsNotEmpty } from 'class-validator';
648 |
649 | export class CreateUserDto {
650 | @IsEmail()
651 | email: string;
652 |
653 | @IsNotEmpty()
654 | password: string;
655 | }
656 |
657 | ```
658 |
659 | If you want to deep dive and to understand in detail how this tool works, please refer to the official [documentation](https://docs.nestjs.com/techniques/validation).
660 |
661 | ### Rate Limiting
662 | Rate limiting is a set of measures put in place to help ensure the stability and performance of an API system. It works by setting limits on how many requests can be made within a certain period of time — usually a few seconds or minutes — and what actions can be taken.
663 | If too many requests are made over that period, the API system will return an error message telling you that the rate limit has been exceeded.
664 | Additionally, rate limiting can help prevent attacks that aim to overwhelm the system, such as DoS attacks, brute force attempts, and API abuse. and also can help businesses save on costs associated with managing an API system.
665 |
666 | In this project we will use the `@nestjs/throttler` package.
667 |
668 | ```typescript
669 | @Module({
670 | imports: [
671 | ThrottlerModule.forRoot([{
672 | name: '100_CALL_PER_MINUTE',
673 | ttl: 60000,
674 | limit: 100,
675 | }]),
676 | ],
677 | })
678 | export class AppModule {}
679 |
680 | ```
681 |
682 | With this configuration we set a maximum of 100 requests per IP address in a 60-seconds interval.
683 |
684 | If you want to deep dive and to understand in detail how this tool works, please refer to the official [documentation](https://docs.nestjs.com/security/rate-limiting).
685 |
686 | ### API Versioning
687 | API versioning is the practice of transparently managing changes to your API. You should version your API whenever you make a change that will require consumers to modify their codebase in order to continue using the API. This type of change is known as a “breaking change,” and it can be made to an API's input and output data structures, success and error feedback, and security mechanisms. \
688 | There are several approaches to API versioning, including:
689 |
690 | - **URI Versioning**
691 | - **Query Parameter Versioning**
692 | - **Header versioning**
693 | - **Consumer-based versioning**
694 |
695 | In this project we will enable _URI versioning_ globally, in the following way:
696 |
697 | ```typescript
698 |
699 | app.enableVersioning({
700 | type: VersioningType.URI,
701 | defaultVersion: '1',
702 | });
703 |
704 | ```
705 |
706 | The version in the URI will be automatically prefixed with `v` by default, however the prefix value can be configured by setting the prefix key to your desired prefix or false if you wish to disable it. \
707 | In addition to the global configuration, it is also possible to specify the version of individual routes or controllers. In this case, this version will override any other version that would affect the route.
708 |
709 | If you want to deep dive and to understand in detail how this tool works, please refer to the official [documentation](https://docs.nestjs.com/techniques/versioning).
710 |
711 | ---
712 |
713 | ## Contributors
714 |
715 |
716 |
717 |
718 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | # PostgreSQL development database
2 | services:
3 | postgresql-dev:
4 | container_name: postgresql-dev
5 | image: postgres:latest
6 | restart: unless-stopped
7 | ports:
8 | - "15432:5432/tcp"
9 | environment:
10 | - POSTGRES_DB=db-dev
11 | - POSTGRES_USER=postgres
12 | - POSTGRES_PASSWORD=postgres
13 | volumes:
14 | - ./postgresql_volume:/var/lib/postgresql/data:rw,Z
15 | stdin_open: true
16 | tty: true
17 | networks:
18 | - postgres
19 |
20 | postgresql-test-e2e:
21 | container_name: postgresql-test-e2e
22 | image: postgres:latest
23 | restart: unless-stopped
24 | ports:
25 | - "25432:5432/tcp"
26 | environment:
27 | - POSTGRES_DB=db-test-e2e
28 | - POSTGRES_USER=postgres
29 | - POSTGRES_PASSWORD=postgres
30 | stdin_open: true
31 | tty: true
32 | networks:
33 | - postgres
34 |
35 | volumes:
36 | postgresql_volume:
37 |
38 | networks:
39 | postgres:
40 | driver: bridge
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import typescriptEslintEslintPlugin from '@typescript-eslint/eslint-plugin';
2 | import tsParser from '@typescript-eslint/parser';
3 | import path from 'node:path';
4 | import { fileURLToPath } from 'node:url';
5 | import { FlatCompat } from '@eslint/eslintrc';
6 |
7 | const __filename = fileURLToPath(import.meta.url);
8 | const __dirname = path.dirname(__filename);
9 | const compat = new FlatCompat({
10 | baseDirectory: __dirname,
11 | });
12 |
13 | export default [
14 | ...compat.extends(
15 | 'plugin:@typescript-eslint/recommended',
16 | 'plugin:prettier/recommended',
17 | ),
18 | {
19 | plugins: {
20 | '@typescript-eslint': typescriptEslintEslintPlugin,
21 | },
22 | languageOptions: {
23 | parser: tsParser,
24 | ecmaVersion: 5,
25 | parserOptions: {
26 | project: 'tsconfig.json',
27 | },
28 | },
29 | },
30 | ];
31 |
--------------------------------------------------------------------------------
/lefthook.yml:
--------------------------------------------------------------------------------
1 | pre-commit:
2 | parallel: true
3 | jobs:
4 | - run: npm run lint
5 | glob: "*"
6 | - run: npm run test
7 | glob: "*"
8 | commit-msg:
9 | scripts:
10 | "commitlint.sh":
11 | runner: bash
--------------------------------------------------------------------------------
/nest-cli.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/nest-cli",
3 | "collection": "@nestjs/schematics",
4 | "sourceRoot": "src",
5 | "compilerOptions": {
6 | "deleteOutDir": true
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nestjs-ddd-devops",
3 | "version": "0.0.1",
4 | "description": "Template repository for NestJS projects following Domain-Driven Design, Clean Architecture and Functional Programming principles and best practice.",
5 | "author": "Andrea Acampora",
6 | "private": true,
7 | "license": "MIT",
8 | "homepage": "https://github.com/andrea-acampra/nestjs-ddd-devops",
9 | "scripts": {
10 | "prepare": "lefthook install",
11 | "build": "nest build",
12 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
13 | "start": "nest start",
14 | "start:dev": "nest start --watch",
15 | "start:debug": "nest start --debug --watch",
16 | "start:prod": "node dist/main",
17 | "lint": "eslint 'src/**/*.{ts,tsx}' --fix --no-ignore",
18 | "test": "jest --config test/config/jest-unit.config.ts --passWithNoTests --coverage",
19 | "test:watch": "jest --config test/config/jest-unit.config.ts --watch",
20 | "test:cov": "jest --config test/config/jest-unit.config.ts --coverage",
21 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
22 | "test:e2e": "jest --config test/config/jest-e2e.config.ts --detectOpenHandles --forceExit",
23 | "schema:update": "npm run build && npx mikro-orm schema:update -r --config src/config/database/mikro-orm.config.ts",
24 | "migrate:up": "npm run build && npx mikro-orm migration:up --config --config src/config/database/mikro-orm.config.ts",
25 | "migrate:down": "npm run build && npx mikro-orm migration:down --config --config src/config/database/mikro-orm.config.ts"
26 | },
27 | "dependencies": {
28 | "@apollo/server": "^4.11.3",
29 | "@as-integrations/fastify": "^2.1.1",
30 | "@eslint/eslintrc": "^3.2.0",
31 | "@fastify/compress": "^8.0.1",
32 | "@fastify/static": "^8.0.4",
33 | "@mikro-orm/core": "^6.4.3",
34 | "@mikro-orm/migrations": "^6.4.3",
35 | "@mikro-orm/nestjs": "^6.0.2",
36 | "@mikro-orm/postgresql": "^6.4.3",
37 | "@nestjs/apollo": "^13.0.3",
38 | "@nestjs/axios": "^4.0.0",
39 | "@nestjs/cache-manager": "^3.0.0",
40 | "@nestjs/common": "^11.0.0",
41 | "@nestjs/config": "^4.0.0",
42 | "@nestjs/core": "^11.0.0",
43 | "@nestjs/cqrs": "^11.0.2",
44 | "@nestjs/event-emitter": "^3.0.0",
45 | "@nestjs/graphql": "^13.0.3",
46 | "@nestjs/platform-express": "^11.0.0",
47 | "@nestjs/platform-fastify": "^11.0.0",
48 | "@nestjs/schedule": "^5.0.0",
49 | "@nestjs/terminus": "^11.0.0",
50 | "@nestjs/throttler": "^6.3.0",
51 | "bcryptjs": "^3.0.0",
52 | "class-transformer": "^0.5.1",
53 | "class-validator": "^0.14.1",
54 | "date-fns": "^4.1.0",
55 | "dotenv": "^16.4.7",
56 | "effect": "^3.12.4",
57 | "graphql": "^16.10.0",
58 | "jsonwebtoken": "^9.0.2",
59 | "reflect-metadata": "^0.2.2",
60 | "rxjs": "^7.8.1",
61 | "uuid": "^11.0.5"
62 | },
63 | "devDependencies": {
64 | "@mikro-orm/cli": "^6.4.3",
65 | "@nestjs/cli": "^11.0.0",
66 | "@nestjs/schematics": "^11.0.0",
67 | "@nestjs/testing": "^11.0.0",
68 | "@semantic-release/changelog": "^6.0.3",
69 | "@semantic-release/git": "^10.0.1",
70 | "@semantic-release/npm": "^12.0.1",
71 | "@types/bcryptjs": "^3.0.0",
72 | "@types/express": "^5.0.0",
73 | "@types/jest": "^29.5.14",
74 | "@types/jsonwebtoken": "^9.0.7",
75 | "@types/node": "^22.10.7",
76 | "@types/supertest": "^6.0.2",
77 | "@types/uuid": "^10.0.0",
78 | "@typescript-eslint/eslint-plugin": "^8.0.0-alpha.10",
79 | "@typescript-eslint/parser": "^8.0.0-alpha.10",
80 | "eslint": "^9.0.1",
81 | "eslint-config-prettier": "^10.0.0",
82 | "eslint-plugin-prettier": "^5.0.0",
83 | "jest": "^29.5.0",
84 | "lefthook": "^1.10.7",
85 | "prettier": "^3.4.2",
86 | "semantic-release-preconfigured-conventional-commits": "1.1.125",
87 | "source-map-support": "^0.5.21",
88 | "supertest": "^7.0.0",
89 | "ts-jest": "^29.1.0",
90 | "ts-loader": "^9.4.3",
91 | "ts-node": "^10.9.1",
92 | "tsconfig-paths": "^4.2.0",
93 | "typescript": "^5.7.3"
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/release.config.cjs:
--------------------------------------------------------------------------------
1 | const config = require('semantic-release-preconfigured-conventional-commits');
2 |
3 | const publishCommands = `
4 | git tag -a -f \${nextRelease.version} \${nextRelease.version} -F CHANGELOG.md || exit 2
5 | git push --force origin \${nextRelease.version} || exit 3
6 | echo "release_status=released" >> $GITHUB_ENV
7 | echo "CONTAINER_VERSION="\${nextRelease.version} >> $GITHUB_ENV
8 | `;
9 |
10 | const releaseBranches = ['main'];
11 | config.branches = releaseBranches;
12 |
13 | config.plugins.push(
14 | [
15 | '@semantic-release/exec',
16 | {
17 | publishCmd: publishCommands,
18 | },
19 | ],
20 | [
21 | '@semantic-release/github',
22 | {
23 | assets: [],
24 | },
25 | ],
26 | [
27 | '@semantic-release/git',
28 | {
29 | assets: ['CHANGELOG.md', 'package.json'],
30 | message: 'chore(release)!: [skip ci] ${nextRelease.version} released',
31 | },
32 | ],
33 | );
34 |
35 | module.exports = config;
36 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": [
4 | "config:recommended",
5 | ":rebaseStalePrs",
6 | ":semanticCommits",
7 | ":semanticCommitTypeAll(chore)"
8 | ],
9 | "packageRules": [
10 | {
11 | "matchUpdateTypes": [
12 | "minor",
13 | "patch",
14 | "pin",
15 | "digest"
16 | ],
17 | "automerge": true
18 | },
19 | {
20 | "matchDepTypes": [
21 | "dependencies",
22 | "devDependencies"
23 | ],
24 | "automerge": true
25 | }
26 | ],
27 | "baseBranches": ["develop"],
28 | "dependencyDashboard": true,
29 | "automerge": true,
30 | "automergeType": "pr",
31 | "prCreation": "immediate",
32 | "rebaseWhen": "auto",
33 | "platformAutomerge": true,
34 | "assignees": [
35 | "andrea-acampora"
36 | ],
37 | "reviewers": [
38 | "andrea-acampora"
39 | ],
40 | "includeForks": true,
41 | "labels": [
42 | "dependencies"
43 | ],
44 | "git-submodules": {
45 | "enabled": true
46 | },
47 | "prConcurrentLimit": 25,
48 | "prHourlyLimit": 0,
49 | "separateMajorMinor": true,
50 | "separateMinorPatch": true,
51 | "separateMultipleMajor": true
52 | }
53 |
--------------------------------------------------------------------------------
/src/app.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { HttpModule } from '@nestjs/axios';
3 | import { CacheModule } from '@nestjs/cache-manager';
4 | import { ConfigModule, ConfigService } from '@nestjs/config';
5 | import { MikroOrmModule } from '@mikro-orm/nestjs';
6 | import mikroOrmConfig from './config/database/mikro-orm.config';
7 | import { ThrottlerModule } from '@nestjs/throttler';
8 | import { EventEmitterModule } from '@nestjs/event-emitter';
9 | import { ScheduleModule } from '@nestjs/schedule';
10 | import { UserModule } from './modules/user/user.module';
11 | import { HealthModule } from './modules/health/health.module';
12 | import { AuthModule } from './modules/auth/auth.module';
13 | import { CqrsModule } from '@nestjs/cqrs';
14 | import { CommunicationModule } from './modules/communication/communication.module';
15 | import { GraphQLModule } from '@nestjs/graphql';
16 | import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
17 |
18 | @Module({
19 | imports: [
20 | HttpModule,
21 | CqrsModule.forRoot(),
22 | CacheModule.register({
23 | isGlobal: true,
24 | }),
25 | ConfigModule.forRoot({
26 | isGlobal: true,
27 | envFilePath: ['.env'],
28 | }),
29 | MikroOrmModule.forRootAsync({
30 | imports: [ConfigModule],
31 | inject: [ConfigService],
32 | useFactory: () => {
33 | return {
34 | ...mikroOrmConfig,
35 | allowGlobalContext: true,
36 | };
37 | },
38 | }),
39 | GraphQLModule.forRoot({
40 | driver: ApolloDriver,
41 | autoSchemaFile: true,
42 | }),
43 | ThrottlerModule.forRoot([
44 | {
45 | name: '100_CALL_PER_MINUTE',
46 | ttl: 60000,
47 | limit: 100,
48 | },
49 | ]),
50 | EventEmitterModule.forRoot(),
51 | ScheduleModule.forRoot(),
52 | HealthModule,
53 | UserModule,
54 | AuthModule,
55 | CommunicationModule,
56 | ],
57 | controllers: [],
58 | providers: [],
59 | })
60 | export class AppModule {}
61 |
--------------------------------------------------------------------------------
/src/config/database/migrations/create-admin-credentials.migration.ts:
--------------------------------------------------------------------------------
1 | import { Migration } from '@mikro-orm/migrations';
2 | import { genSalt, hash } from 'bcryptjs';
3 | import { v4 } from 'uuid';
4 |
5 | export class CreateAdminCredentialsMigration extends Migration {
6 | async up(): Promise {
7 | await this.getEntityManager()
8 | .getConnection()
9 | .execute(
10 | `INSERT INTO users (id, email, password, first_name, last_name, role, state, created_at, updated_at)
11 | VALUES (?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
12 | ON CONFLICT (email) DO NOTHING`,
13 | [
14 | v4(),
15 | 'admin@email.com',
16 | await hash('Test1234!', await genSalt()),
17 | 'Admin',
18 | 'Admin',
19 | 0,
20 | 'ACTIVE',
21 | ],
22 | );
23 | }
24 |
25 | async down(): Promise {
26 | await this.getEntityManager()
27 | .getConnection()
28 | .execute(`DELETE FROM users WHERE role = 0`);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/config/database/mikro-orm.config.ts:
--------------------------------------------------------------------------------
1 | import 'dotenv/config';
2 | import { defineConfig } from '@mikro-orm/postgresql';
3 | import { Migrator } from '@mikro-orm/migrations';
4 |
5 | export default defineConfig({
6 | entities: ['dist/**/*.entity.js'],
7 | entitiesTs: ['src/**/*.entity.ts'],
8 | dbName: process.env.DATABASE_NAME,
9 | host: process.env.DATABASE_HOST,
10 | port: Number(process.env.DATABASE_PORT),
11 | user: process.env.DATABASE_USER,
12 | password: process.env.DATABASE_PASSWORD,
13 | extensions: [Migrator],
14 | migrations: {
15 | tableName: 'migrations',
16 | path: 'src/config/database/migrations',
17 | },
18 | });
19 |
--------------------------------------------------------------------------------
/src/config/env/configuration.constant.ts:
--------------------------------------------------------------------------------
1 | export const JWT_SECRET = 'JWT_SECRET';
2 | export const JWT_REFRESH_SECRET = 'JWT_REFRESH_SECRET';
3 |
4 | export const JWT_EXPIRES_IN = 'JWT_EXPIRES_IN';
5 | export const JWT_REFRESH_EXPIRES_IN = 'JWT_REFRESH_EXPIRES_IN';
6 |
7 | export const PORT = 'PORT';
8 |
--------------------------------------------------------------------------------
/src/libs/api/api-role.enum.ts:
--------------------------------------------------------------------------------
1 | export enum ApiRole {
2 | ADMIN = 0,
3 | USER = 1,
4 | }
5 |
--------------------------------------------------------------------------------
/src/libs/api/graphql/paginated.type.ts:
--------------------------------------------------------------------------------
1 | import { Field, Int, ObjectType } from '@nestjs/graphql';
2 | import { Type } from '@nestjs/common';
3 |
4 | interface IEdgeType {
5 | cursor: string;
6 | node: T;
7 | }
8 |
9 | export interface IPaginatedType {
10 | edges: IEdgeType[];
11 | nodes: T[];
12 | totalCount: number;
13 | hasNextPage: boolean;
14 | }
15 |
16 | export function Paginated(classRef: Type): Type> {
17 | @ObjectType(`${classRef.name}Edge`)
18 | abstract class EdgeType {
19 | @Field(() => String)
20 | cursor!: string;
21 |
22 | @Field(() => classRef)
23 | node!: T;
24 | }
25 |
26 | @ObjectType({ isAbstract: true })
27 | abstract class PaginatedType implements IPaginatedType {
28 | @Field(() => [EdgeType], { nullable: true })
29 | edges!: EdgeType[];
30 |
31 | @Field(() => [classRef], { nullable: true })
32 | nodes!: T[];
33 |
34 | @Field(() => Int)
35 | totalCount!: number;
36 |
37 | @Field()
38 | hasNextPage!: boolean;
39 | }
40 | return PaginatedType as Type>;
41 | }
42 |
--------------------------------------------------------------------------------
/src/libs/api/rest/collection.interface.ts:
--------------------------------------------------------------------------------
1 | export interface Collection {
2 | items: E[];
3 | total: number;
4 | }
5 |
6 | export const emptyCollection = (): Collection => ({
7 | items: [],
8 | total: 0,
9 | });
10 |
--------------------------------------------------------------------------------
/src/libs/api/rest/paginated-query-params.dto.ts:
--------------------------------------------------------------------------------
1 | import { Type } from 'class-transformer';
2 | import { IsInt, IsOptional, Max, Min } from 'class-validator';
3 |
4 | export class PaginatedQueryParams {
5 | @IsOptional()
6 | @IsInt()
7 | @Min(0)
8 | @Max(99999)
9 | @Type(() => Number)
10 | offset: number = 0;
11 |
12 | @IsOptional()
13 | @IsInt()
14 | @Min(0)
15 | @Max(99999)
16 | @Type(() => Number)
17 | limit: number = 10;
18 | }
19 |
--------------------------------------------------------------------------------
/src/libs/api/rest/paginated.response.dto.ts:
--------------------------------------------------------------------------------
1 | import { Collection } from './collection.interface';
2 |
3 | export interface PaginatedResponse {
4 | readonly data: E[];
5 | readonly offset: number;
6 | readonly limit: number;
7 | readonly total: number;
8 | }
9 |
10 | export const toPaginatedResponse = (
11 | data: Collection,
12 | offset: number,
13 | limit: number,
14 | ): PaginatedResponse => {
15 | return {
16 | data: data.items,
17 | offset,
18 | limit,
19 | total: data.total,
20 | };
21 | };
22 |
--------------------------------------------------------------------------------
/src/libs/api/rest/sorting-type.enum.ts:
--------------------------------------------------------------------------------
1 | export enum SortingType {
2 | ASC = 'ASC',
3 | DESC = 'DESC',
4 | }
5 |
--------------------------------------------------------------------------------
/src/libs/database/base.entity.ts:
--------------------------------------------------------------------------------
1 | import { PrimaryKey, Property } from '@mikro-orm/core';
2 |
3 | export abstract class BaseEntity {
4 | @PrimaryKey({ autoincrement: true })
5 | id!: number | string;
6 |
7 | @Property({ onCreate: () => new Date() })
8 | createdAt!: Date;
9 |
10 | @Property({ onCreate: () => new Date(), onUpdate: () => new Date() })
11 | updatedAt!: Date;
12 |
13 | @Property({ nullable: true })
14 | deletedAt?: Date;
15 | }
16 |
--------------------------------------------------------------------------------
/src/libs/ddd/application-service.interface.ts:
--------------------------------------------------------------------------------
1 | export interface ApplicationService {
2 | execute(input: I): Promise;
3 | }
4 |
--------------------------------------------------------------------------------
/src/libs/ddd/base-entity.interface.ts:
--------------------------------------------------------------------------------
1 | export interface BaseEntity {
2 | id: number;
3 | }
4 |
--------------------------------------------------------------------------------
/src/libs/ddd/domain-event.abstract.ts:
--------------------------------------------------------------------------------
1 | import { v4 } from 'uuid';
2 |
3 | export abstract class DomainEvent {
4 | public readonly eventId: string;
5 | public readonly name: string;
6 | public readonly timeStamp: Date;
7 | public readonly payload: T;
8 | public readonly correlationId?: string;
9 | public readonly version: number;
10 |
11 | protected constructor(
12 | name: string,
13 | payload: T,
14 | options: { correlationId?: string; version?: number } = {},
15 | ) {
16 | this.eventId = v4();
17 | this.name = name;
18 | this.timeStamp = new Date();
19 | this.payload = payload;
20 | this.correlationId = options.correlationId;
21 | this.version = options.version || 1;
22 | }
23 |
24 | toJSON(): string {
25 | return JSON.stringify({
26 | eventId: this.eventId,
27 | name: this.name,
28 | occurredOn: this.timeStamp.toISOString(),
29 | payload: this.payload,
30 | correlationId: this.correlationId,
31 | version: this.version,
32 | });
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/libs/ddd/domain-service.interface.ts:
--------------------------------------------------------------------------------
1 | export interface DomainService {
2 | execute(): Promise;
3 | }
4 |
--------------------------------------------------------------------------------
/src/libs/ddd/mapper.interface.ts:
--------------------------------------------------------------------------------
1 | export interface Mapper {
2 | toDomain(record: PersistenceModel): DomainEntity;
3 |
4 | toPersistence(entity: DomainEntity): PersistenceModel;
5 |
6 | toResponse?(entity: DomainEntity): ResponseDTO;
7 | }
8 |
--------------------------------------------------------------------------------
/src/libs/ddd/mikro-orm-repository.abstract.ts:
--------------------------------------------------------------------------------
1 | import { EntityManager, EntityRepository, FilterQuery } from '@mikro-orm/core';
2 | import { fromNullable, getOrThrowWith, Option } from 'effect/Option';
3 | import { Repository } from './repository.interface';
4 | import { Collection } from '../api/rest/collection.interface';
5 | import { BaseEntity } from './base-entity.interface';
6 |
7 | export abstract class MikroOrmRepository
8 | implements Repository
9 | {
10 | constructor(
11 | protected readonly repository: EntityRepository,
12 | protected readonly em: EntityManager,
13 | ) {}
14 |
15 | async findById(id: number): Promise> {
16 | return fromNullable(
17 | await this.repository.findOne({ id } as FilterQuery),
18 | );
19 | }
20 |
21 | async findAll(): Promise> {
22 | const items = await this.repository.findAll();
23 | return { items, total: items.length };
24 | }
25 |
26 | async save(entity: T): Promise {
27 | await this.repository.insert(entity);
28 | await this.em.flush();
29 | return entity;
30 | }
31 |
32 | async update(data: Partial): Promise {
33 | if (!data.id) throw new Error(`ID is required for updating`);
34 | const entity: T = getOrThrowWith(
35 | await this.findById(data.id),
36 | () => new Error(`Entity not found`),
37 | );
38 | for (const [key, value] of Object.entries(data)) {
39 | entity[key] = value;
40 | }
41 | await this.em.flush();
42 | return entity;
43 | }
44 |
45 | async delete(id: number): Promise {
46 | const entity: T = getOrThrowWith(
47 | await this.findById(id),
48 | () => new Error(`Entity not found`),
49 | );
50 | if (!entity) return false;
51 | await this.em.removeAndFlush(entity);
52 | return true;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/libs/ddd/repository.interface.ts:
--------------------------------------------------------------------------------
1 | import { Option } from 'effect/Option';
2 | import { Collection } from '../api/rest/collection.interface';
3 |
4 | export interface Repository {
5 | findById(id: number): Promise>;
6 |
7 | findAll(): Promise>;
8 |
9 | save(entity: T): Promise;
10 |
11 | update(data: Partial): Promise;
12 |
13 | delete(id: number): Promise;
14 | }
15 |
--------------------------------------------------------------------------------
/src/libs/ddd/use-case.interface.ts:
--------------------------------------------------------------------------------
1 | export interface UseCase {
2 | execute(input: I): Promise;
3 | }
4 |
--------------------------------------------------------------------------------
/src/libs/ddd/value-object.abstract.ts:
--------------------------------------------------------------------------------
1 | export abstract class ValueObject {
2 | protected readonly props: T;
3 |
4 | constructor(props: T) {
5 | this.props = Object.freeze(props);
6 | }
7 |
8 | equals(other?: ValueObject): boolean {
9 | if (other === null || other === undefined) return false;
10 | return JSON.stringify(this.props) === JSON.stringify(other.props);
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/libs/decorator/auth.decorator.ts:
--------------------------------------------------------------------------------
1 | import {
2 | createParamDecorator,
3 | ExecutionContext,
4 | SetMetadata,
5 | } from '@nestjs/common';
6 | import { ApiRole } from '../api/api-role.enum';
7 |
8 | export const AUTH_ROLES_KEY = 'AUTH_ROLES_KEY';
9 | export const IS_PUBLIC_API = 'IS_PUBLIC_API';
10 |
11 | export const AuthRoles = (...roles: ApiRole[]) =>
12 | SetMetadata(AUTH_ROLES_KEY, roles);
13 |
14 | export const PublicApi = () => SetMetadata(IS_PUBLIC_API, true);
15 |
16 | export const InjectAuthUser = createParamDecorator(
17 | (_data: unknown, context: ExecutionContext) => {
18 | return context.switchToHttp().getRequest().user;
19 | },
20 | );
21 |
--------------------------------------------------------------------------------
/src/libs/decorator/query-params.decorator.ts:
--------------------------------------------------------------------------------
1 | import { createParamDecorator, ExecutionContext } from '@nestjs/common';
2 |
3 | export const QueryParams = createParamDecorator(
4 | (_data: unknown, ctx: ExecutionContext): unknown => {
5 | const request = ctx.switchToHttp().getRequest();
6 | const params = request.query;
7 | if (params.limit) params.limit = Number(params.limit);
8 | if (params.offset) params.offset = Number(params.offset);
9 | params.filter = convertToObject(params, 'filter');
10 | params.sort = convertToObject(params, 'sort');
11 | return params;
12 | },
13 | );
14 |
15 | function convertToObject(
16 | params: Record,
17 | property: string,
18 | ): object {
19 | const object: Record = {};
20 | const regex = new RegExp(`${property}\\[(.*?)\\]`);
21 | for (const key in params) {
22 | const match = regex.exec(key);
23 | if (match) {
24 | object[match[1]] = params[key];
25 | delete params[key];
26 | }
27 | }
28 | return object;
29 | }
30 |
--------------------------------------------------------------------------------
/src/libs/exceptions/custom-bad-request.exception.ts:
--------------------------------------------------------------------------------
1 | import { HttpException, HttpStatus } from '@nestjs/common';
2 |
3 | export class CustomBadRequestException extends HttpException {
4 | constructor(message: string) {
5 | super(
6 | {
7 | statusCode: HttpStatus.BAD_REQUEST,
8 | message,
9 | },
10 | HttpStatus.BAD_REQUEST,
11 | );
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/libs/exceptions/custom-conflict.exception.ts:
--------------------------------------------------------------------------------
1 | import { HttpException, HttpStatus } from '@nestjs/common';
2 |
3 | export class CustomConflictException extends HttpException {
4 | constructor(resource: string) {
5 | super(
6 | {
7 | statusCode: HttpStatus.CONFLICT,
8 | message: `${resource} already exists`,
9 | },
10 | HttpStatus.CONFLICT,
11 | );
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/libs/exceptions/custom-not-found.exception.ts:
--------------------------------------------------------------------------------
1 | import { HttpException, HttpStatus } from '@nestjs/common';
2 |
3 | export class CustomNotFoundException extends HttpException {
4 | constructor(resource: string) {
5 | super(
6 | {
7 | statusCode: HttpStatus.NOT_FOUND,
8 | message: `${resource} not found !`,
9 | },
10 | HttpStatus.NOT_FOUND,
11 | );
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/libs/exceptions/exception.filter.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ArgumentsHost,
3 | Catch,
4 | ExceptionFilter,
5 | HttpException,
6 | } from '@nestjs/common';
7 | import { GraphQLError } from 'graphql';
8 |
9 | @Catch(HttpException)
10 | export class HttpExceptionFilter implements ExceptionFilter {
11 | catch(exception: HttpException, host: ArgumentsHost) {
12 | const httpHost = host.switchToHttp();
13 | const exceptionResponse = exception.getResponse();
14 | const responseMessage =
15 | typeof exceptionResponse === 'string'
16 | ? exceptionResponse
17 | : exceptionResponse['message'] || exception.message;
18 | if (host.getType().toString() === 'graphql') {
19 | throw new GraphQLError(responseMessage, {
20 | extensions: {
21 | code: exception.getStatus(),
22 | http: {
23 | status: exception.getStatus(),
24 | },
25 | timestamp: new Date().toISOString(),
26 | },
27 | });
28 | } else {
29 | const response = httpHost.getResponse();
30 | const status = exception.getStatus();
31 | const request = httpHost.getRequest();
32 | response.code(status).send({
33 | statusCode: status,
34 | message: responseMessage,
35 | timestamp: new Date().toISOString(),
36 | path: request?.url || 'unknown',
37 | });
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/libs/pipe/query-params-validation.pipe.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ArgumentMetadata,
3 | BadRequestException,
4 | Injectable,
5 | PipeTransform,
6 | } from '@nestjs/common';
7 | import { plainToInstance } from 'class-transformer';
8 | import { validate, ValidationError } from 'class-validator';
9 |
10 | @Injectable()
11 | export class QueryParamsValidationPipe implements PipeTransform {
12 | async transform(
13 | params: Record,
14 | { metatype }: ArgumentMetadata,
15 | ) {
16 | if (!metatype) return params;
17 | const dto = plainToInstance(metatype, params);
18 | const errors: ValidationError[] = await validate(dto, {
19 | transform: true,
20 | forbidNonWhitelisted: true,
21 | whitelist: true,
22 | });
23 | if (errors.length > 0) {
24 | const message = this.buildErrorMessages(errors);
25 | throw new BadRequestException('Invalid query parameters: ' + message);
26 | }
27 | return dto;
28 | }
29 |
30 | private buildErrorMessages(
31 | errors: ValidationError[],
32 | parentPath: string = '',
33 | ): string {
34 | return errors
35 | .map((err) => {
36 | const propertyPath = parentPath
37 | ? `${parentPath}.${err.property}`
38 | : err.property;
39 | if (err.children && err.children.length > 0) {
40 | return this.buildErrorMessages(err.children, propertyPath);
41 | }
42 | const constraints = Object.values(err.constraints || {}).join(', ');
43 | return `${propertyPath}: ${constraints}`;
44 | })
45 | .join('; ');
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/libs/util/config.util.ts:
--------------------------------------------------------------------------------
1 | import { fromNullable, getOrThrowWith } from 'effect/Option';
2 | import { ConfigService } from '@nestjs/config';
3 |
4 | export const getConfigValue = (
5 | configService: ConfigService,
6 | key: string,
7 | ): T => {
8 | return getOrThrowWith(
9 | fromNullable(configService.get(key)),
10 | () => new Error(`Missing ${key}`),
11 | );
12 | };
13 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { NestFactory } from '@nestjs/core';
2 | import { AppModule } from './app.module';
3 | import {
4 | FastifyAdapter,
5 | NestFastifyApplication,
6 | } from '@nestjs/platform-fastify';
7 | import { join } from 'path';
8 | import {
9 | BadRequestException,
10 | ValidationPipe,
11 | VersioningType,
12 | } from '@nestjs/common';
13 | import { HttpExceptionFilter } from './libs/exceptions/exception.filter';
14 |
15 | async function bootstrap() {
16 | const app = await NestFactory.create(
17 | AppModule,
18 | new FastifyAdapter(),
19 | );
20 | app.enableVersioning({
21 | type: VersioningType.URI,
22 | defaultVersion: '1',
23 | });
24 | app.enableShutdownHooks();
25 | app.enableCors();
26 | app.useStaticAssets({
27 | root: join(__dirname, '../public'),
28 | });
29 | app.setGlobalPrefix('api');
30 | app.useGlobalPipes(
31 | new ValidationPipe({
32 | transform: true,
33 | whitelist: true,
34 | forbidNonWhitelisted: true,
35 | exceptionFactory: (errors) => {
36 | return new BadRequestException(
37 | errors
38 | .map((error) => Object.values(error.constraints as object))
39 | .flat(),
40 | );
41 | },
42 | }),
43 | );
44 | app.useGlobalFilters(new HttpExceptionFilter());
45 | const port = process.env.PORT || 3000;
46 | await app.listen(port, '0.0.0.0');
47 | }
48 |
49 | bootstrap();
50 |
--------------------------------------------------------------------------------
/src/modules/auth/api/guard/auth.guard.ts:
--------------------------------------------------------------------------------
1 | import {
2 | CanActivate,
3 | ExecutionContext,
4 | Inject,
5 | Injectable,
6 | } from '@nestjs/common';
7 | import { FastifyRequest } from 'fastify';
8 | import { Reflector } from '@nestjs/core';
9 | import { JWT_AUTH_SERVICE } from '../../auth.tokens';
10 | import { isNone, none, Option, some } from 'effect/Option';
11 | import { QueryBus } from '@nestjs/cqrs';
12 | import { CheckAuthUserByIdQuery } from '../../application/query/check-auth-user-by-id.query';
13 | import {
14 | AUTH_ROLES_KEY,
15 | IS_PUBLIC_API,
16 | } from '../../../../libs/decorator/auth.decorator';
17 | import { ApiRole } from '../../../../libs/api/api-role.enum';
18 | import { GqlExecutionContext } from '@nestjs/graphql';
19 | import { JwtAuthService } from '../../application/service/jwt-auth-service.interface';
20 |
21 | @Injectable()
22 | export class AuthGuard implements CanActivate {
23 | private authenticationHeaders: string[] = ['Authorization', 'authorization'];
24 |
25 | constructor(
26 | private readonly reflector: Reflector,
27 | @Inject(JWT_AUTH_SERVICE)
28 | private readonly jwtService: JwtAuthService,
29 | private readonly queryBus: QueryBus,
30 | ) {}
31 |
32 | async canActivate(context: ExecutionContext): Promise {
33 | const apiRoles = this.reflector.getAllAndOverride(
34 | AUTH_ROLES_KEY,
35 | [context.getHandler(), context.getClass()],
36 | );
37 | const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_API, [
38 | context.getHandler(),
39 | context.getClass(),
40 | ]);
41 | if (isPublic && !apiRoles) return true;
42 | let request: FastifyRequest;
43 | if (context.getType().toString() === 'graphql') {
44 | const gqlContext = GqlExecutionContext.create(context);
45 | request = gqlContext.getContext().req;
46 | } else {
47 | request = context.switchToHttp().getRequest();
48 | }
49 | const token = this.extractToken(request);
50 | if (isNone(token)) return false;
51 | try {
52 | const authUser = await this.jwtService.verifyToken(token.value);
53 | if (isNone(authUser)) return false;
54 | request['user'] = authUser.value;
55 | const isActiveUser = await this.isActiveUser(authUser.value.id);
56 | return isActiveUser && apiRoles.includes(authUser.value.role);
57 | } catch {
58 | return false;
59 | }
60 | }
61 |
62 | private extractToken(request: FastifyRequest): Option {
63 | for (const header of this.authenticationHeaders) {
64 | const tokenHeader = request.headers[header] as string;
65 | if (tokenHeader) {
66 | const splitted = tokenHeader.split(' ');
67 | if (splitted[0] !== 'Bearer') {
68 | return none();
69 | } else {
70 | return some(splitted[1]);
71 | }
72 | }
73 | }
74 | return none();
75 | }
76 |
77 | private async isActiveUser(userId: string): Promise {
78 | return await this.queryBus.execute(new CheckAuthUserByIdQuery(userId));
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/modules/auth/api/rest/controller/auth.controller.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Body,
3 | Controller,
4 | Inject,
5 | Post,
6 | UnauthorizedException,
7 | } from '@nestjs/common';
8 | import { LoginBody } from '../presentation/body/login.body';
9 | import { SignupBody } from '../presentation/body/signup.body';
10 | import { PublicApi } from '../../../../../libs/decorator/auth.decorator';
11 | import { getOrThrowWith, Option } from 'effect/Option';
12 | import { JwtUser } from '../presentation/dto/jwt-user.dto';
13 | import { JwtAuthService } from '../../../application/service/jwt-auth-service.interface';
14 | import {
15 | JWT_AUTH_SERVICE,
16 | LOGIN_USE_CASE,
17 | SIGNUP_USE_CASE,
18 | } from '../../../auth.tokens';
19 | import { UseCase } from '../../../../../libs/ddd/use-case.interface';
20 | import { AuthUser } from '../presentation/dto/auth-user.dto';
21 | import { RefreshTokenBody } from '../presentation/body/refresh-token.body';
22 |
23 | @Controller('auth')
24 | export class AuthController {
25 | constructor(
26 | @Inject(JWT_AUTH_SERVICE)
27 | private readonly jwtAuth: JwtAuthService,
28 | @Inject(LOGIN_USE_CASE)
29 | private readonly loginUseCase: UseCase>,
30 | @Inject(SIGNUP_USE_CASE)
31 | private readonly signupUseCase: UseCase>,
32 | ) {}
33 |
34 | @PublicApi()
35 | @Post('/login')
36 | async login(@Body() body: LoginBody): Promise {
37 | return this.jwtAuth.generateJwtUser(
38 | getOrThrowWith(
39 | await this.loginUseCase.execute(body),
40 | () => new UnauthorizedException('Login Error!'),
41 | ),
42 | );
43 | }
44 |
45 | @PublicApi()
46 | @Post('/signup')
47 | async signup(@Body() body: SignupBody) {
48 | return this.jwtAuth.generateJwtUser(
49 | getOrThrowWith(
50 | await this.signupUseCase.execute(body),
51 | () => new UnauthorizedException('Signup Error!'),
52 | ),
53 | );
54 | }
55 |
56 | @PublicApi()
57 | @Post('/token/refresh')
58 | async refreshToken(@Body() body: RefreshTokenBody) {
59 | return this.jwtAuth.generateJwtUserFromRefresh(body.token);
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/modules/auth/api/rest/presentation/body/login.body.ts:
--------------------------------------------------------------------------------
1 | import {
2 | IsEmail,
3 | IsNotEmpty,
4 | IsString,
5 | IsStrongPassword,
6 | MaxLength,
7 | } from 'class-validator';
8 |
9 | export class LoginBody {
10 | @IsNotEmpty()
11 | @IsEmail()
12 | @IsString()
13 | email!: string;
14 |
15 | @IsNotEmpty()
16 | @IsString()
17 | @MaxLength(20)
18 | @IsStrongPassword(
19 | {
20 | minLength: 8,
21 | minSymbols: 1,
22 | },
23 | {
24 | message:
25 | 'Password must be at least 8 characters long and contain at least one symbol',
26 | },
27 | )
28 | password!: string;
29 | }
30 |
--------------------------------------------------------------------------------
/src/modules/auth/api/rest/presentation/body/refresh-token.body.ts:
--------------------------------------------------------------------------------
1 | import { IsJWT, IsNotEmpty, IsString } from 'class-validator';
2 |
3 | export class RefreshTokenBody {
4 | @IsNotEmpty()
5 | @IsString()
6 | @IsJWT()
7 | token!: string;
8 | }
9 |
--------------------------------------------------------------------------------
/src/modules/auth/api/rest/presentation/body/signup.body.ts:
--------------------------------------------------------------------------------
1 | import { LoginBody } from './login.body';
2 | import { IsNotEmpty, IsString } from 'class-validator';
3 |
4 | export class SignupBody extends LoginBody {
5 | @IsNotEmpty()
6 | @IsString()
7 | firstName!: string;
8 |
9 | @IsNotEmpty()
10 | @IsString()
11 | lastName!: string;
12 | }
13 |
--------------------------------------------------------------------------------
/src/modules/auth/api/rest/presentation/dto/auth-user.dto.ts:
--------------------------------------------------------------------------------
1 | export interface AuthUser {
2 | id: string;
3 | email: string;
4 | role: number;
5 | }
6 |
--------------------------------------------------------------------------------
/src/modules/auth/api/rest/presentation/dto/jwt-user.dto.ts:
--------------------------------------------------------------------------------
1 | import { AuthUser } from './auth-user.dto';
2 |
3 | export interface JwtUser {
4 | token: string;
5 | expiresIn: number;
6 | refreshToken: string;
7 | refreshExpiresIn: number;
8 | user: AuthUser;
9 | }
10 |
--------------------------------------------------------------------------------
/src/modules/auth/application/command/register-user.command.ts:
--------------------------------------------------------------------------------
1 | import { ICommand } from '@nestjs/cqrs';
2 |
3 | export class RegisterUserCommand implements ICommand {
4 | constructor(
5 | readonly email: string,
6 | readonly password: string,
7 | readonly firstName: string,
8 | readonly lastName: string,
9 | ) {}
10 | }
11 |
--------------------------------------------------------------------------------
/src/modules/auth/application/query/check-auth-user-by-id.query.ts:
--------------------------------------------------------------------------------
1 | export class CheckAuthUserByIdQuery {
2 | constructor(readonly id: string) {}
3 | }
4 |
--------------------------------------------------------------------------------
/src/modules/auth/application/query/get-auth-user-by-email.query.ts:
--------------------------------------------------------------------------------
1 | export class GetAuthUserByEmailQuery {
2 | constructor(readonly email: string) {}
3 | }
4 |
--------------------------------------------------------------------------------
/src/modules/auth/application/service/jwt-auth-service.interface.ts:
--------------------------------------------------------------------------------
1 | import { Option } from 'effect/Option';
2 | import { AuthUser } from '../../api/rest/presentation/dto/auth-user.dto';
3 | import { JwtUser } from '../../api/rest/presentation/dto/jwt-user.dto';
4 |
5 | export interface JwtAuthService {
6 | verifyToken(token: string): Promise>;
7 |
8 | verifyRefreshToken(refreshToken: string): Promise >;
9 |
10 | generateToken(user: AuthUser): Promise;
11 |
12 | generateRefreshToken(user: AuthUser): Promise;
13 |
14 | generateJwtUser(user: AuthUser): Promise;
15 |
16 | generateJwtUserFromRefresh(token: string): Promise;
17 | }
18 |
--------------------------------------------------------------------------------
/src/modules/auth/application/use-case/login.use-case.ts:
--------------------------------------------------------------------------------
1 | import { UseCase } from '../../../../libs/ddd/use-case.interface';
2 | import { LoginBody } from '../../api/rest/presentation/body/login.body';
3 | import { fromNullable, isNone, none, Option } from 'effect/Option';
4 | import { QueryBus } from '@nestjs/cqrs';
5 | import { Injectable, UnauthorizedException } from '@nestjs/common';
6 | import { UserState } from '../../../user/domain/value-object/user-state.enum';
7 | import { compare } from 'bcryptjs';
8 | import { AuthUser } from '../../api/rest/presentation/dto/auth-user.dto';
9 | import { GetAuthUserByEmailQuery } from '../query/get-auth-user-by-email.query';
10 | import { User } from '../../../user/domain/entity/user.entity';
11 |
12 | @Injectable()
13 | export class LoginUseCase implements UseCase> {
14 | constructor(private readonly queryBus: QueryBus) {}
15 |
16 | async execute(body: LoginBody): Promise> {
17 | const user: Option = await this.queryBus.execute(
18 | new GetAuthUserByEmailQuery(body.email),
19 | );
20 | if (isNone(user) || user.value.props.state !== UserState.ACTIVE)
21 | return none();
22 | const match = await compare(body.password, user.value.props.password);
23 | if (!match) throw new UnauthorizedException('Invalid Credentials!');
24 | return fromNullable({
25 | id: user.value.id,
26 | email: user.value.props.email,
27 | role: user.value.props.role,
28 | });
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/modules/auth/application/use-case/signup.use-case.ts:
--------------------------------------------------------------------------------
1 | import { UseCase } from '../../../../libs/ddd/use-case.interface';
2 | import { isSome, map, Option } from 'effect/Option';
3 | import { CommandBus, QueryBus } from '@nestjs/cqrs';
4 | import { Injectable } from '@nestjs/common';
5 | import { AuthUser } from '../../api/rest/presentation/dto/auth-user.dto';
6 | import { SignupBody } from '../../api/rest/presentation/body/signup.body';
7 | import { RegisterUserCommand } from '../command/register-user.command';
8 | import { GetAuthUserByEmailQuery } from '../query/get-auth-user-by-email.query';
9 | import { CustomConflictException } from '../../../../libs/exceptions/custom-conflict.exception';
10 | import { User } from '../../../user/domain/entity/user.entity';
11 |
12 | @Injectable()
13 | export class SignupUseCase implements UseCase> {
14 | constructor(
15 | private readonly queryBus: QueryBus,
16 | private readonly commandBus: CommandBus,
17 | ) {}
18 |
19 | async execute(body: SignupBody): Promise> {
20 | const found: Option = await this.queryBus.execute(
21 | new GetAuthUserByEmailQuery(body.email),
22 | );
23 | if (isSome(found))
24 | throw new CustomConflictException(found.value.props.email);
25 | return map(
26 | await this.commandBus.execute(
27 | new RegisterUserCommand(
28 | body.email,
29 | body.password,
30 | body.firstName,
31 | body.lastName,
32 | ),
33 | ),
34 | (user: User) => ({
35 | id: user.id,
36 | email: user.props.email,
37 | role: user.props.role,
38 | }),
39 | );
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/modules/auth/auth.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { APP_GUARD } from '@nestjs/core';
3 | import { JwtService } from './infrastructure/jwt/jwt.service';
4 | import { AuthGuard } from './api/guard/auth.guard';
5 | import { AuthController } from './api/rest/controller/auth.controller';
6 | import {
7 | JWT_AUTH_SERVICE,
8 | LOGIN_USE_CASE,
9 | SIGNUP_USE_CASE,
10 | } from './auth.tokens';
11 | import { LoginUseCase } from './application/use-case/login.use-case';
12 | import { SignupUseCase } from './application/use-case/signup.use-case';
13 |
14 | @Module({
15 | imports: [],
16 | controllers: [AuthController],
17 | providers: [
18 | {
19 | provide: APP_GUARD,
20 | useClass: AuthGuard,
21 | },
22 | {
23 | provide: JWT_AUTH_SERVICE,
24 | useClass: JwtService,
25 | },
26 | {
27 | provide: LOGIN_USE_CASE,
28 | useClass: LoginUseCase,
29 | },
30 | {
31 | provide: SIGNUP_USE_CASE,
32 | useClass: SignupUseCase,
33 | },
34 | ],
35 | exports: [],
36 | })
37 | export class AuthModule {}
38 |
--------------------------------------------------------------------------------
/src/modules/auth/auth.tokens.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Constants for NestJS Dependency Injection System.
3 | */
4 | export const JWT_AUTH_SERVICE = 'JWT_AUTH_SERVICE';
5 | export const LOGIN_USE_CASE = 'LOGIN_USE_CASE';
6 | export const SIGNUP_USE_CASE = 'SIGNUP_USE_CASE';
7 |
--------------------------------------------------------------------------------
/src/modules/auth/infrastructure/jwt/jwt.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, UnauthorizedException } from '@nestjs/common';
2 | import { ConfigService } from '@nestjs/config';
3 | import {
4 | JWT_EXPIRES_IN,
5 | JWT_REFRESH_EXPIRES_IN,
6 | JWT_REFRESH_SECRET,
7 | JWT_SECRET,
8 | } from '../../../../config/env/configuration.constant';
9 | import { isNone, liftThrowable, Option } from 'effect/Option';
10 | import { JwtAuthService } from '../../application/service/jwt-auth-service.interface';
11 | import * as jwt from 'jsonwebtoken';
12 | import { getConfigValue } from '../../../../libs/util/config.util';
13 | import { AuthUser } from '../../api/rest/presentation/dto/auth-user.dto';
14 | import { JwtUser } from '../../api/rest/presentation/dto/jwt-user.dto';
15 |
16 | @Injectable()
17 | export class JwtService implements JwtAuthService {
18 | private readonly tokenSecret: string;
19 | private readonly refreshTokenSecret: string;
20 | private readonly tokenExpiration: number;
21 | private readonly refreshTokenExpiration: number;
22 |
23 | constructor(private readonly configService: ConfigService) {
24 | this.tokenSecret = getConfigValue(this.configService, JWT_SECRET);
25 | this.refreshTokenSecret = getConfigValue(
26 | this.configService,
27 | JWT_REFRESH_SECRET,
28 | );
29 | this.tokenExpiration = getConfigValue(
30 | this.configService,
31 | JWT_EXPIRES_IN,
32 | );
33 | this.refreshTokenExpiration = getConfigValue(
34 | this.configService,
35 | JWT_REFRESH_EXPIRES_IN,
36 | );
37 | }
38 |
39 | async generateToken(user: AuthUser): Promise {
40 | return jwt.sign(user, this.tokenSecret, {
41 | expiresIn: this.tokenExpiration,
42 | });
43 | }
44 |
45 | async generateRefreshToken(user: AuthUser): Promise {
46 | return jwt.sign(user, this.refreshTokenSecret, {
47 | expiresIn: this.refreshTokenExpiration,
48 | });
49 | }
50 |
51 | async generateJwtUser(authUser: AuthUser): Promise {
52 | const token = await this.generateToken(authUser);
53 | const refreshToken = await this.generateRefreshToken(authUser);
54 | return {
55 | token,
56 | expiresIn: this.tokenExpiration,
57 | refreshToken,
58 | refreshExpiresIn: this.refreshTokenExpiration,
59 | user: authUser,
60 | };
61 | }
62 |
63 | async generateJwtUserFromRefresh(refreshToken: string): Promise {
64 | const authUser = this.verifyJwt(
65 | refreshToken,
66 | this.refreshTokenSecret,
67 | );
68 | if (isNone(authUser)) throw new UnauthorizedException('Invalid Token!');
69 | return this.generateJwtUser(this.convertToAuthUser(authUser.value));
70 | }
71 |
72 | async verifyToken(token: string): Promise> {
73 | return this.verifyJwt(token, this.tokenSecret);
74 | }
75 |
76 | async verifyRefreshToken(refreshToken: string): Promise> {
77 | return this.verifyJwt(refreshToken, this.refreshTokenSecret);
78 | }
79 |
80 | /**
81 | * Generic JWT verification method.
82 | */
83 | private verifyJwt(token: string, secret: string): Option {
84 | return liftThrowable(() => jwt.verify(token, secret) as T)();
85 | }
86 |
87 | /**
88 | * Helper method to clean the AuthUser object.
89 | */
90 | convertToAuthUser = (authUser: AuthUser): AuthUser => ({
91 | id: authUser.id,
92 | email: authUser.email,
93 | role: authUser.role,
94 | });
95 | }
96 |
--------------------------------------------------------------------------------
/src/modules/communication/application/handler/event/created-user.handler.ts:
--------------------------------------------------------------------------------
1 | import { CreatedUserEvent } from '../../../../user/domain/event/created-user.event';
2 | import { EventsHandler } from '@nestjs/cqrs';
3 | import { Inject, Injectable } from '@nestjs/common';
4 | import { EmailService } from '../../service/email.service.interface';
5 | import { EMAIL_SERVICE } from '../../../communication.tokens';
6 |
7 | @EventsHandler(CreatedUserEvent)
8 | @Injectable()
9 | export class CreatedUserHandler {
10 | constructor(
11 | @Inject(EMAIL_SERVICE) private readonly emailService: EmailService,
12 | ) {}
13 |
14 | async handle(event: CreatedUserEvent): Promise {
15 | await this.emailService.sendWelcomeEmail(event.payload.props.email);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/modules/communication/application/service/email.service.interface.ts:
--------------------------------------------------------------------------------
1 | export interface EmailService {
2 | sendWelcomeEmail(email: string): Promise;
3 | }
4 |
--------------------------------------------------------------------------------
/src/modules/communication/communication.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { CreatedUserHandler } from './application/handler/event/created-user.handler';
3 | import { EMAIL_SERVICE } from './communication.tokens';
4 | import { EmailServiceImpl } from './infrastructure/email/email.service';
5 |
6 | @Module({
7 | imports: [],
8 | controllers: [],
9 | providers: [
10 | CreatedUserHandler,
11 | {
12 | provide: EMAIL_SERVICE,
13 | useClass: EmailServiceImpl,
14 | },
15 | ],
16 | exports: [],
17 | })
18 | export class CommunicationModule {}
19 |
--------------------------------------------------------------------------------
/src/modules/communication/communication.tokens.ts:
--------------------------------------------------------------------------------
1 | export const EMAIL_SERVICE = 'EMAIL_SERVICE';
2 |
--------------------------------------------------------------------------------
/src/modules/communication/infrastructure/email/email.service.ts:
--------------------------------------------------------------------------------
1 | import { EmailService } from '../../application/service/email.service.interface';
2 |
3 | /**
4 | * Contains the implementation of the Email Service interface.
5 | * It can use a specific package like NodeMailer or MailGun.
6 | */
7 | export class EmailServiceImpl implements EmailService {
8 | constructor() {}
9 | async sendWelcomeEmail(email: string): Promise {
10 | console.log(`Sending email to ${email}`);
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/modules/health/api/rest/controller/health.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller, Get } from '@nestjs/common';
2 | import {
3 | DiskHealthIndicator,
4 | HealthCheck,
5 | HealthCheckService,
6 | HttpHealthIndicator,
7 | MikroOrmHealthIndicator,
8 | } from '@nestjs/terminus';
9 | import { PublicApi } from '../../../../../libs/decorator/auth.decorator';
10 |
11 | @Controller({
12 | path: 'health',
13 | })
14 | export class HealthController {
15 | constructor(
16 | private health: HealthCheckService,
17 | private http: HttpHealthIndicator,
18 | private db: MikroOrmHealthIndicator,
19 | private readonly disk: DiskHealthIndicator,
20 | // private memory: MemoryHealthIndicator,
21 | ) {}
22 |
23 | @PublicApi()
24 | @Get()
25 | @HealthCheck()
26 | check() {
27 | return this.health.check([
28 | () => this.http.pingCheck('external-connectivity', 'https://google.com'),
29 | () => this.db.pingCheck('database'),
30 | () =>
31 | this.disk.checkStorage('storage', { path: '/', thresholdPercent: 0.8 }),
32 | //() => this.memory.checkHeap('memory', 750 * 1024 * 1024),
33 | ]);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/modules/health/health.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { TerminusModule } from '@nestjs/terminus';
3 | import { HealthController } from './api/rest/controller/health.controller';
4 | import { HttpModule } from '@nestjs/axios';
5 |
6 | @Module({
7 | imports: [TerminusModule, HttpModule],
8 | controllers: [HealthController],
9 | providers: [],
10 | exports: [],
11 | })
12 | export class HealthModule {}
13 |
--------------------------------------------------------------------------------
/src/modules/user/api/graphql/presentation/input/create-user.input.ts:
--------------------------------------------------------------------------------
1 | import { Field, InputType } from '@nestjs/graphql';
2 | import {
3 | IsEmail,
4 | IsNotEmpty,
5 | IsString,
6 | IsStrongPassword,
7 | MaxLength,
8 | } from 'class-validator';
9 |
10 | @InputType()
11 | export class CreateUserInput {
12 | @IsNotEmpty()
13 | @IsEmail()
14 | @Field()
15 | email!: string;
16 |
17 | @Field()
18 | @IsNotEmpty()
19 | @IsString()
20 | @MaxLength(20)
21 | @IsStrongPassword(
22 | {
23 | minLength: 8,
24 | minSymbols: 1,
25 | },
26 | {
27 | message:
28 | 'Password must be at least 8 characters long and contain at least one symbol',
29 | },
30 | )
31 | password!: string;
32 |
33 | @Field()
34 | @IsNotEmpty()
35 | @IsString()
36 | firstName!: string;
37 |
38 | @Field()
39 | @IsNotEmpty()
40 | @IsString()
41 | lastName!: string;
42 | }
43 |
--------------------------------------------------------------------------------
/src/modules/user/api/graphql/presentation/model/paginated-user.model.ts:
--------------------------------------------------------------------------------
1 | import { ObjectType } from '@nestjs/graphql';
2 | import { UserModel } from './user.model';
3 | import { Paginated } from '../../../../../../libs/api/graphql/paginated.type';
4 |
5 | @ObjectType()
6 | export class PaginatedUser extends Paginated(UserModel) {}
7 |
--------------------------------------------------------------------------------
/src/modules/user/api/graphql/presentation/model/user.model.ts:
--------------------------------------------------------------------------------
1 | import { Field, ObjectType } from '@nestjs/graphql';
2 | import { User } from '../../../../domain/entity/user.entity';
3 |
4 | @ObjectType()
5 | export class UserModel {
6 | @Field(() => String)
7 | readonly id!: string;
8 |
9 | @Field({ nullable: true })
10 | readonly firstName?: string;
11 |
12 | @Field({ nullable: true })
13 | readonly lastName?: string;
14 |
15 | @Field(() => String)
16 | readonly email!: string;
17 |
18 | @Field(() => Date, { nullable: true })
19 | readonly createdAt?: Date;
20 | }
21 |
22 | export const toUserModel = (user: User): UserModel => ({
23 | id: user.id,
24 | firstName: user.props.firstName,
25 | lastName: user.props.lastName,
26 | email: user.props.email,
27 | createdAt: user.props.createdAt,
28 | });
29 |
--------------------------------------------------------------------------------
/src/modules/user/api/graphql/resolver/user.resolver.ts:
--------------------------------------------------------------------------------
1 | import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
2 | import { CommandBus, QueryBus } from '@nestjs/cqrs';
3 | import { User } from '../../../domain/entity/user.entity';
4 | import { getOrThrowWith, map } from 'effect/Option';
5 | import { AuthRoles } from '../../../../../libs/decorator/auth.decorator';
6 | import { ApiRole } from '../../../../../libs/api/api-role.enum';
7 | import { GetUserByIdQuery } from '../../../application/query/get-user-by-id.query';
8 | import { toUserModel, UserModel } from '../presentation/model/user.model';
9 | import { GetAllUsersQuery } from '../../../application/query/get-all-users.query';
10 | import { CreateUserInput } from '../presentation/input/create-user.input';
11 | import { CreateUserCommand } from '../../../application/command/create-user.command';
12 | import { UserRole } from '../../../domain/value-object/user-role.enum';
13 | import { GraphQLException } from '@nestjs/graphql/dist/exceptions';
14 | import { Collection } from '../../../../../libs/api/rest/collection.interface';
15 | import { HttpStatus } from '@nestjs/common';
16 |
17 | @Resolver(() => UserModel)
18 | export class UserResolver {
19 | constructor(
20 | private readonly queryBus: QueryBus,
21 | private readonly commandBus: CommandBus,
22 | ) {}
23 |
24 | @AuthRoles(ApiRole.ADMIN)
25 | @Mutation(() => String)
26 | async createUser(@Args('input') input: CreateUserInput) {
27 | return getOrThrowWith(
28 | map(
29 | await this.commandBus.execute(
30 | new CreateUserCommand(
31 | input.email,
32 | input.password,
33 | input.firstName,
34 | input.lastName,
35 | UserRole.USER,
36 | ),
37 | ),
38 | (user: User) => user.id,
39 | ),
40 | () =>
41 | new GraphQLException('Error in User Creation', {
42 | extensions: {
43 | http: {
44 | status: HttpStatus.BAD_REQUEST,
45 | },
46 | },
47 | }),
48 | );
49 | }
50 |
51 | @AuthRoles(ApiRole.ADMIN)
52 | @Query(() => [UserModel])
53 | async getUsers(): Promise {
54 | const users: Collection = await this.queryBus.execute(
55 | new GetAllUsersQuery(),
56 | );
57 | return users.items.map(toUserModel);
58 | }
59 |
60 | @AuthRoles(ApiRole.ADMIN)
61 | @Query(() => UserModel, {})
62 | async getUser(
63 | @Args('id', { type: () => String }) id: string,
64 | ): Promise {
65 | return getOrThrowWith(
66 | map(await this.queryBus.execute(new GetUserByIdQuery(id)), toUserModel),
67 | () =>
68 | new GraphQLException('User Not Found', {
69 | extensions: {
70 | http: {
71 | status: HttpStatus.NOT_FOUND,
72 | },
73 | },
74 | }),
75 | );
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/modules/user/api/rest/controller/user.controller.ts:
--------------------------------------------------------------------------------
1 | import {
2 | BadRequestException,
3 | Body,
4 | Controller,
5 | Get,
6 | HttpCode,
7 | HttpStatus,
8 | Post,
9 | } from '@nestjs/common';
10 | import { ApiRole } from '../../../../../libs/api/api-role.enum';
11 | import { AuthRoles } from '../../../../../libs/decorator/auth.decorator';
12 | import { CreateUserBody } from '../presentation/body/create-user.body';
13 | import { CommandBus, QueryBus } from '@nestjs/cqrs';
14 | import { CreateUserCommand } from '../../../application/command/create-user.command';
15 | import { UserRole } from '../../../domain/value-object/user-role.enum';
16 | import { getOrThrowWith } from 'effect/Option';
17 | import {
18 | PaginatedResponse,
19 | toPaginatedResponse,
20 | } from '../../../../../libs/api/rest/paginated.response.dto';
21 | import { toUserDto, UserDto } from '../presentation/dto/user.dto';
22 | import { UserParams } from '../presentation/params/user.params';
23 | import { GetAllUsersQuery } from '../../../application/query/get-all-users.query';
24 | import { QueryParams } from '../../../../../libs/decorator/query-params.decorator';
25 | import { QueryParamsValidationPipe } from '../../../../../libs/pipe/query-params-validation.pipe';
26 | import { Collection } from '../../../../../libs/api/rest/collection.interface';
27 | import { User } from '../../../domain/entity/user.entity';
28 |
29 | @Controller('users')
30 | export class UserController {
31 | constructor(
32 | private readonly commandBus: CommandBus,
33 | private readonly queryBus: QueryBus,
34 | ) {}
35 |
36 | @HttpCode(HttpStatus.NO_CONTENT)
37 | @AuthRoles(ApiRole.ADMIN)
38 | @Post()
39 | async createUser(@Body() body: CreateUserBody) {
40 | getOrThrowWith(
41 | await this.commandBus.execute(
42 | this.getCommandForRole(body, UserRole.USER),
43 | ),
44 | () => new BadRequestException('Error in User Creation'),
45 | );
46 | }
47 |
48 | @HttpCode(HttpStatus.NO_CONTENT)
49 | @AuthRoles(ApiRole.ADMIN)
50 | @Post('/admin')
51 | async createAdminUser(@Body() body: CreateUserBody) {
52 | getOrThrowWith(
53 | await this.commandBus.execute(
54 | this.getCommandForRole(body, UserRole.ADMIN),
55 | ),
56 | () => new BadRequestException('Error in Admin Creation'),
57 | );
58 | }
59 |
60 | @AuthRoles(ApiRole.ADMIN)
61 | @Get()
62 | async getUsers(
63 | @QueryParams(new QueryParamsValidationPipe()) params: UserParams,
64 | ): Promise> {
65 | const users: Collection = await this.queryBus.execute(
66 | new GetAllUsersQuery(params),
67 | );
68 | return toPaginatedResponse(
69 | {
70 | items: users.items.map(toUserDto),
71 | total: users.total,
72 | },
73 | params.offset,
74 | params.limit,
75 | );
76 | }
77 |
78 | /**
79 | * Helper function to get the CQRS command depending on User role.
80 | */
81 | getCommandForRole = (
82 | body: CreateUserBody,
83 | role: UserRole,
84 | ): CreateUserCommand => {
85 | return new CreateUserCommand(
86 | body.email,
87 | body.password,
88 | body.firstName,
89 | body.lastName,
90 | role,
91 | );
92 | };
93 | }
94 |
--------------------------------------------------------------------------------
/src/modules/user/api/rest/presentation/body/create-user.body.ts:
--------------------------------------------------------------------------------
1 | import {
2 | IsEmail,
3 | IsNotEmpty,
4 | IsString,
5 | IsStrongPassword,
6 | MaxLength,
7 | } from 'class-validator';
8 |
9 | export class CreateUserBody {
10 | @IsNotEmpty()
11 | @IsString()
12 | @IsEmail()
13 | email!: string;
14 |
15 | @IsNotEmpty()
16 | @IsString()
17 | @MaxLength(20)
18 | @IsStrongPassword(
19 | {
20 | minLength: 8,
21 | minSymbols: 1,
22 | },
23 | {
24 | message:
25 | 'Password must be at least 8 characters long and contain at least one symbol',
26 | },
27 | )
28 | password!: string;
29 |
30 | @IsNotEmpty()
31 | @IsString()
32 | firstName!: string;
33 |
34 | @IsNotEmpty()
35 | @IsString()
36 | lastName!: string;
37 | }
38 |
--------------------------------------------------------------------------------
/src/modules/user/api/rest/presentation/dto/user.dto.ts:
--------------------------------------------------------------------------------
1 | import { User } from '../../../../domain/entity/user.entity';
2 |
3 | export interface UserDto {
4 | id: string;
5 |
6 | firstName?: string;
7 |
8 | lastName?: string;
9 |
10 | email: string;
11 |
12 | createdAt?: Date;
13 | }
14 |
15 | export const toUserDto = (user: User): UserDto => ({
16 | id: user.id,
17 | firstName: user.props.firstName,
18 | lastName: user.props.lastName,
19 | email: user.props.email,
20 | createdAt: user.props.createdAt,
21 | });
22 |
--------------------------------------------------------------------------------
/src/modules/user/api/rest/presentation/params/user-filter.params.ts:
--------------------------------------------------------------------------------
1 | import { IsDateString, IsEmail, IsOptional, IsString } from 'class-validator';
2 |
3 | export class UserFilterParams {
4 | @IsString()
5 | @IsOptional()
6 | id?: string;
7 |
8 | @IsString()
9 | @IsOptional()
10 | firstName?: string;
11 |
12 | @IsString()
13 | @IsOptional()
14 | lastName?: string;
15 |
16 | @IsString()
17 | @IsEmail()
18 | @IsOptional()
19 | email?: string;
20 |
21 | @IsOptional()
22 | @IsDateString()
23 | createdAt?: Date;
24 | }
25 |
--------------------------------------------------------------------------------
/src/modules/user/api/rest/presentation/params/user-sort.params.ts:
--------------------------------------------------------------------------------
1 | import { IsEnum, IsOptional } from 'class-validator';
2 | import { SortingType } from '../../../../../../libs/api/rest/sorting-type.enum';
3 | import { Type } from 'class-transformer';
4 |
5 | export class UserSortParams {
6 | @IsOptional()
7 | @IsEnum(SortingType)
8 | @Type(() => String)
9 | createdAt?: SortingType = SortingType.DESC;
10 | }
11 |
--------------------------------------------------------------------------------
/src/modules/user/api/rest/presentation/params/user.params.ts:
--------------------------------------------------------------------------------
1 | import { PaginatedQueryParams } from '../../../../../../libs/api/rest/paginated-query-params.dto';
2 | import { UserFilterParams } from './user-filter.params';
3 | import { Type } from 'class-transformer';
4 | import { IsOptional, ValidateNested } from 'class-validator';
5 | import { UserSortParams } from './user-sort.params';
6 |
7 | export class UserParams extends PaginatedQueryParams {
8 | @IsOptional()
9 | @ValidateNested()
10 | @Type(() => UserFilterParams)
11 | filter?: UserFilterParams;
12 |
13 | @IsOptional()
14 | @ValidateNested()
15 | @Type(() => UserSortParams)
16 | sort?: UserSortParams;
17 | }
18 |
--------------------------------------------------------------------------------
/src/modules/user/application/command/create-user.command.ts:
--------------------------------------------------------------------------------
1 | import { UserRole } from '../../domain/value-object/user-role.enum';
2 |
3 | export class CreateUserCommand {
4 | constructor(
5 | readonly email: string,
6 | readonly password: string,
7 | readonly firstName: string,
8 | readonly lastName: string,
9 | readonly role: UserRole,
10 | ) {}
11 | }
12 |
--------------------------------------------------------------------------------
/src/modules/user/application/handler/command/create-user.handler.ts:
--------------------------------------------------------------------------------
1 | import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
2 | import { Inject } from '@nestjs/common';
3 | import { Option } from 'effect/Option';
4 | import { CreateUserCommand } from '../../command/create-user.command';
5 | import { CreateUserUseCase } from '../../use-case/create-user.use-case';
6 | import { CREATE_USER_USE_CASE } from '../../../user.tokens';
7 | import { UserState } from '../../../domain/value-object/user-state.enum';
8 | import { User } from '../../../domain/entity/user.entity';
9 |
10 | @CommandHandler(CreateUserCommand)
11 | export class CreateUserHandler implements ICommandHandler {
12 | constructor(
13 | @Inject(CREATE_USER_USE_CASE)
14 | private readonly createUserUseCase: CreateUserUseCase,
15 | ) {}
16 | async execute(command: CreateUserCommand): Promise> {
17 | return await this.createUserUseCase.execute({
18 | firstName: command.firstName,
19 | lastName: command.lastName,
20 | password: command.password,
21 | email: command.email,
22 | role: command.role,
23 | state: UserState.ACTIVE,
24 | });
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/modules/user/application/handler/command/register-user.handler.ts:
--------------------------------------------------------------------------------
1 | import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
2 | import { RegisterUserCommand } from '../../../../auth/application/command/register-user.command';
3 | import { CREATE_USER_USE_CASE } from '../../../user.tokens';
4 | import { Inject } from '@nestjs/common';
5 | import { Option } from 'effect/Option';
6 | import { UserState } from '../../../domain/value-object/user-state.enum';
7 | import { CreateUserUseCase } from '../../use-case/create-user.use-case';
8 | import { UserRole } from '../../../domain/value-object/user-role.enum';
9 | import { User } from '../../../domain/entity/user.entity';
10 |
11 | @CommandHandler(RegisterUserCommand)
12 | export class RegisterUserHandler
13 | implements ICommandHandler
14 | {
15 | constructor(
16 | @Inject(CREATE_USER_USE_CASE)
17 | private readonly createUserUseCase: CreateUserUseCase,
18 | ) {}
19 | async execute(command: RegisterUserCommand): Promise> {
20 | return await this.createUserUseCase.execute({
21 | firstName: command.firstName,
22 | lastName: command.lastName,
23 | password: command.password,
24 | email: command.email,
25 | role: UserRole.USER,
26 | state: UserState.ACTIVE,
27 | });
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/modules/user/application/handler/query/check-auth-user-by-id.handler.ts:
--------------------------------------------------------------------------------
1 | import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
2 | import { UserRepository } from '../../../domain/repository/user.repository.interface';
3 | import { Inject } from '@nestjs/common';
4 | import { USER_REPOSITORY } from '../../../user.tokens';
5 | import { CheckAuthUserByIdQuery } from '../../../../auth/application/query/check-auth-user-by-id.query';
6 |
7 | @QueryHandler(CheckAuthUserByIdQuery)
8 | export class CheckAuthUserByIdHandler
9 | implements IQueryHandler
10 | {
11 | constructor(
12 | @Inject(USER_REPOSITORY)
13 | private readonly userRepository: UserRepository,
14 | ) {}
15 |
16 | async execute(query: CheckAuthUserByIdQuery): Promise {
17 | return await this.userRepository.checkActiveUserById(query.id);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/modules/user/application/handler/query/get-all-users.handler.ts:
--------------------------------------------------------------------------------
1 | import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
2 | import { UserRepository } from '../../../domain/repository/user.repository.interface';
3 | import { Inject } from '@nestjs/common';
4 | import { USER_REPOSITORY } from '../../../user.tokens';
5 | import { GetAllUsersQuery } from '../../query/get-all-users.query';
6 | import { Collection } from '../../../../../libs/api/rest/collection.interface';
7 | import { User } from '../../../domain/entity/user.entity';
8 |
9 | @QueryHandler(GetAllUsersQuery)
10 | export class GetAllUsersHandler implements IQueryHandler {
11 | constructor(
12 | @Inject(USER_REPOSITORY)
13 | private readonly userRepository: UserRepository,
14 | ) {}
15 |
16 | async execute(query?: GetAllUsersQuery): Promise> {
17 | return await this.userRepository.getAllUsers(query?.params);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/modules/user/application/handler/query/get-auth-user-by-email.handler.ts:
--------------------------------------------------------------------------------
1 | import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
2 | import { GetAuthUserByEmailQuery } from '../../../../auth/application/query/get-auth-user-by-email.query';
3 | import { UserRepository } from '../../../domain/repository/user.repository.interface';
4 | import { Inject } from '@nestjs/common';
5 | import { USER_REPOSITORY } from '../../../user.tokens';
6 | import { Option } from 'effect/Option';
7 | import { User } from '../../../domain/entity/user.entity';
8 |
9 | @QueryHandler(GetAuthUserByEmailQuery)
10 | export class GetAuthUserByEmailHandler
11 | implements IQueryHandler
12 | {
13 | constructor(
14 | @Inject(USER_REPOSITORY)
15 | private readonly userRepository: UserRepository,
16 | ) {}
17 |
18 | async execute(query: GetAuthUserByEmailQuery): Promise> {
19 | return await this.userRepository.getUserByEmail(query.email);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/modules/user/application/handler/query/get-user-by-id.handler.ts:
--------------------------------------------------------------------------------
1 | import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
2 | import { UserRepository } from '../../../domain/repository/user.repository.interface';
3 | import { Inject } from '@nestjs/common';
4 | import { USER_REPOSITORY } from '../../../user.tokens';
5 | import { Option } from 'effect/Option';
6 | import { User } from '../../../domain/entity/user.entity';
7 | import { GetUserByIdQuery } from '../../query/get-user-by-id.query';
8 |
9 | @QueryHandler(GetUserByIdQuery)
10 | export class GetUserByIdHandler implements IQueryHandler {
11 | constructor(
12 | @Inject(USER_REPOSITORY)
13 | private readonly userRepository: UserRepository,
14 | ) {}
15 |
16 | async execute(query: GetUserByIdQuery): Promise> {
17 | return await this.userRepository.getUserById(query.id);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/modules/user/application/query/get-all-users.query.ts:
--------------------------------------------------------------------------------
1 | import { UserParams } from '../../api/rest/presentation/params/user.params';
2 |
3 | export class GetAllUsersQuery {
4 | constructor(readonly params?: UserParams) {}
5 | }
6 |
--------------------------------------------------------------------------------
/src/modules/user/application/query/get-user-by-email.query.ts:
--------------------------------------------------------------------------------
1 | export class GetUserByEmailQuery {
2 | constructor(readonly email: string) {}
3 | }
4 |
--------------------------------------------------------------------------------
/src/modules/user/application/query/get-user-by-id.query.ts:
--------------------------------------------------------------------------------
1 | export class GetUserByIdQuery {
2 | constructor(readonly id: string) {}
3 | }
4 |
--------------------------------------------------------------------------------
/src/modules/user/application/use-case/create-user.use-case.ts:
--------------------------------------------------------------------------------
1 | import { UseCase } from '../../../../libs/ddd/use-case.interface';
2 | import { isNone, isSome, none, Option } from 'effect/Option';
3 | import { Inject, Injectable } from '@nestjs/common';
4 | import { UserRepository } from '../../domain/repository/user.repository.interface';
5 | import { genSalt, hash } from 'bcryptjs';
6 | import { USER_REPOSITORY } from '../../user.tokens';
7 | import { CustomConflictException } from '../../../../libs/exceptions/custom-conflict.exception';
8 | import { User, UserProps } from '../../domain/entity/user.entity';
9 | import { EventPublisher } from '@nestjs/cqrs';
10 |
11 | @Injectable()
12 | export class CreateUserUseCase implements UseCase> {
13 | constructor(
14 | @Inject(USER_REPOSITORY)
15 | private readonly userRepository: UserRepository,
16 | private readonly eventPublisher: EventPublisher,
17 | ) {}
18 |
19 | async execute(data: UserProps): Promise> {
20 | const hashedPassword = await hash(data.password, await genSalt());
21 | const found = await this.userRepository.getUserByEmail(data.email);
22 | if (isSome(found))
23 | throw new CustomConflictException(found.value.props.email);
24 | const user = await this.userRepository.createUser({
25 | email: data.email,
26 | password: hashedPassword,
27 | firstName: data.firstName,
28 | lastName: data.lastName,
29 | role: data.role,
30 | state: data.state,
31 | });
32 | if (isNone(user)) return none();
33 | this.eventPublisher.mergeObjectContext(user.value);
34 | // Dispatch Event from Aggregate Root
35 | user.value.commit();
36 | return user;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/modules/user/domain/entity/user.entity.ts:
--------------------------------------------------------------------------------
1 | import { AggregateRoot } from '@nestjs/cqrs';
2 | import { UserRole } from '../value-object/user-role.enum';
3 | import { UserState } from '../value-object/user-state.enum';
4 | import { CreatedUserEvent } from '../event/created-user.event';
5 |
6 | export interface UserProps {
7 | email: string;
8 | password: string;
9 | firstName?: string;
10 | lastName?: string;
11 | role: UserRole;
12 | state: UserState;
13 | createdAt?: Date;
14 | updatedAt?: Date;
15 | }
16 |
17 | export class User extends AggregateRoot {
18 | id: string;
19 | props: UserProps;
20 |
21 | constructor(id: string, props: UserProps) {
22 | super();
23 | this.id = id;
24 | this.props = props;
25 | }
26 |
27 | create() {
28 | this.apply(new CreatedUserEvent(this));
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/modules/user/domain/event/created-user.event.ts:
--------------------------------------------------------------------------------
1 | import { DomainEvent } from '../../../../libs/ddd/domain-event.abstract';
2 | import { IEvent } from '@nestjs/cqrs';
3 | import { User } from '../entity/user.entity';
4 |
5 | export class CreatedUserEvent extends DomainEvent implements IEvent {
6 | constructor(
7 | user: User,
8 | options: { correlationId?: string; version?: number } = {},
9 | ) {
10 | super('CreatedUserEvent', user, options);
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/modules/user/domain/repository/user.repository.interface.ts:
--------------------------------------------------------------------------------
1 | import { Option } from 'effect/Option';
2 | import { Collection } from '../../../../libs/api/rest/collection.interface';
3 | import { PaginatedQueryParams } from '../../../../libs/api/rest/paginated-query-params.dto';
4 | import { User, UserProps } from '../entity/user.entity';
5 |
6 | export interface UserRepository {
7 | createUser(data: UserProps): Promise>;
8 |
9 | getUserByEmail(email: string): Promise >;
10 |
11 | getUserById(id: string): Promise >;
12 |
13 | checkActiveUserById(id: string): Promise;
14 |
15 | getAllUsers(
16 | params?: T,
17 | ): Promise>;
18 | }
19 |
--------------------------------------------------------------------------------
/src/modules/user/domain/value-object/user-role.enum.ts:
--------------------------------------------------------------------------------
1 | export enum UserRole {
2 | ADMIN = 0,
3 | USER = 1,
4 | }
5 |
--------------------------------------------------------------------------------
/src/modules/user/domain/value-object/user-state.enum.ts:
--------------------------------------------------------------------------------
1 | export enum UserState {
2 | ACTIVE = 'ACTIVE',
3 | DISABLED = 'DISABLED',
4 | }
5 |
--------------------------------------------------------------------------------
/src/modules/user/infrastructure/database/entity/user.entity.ts:
--------------------------------------------------------------------------------
1 | import { Entity, Enum, PrimaryKey, Property } from '@mikro-orm/core';
2 | import { v4 } from 'uuid';
3 | import { UserRole } from '../../../domain/value-object/user-role.enum';
4 | import { UserState } from '../../../domain/value-object/user-state.enum';
5 | import { BaseEntity } from '../../../../../libs/database/base.entity';
6 |
7 | @Entity({
8 | tableName: 'users',
9 | })
10 | export class UserEntity extends BaseEntity {
11 | @PrimaryKey()
12 | override id: string = v4();
13 |
14 | @Property({ unique: true, index: true })
15 | email!: string;
16 |
17 | @Property()
18 | password!: string;
19 |
20 | @Property({ nullable: true })
21 | firstName?: string;
22 |
23 | @Property({ nullable: true })
24 | lastName?: string;
25 |
26 | @Enum({ items: () => UserRole })
27 | role!: UserRole;
28 |
29 | @Enum({ items: () => UserState })
30 | state!: UserState;
31 | }
32 |
--------------------------------------------------------------------------------
/src/modules/user/infrastructure/database/mapper/user.mapper.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { Mapper } from '../../../../../libs/ddd/mapper.interface';
3 | import { User } from '../../../domain/entity/user.entity';
4 | import { UserEntity } from '../entity/user.entity';
5 |
6 | @Injectable()
7 | export class UserMapper implements Mapper {
8 | toDomain(record: UserEntity): User {
9 | return new User(record.id, {
10 | email: record.email,
11 | password: record.password,
12 | firstName: record.firstName,
13 | lastName: record.lastName,
14 | role: record.role,
15 | state: record.state,
16 | createdAt: record.createdAt,
17 | updatedAt: record.updatedAt,
18 | });
19 | }
20 |
21 | toPersistence(entity: User): UserEntity {
22 | const props = entity.props;
23 | return {
24 | id: entity.id,
25 | email: props.email,
26 | password: props.password,
27 | firstName: props.firstName,
28 | lastName: props.lastName,
29 | role: props.role,
30 | state: props.state,
31 | createdAt: props.createdAt,
32 | updatedAt: props.updatedAt,
33 | } as UserEntity;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/modules/user/infrastructure/database/repository/user.repository.ts:
--------------------------------------------------------------------------------
1 | import { fromNullable, isSome, map, Option } from 'effect/Option';
2 | import { UserRepository } from '../../../domain/repository/user.repository.interface';
3 | import { UserState } from '../../../domain/value-object/user-state.enum';
4 | import { EntityRepository, QueryBuilder } from '@mikro-orm/postgresql';
5 | import { InjectRepository } from '@mikro-orm/nestjs';
6 | import { Collection } from '../../../../../libs/api/rest/collection.interface';
7 | import { UserParams } from '../../../api/rest/presentation/params/user.params';
8 | import { endOfDay, startOfDay } from 'date-fns';
9 | import { SortingType } from '../../../../../libs/api/rest/sorting-type.enum';
10 | import { User, UserProps } from '../../../domain/entity/user.entity';
11 | import { UserMapper } from '../mapper/user.mapper';
12 | import { UserEntity } from '../entity/user.entity';
13 | import { pipe } from 'effect';
14 |
15 | export class UserRepositoryImpl implements UserRepository {
16 | constructor(
17 | @InjectRepository(UserEntity)
18 | private readonly mikroOrmRepository: EntityRepository,
19 | private readonly mapper: UserMapper,
20 | ) {}
21 |
22 | async getUserByEmail(email: string): Promise> {
23 | return map(
24 | fromNullable(await this.mikroOrmRepository.findOne({ email })),
25 | this.mapper.toDomain,
26 | );
27 | }
28 |
29 | async getUserById(id: string): Promise > {
30 | return map(
31 | fromNullable(await this.mikroOrmRepository.findOne({ id })),
32 | this.mapper.toDomain,
33 | );
34 | }
35 |
36 | async checkActiveUserById(id: string): Promise {
37 | return isSome(
38 | fromNullable(
39 | await this.mikroOrmRepository.findOne({ id, state: UserState.ACTIVE }),
40 | ),
41 | );
42 | }
43 |
44 | async createUser(data: UserProps): Promise> {
45 | const entity = this.mikroOrmRepository.create({
46 | email: data.email,
47 | password: data.password,
48 | firstName: data.firstName,
49 | lastName: data.lastName,
50 | role: data.role,
51 | state: data.state,
52 | createdAt: new Date(),
53 | updatedAt: new Date(),
54 | });
55 | await this.mikroOrmRepository.insert(entity);
56 | return pipe(map(fromNullable(entity), this.mapper.toDomain), (user) => {
57 | if (isSome(user)) user.value.create();
58 | return user;
59 | });
60 | }
61 |
62 | async getAllUsers(params?: UserParams): Promise> {
63 | const queryBuilder = this.mikroOrmRepository.createQueryBuilder('user');
64 | if (params) this.applyFilters(queryBuilder, params);
65 | const [items, total] = await queryBuilder.getResultAndCount();
66 | return {
67 | items: items.map(this.mapper.toDomain),
68 | total,
69 | };
70 | }
71 |
72 | private applyFilters(
73 | queryBuilder: QueryBuilder,
74 | params: UserParams,
75 | ) {
76 | const filters = [
77 | params.filter?.id && {
78 | id: params.filter.id,
79 | },
80 | params.filter?.firstName && {
81 | firstName: {
82 | $ilike: `%${params.filter.firstName}%`,
83 | },
84 | },
85 | params.filter?.lastName && {
86 | lastName: {
87 | $ilike: `%${params.filter.lastName}%`,
88 | },
89 | },
90 | params.filter?.email && {
91 | email: {
92 | $ilike: `%${params.filter.email}%`,
93 | },
94 | },
95 | params.filter?.createdAt && {
96 | createdAt: {
97 | $gte: startOfDay(params.filter.createdAt),
98 | $lte: endOfDay(params.filter.createdAt),
99 | },
100 | },
101 | ];
102 | filters.filter(Boolean).forEach((filter) => {
103 | if (filter) queryBuilder.andWhere(filter);
104 | });
105 | if (params.sort?.createdAt)
106 | queryBuilder.orderBy({ createdAt: params?.sort?.createdAt });
107 | else queryBuilder.orderBy({ createdAt: SortingType.DESC });
108 | queryBuilder.offset(params.offset);
109 | queryBuilder.limit(params.limit);
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/src/modules/user/user.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { MikroOrmModule } from '@mikro-orm/nestjs';
3 | import { UserRepositoryImpl } from './infrastructure/database/repository/user.repository';
4 | import { CREATE_USER_USE_CASE, USER_REPOSITORY } from './user.tokens';
5 | import { GetAuthUserByEmailHandler } from './application/handler/query/get-auth-user-by-email.handler';
6 | import { CheckAuthUserByIdHandler } from './application/handler/query/check-auth-user-by-id.handler';
7 | import { RegisterUserHandler } from './application/handler/command/register-user.handler';
8 | import { UserController } from './api/rest/controller/user.controller';
9 | import { CreateUserUseCase } from './application/use-case/create-user.use-case';
10 | import { CreateUserHandler } from './application/handler/command/create-user.handler';
11 | import { GetAllUsersHandler } from './application/handler/query/get-all-users.handler';
12 | import { UserMapper } from './infrastructure/database/mapper/user.mapper';
13 | import { UserResolver } from './api/graphql/resolver/user.resolver';
14 | import { GetUserByIdHandler } from './application/handler/query/get-user-by-id.handler';
15 | import { UserEntity } from './infrastructure/database/entity/user.entity';
16 |
17 | @Module({
18 | imports: [MikroOrmModule.forFeature([UserEntity])],
19 | controllers: [UserController],
20 | providers: [
21 | RegisterUserHandler,
22 | CreateUserHandler,
23 | GetAllUsersHandler,
24 | GetAuthUserByEmailHandler,
25 | GetUserByIdHandler,
26 | CheckAuthUserByIdHandler,
27 | UserMapper,
28 | UserResolver,
29 | {
30 | provide: USER_REPOSITORY,
31 | useClass: UserRepositoryImpl,
32 | },
33 | {
34 | provide: CREATE_USER_USE_CASE,
35 | useClass: CreateUserUseCase,
36 | },
37 | ],
38 | exports: [],
39 | })
40 | export class UserModule {}
41 |
--------------------------------------------------------------------------------
/src/modules/user/user.tokens.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Constants for NestJS Dependency Injection System.
3 | */
4 | export const USER_REPOSITORY = 'USER_REPOSITORY';
5 | export const CREATE_USER_USE_CASE = 'CREATE_USER_USE_CASE';
6 |
--------------------------------------------------------------------------------
/test/config/jest-e2e.config.ts:
--------------------------------------------------------------------------------
1 | import { Config } from 'jest';
2 | import baseConfig from './jest.config.base';
3 |
4 | const e2eConfig: Config = {
5 | ...baseConfig,
6 | testRegex: '.e2e-spec.ts$',
7 | };
8 |
9 | export default e2eConfig;
10 |
--------------------------------------------------------------------------------
/test/config/jest-unit.config.ts:
--------------------------------------------------------------------------------
1 | import { Config } from 'jest';
2 | import baseConfig from './jest.config.base';
3 |
4 | const unitConfig: Config = {
5 | ...baseConfig,
6 | testRegex: '.*\\.spec\\.ts$',
7 | coverageDirectory: '../coverage',
8 | collectCoverageFrom: ['**/*.(t|j)s'],
9 | };
10 |
11 | export default unitConfig;
12 |
--------------------------------------------------------------------------------
/test/config/jest.config.base.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from 'jest';
2 |
3 | const baseConfig: Config = {
4 | rootDir: '../',
5 | moduleFileExtensions: ['js', 'json', 'ts'],
6 | testEnvironment: 'node',
7 | transform: {
8 | '^.+\\.(t|j)s$': 'ts-jest',
9 | },
10 | setupFiles: ['./config/jest.setup.ts'],
11 | };
12 |
13 | export default baseConfig;
14 |
--------------------------------------------------------------------------------
/test/config/jest.setup.ts:
--------------------------------------------------------------------------------
1 | import { config } from 'dotenv';
2 |
3 | config({ path: '.env.test' });
4 |
--------------------------------------------------------------------------------
/test/e2e/auth/auth.e2e-spec.ts:
--------------------------------------------------------------------------------
1 | import { HttpStatus, INestApplication } from '@nestjs/common';
2 | import * as request from 'supertest';
3 | import { MikroORM } from '@mikro-orm/core';
4 | import {
5 | clearDatabase,
6 | initializeApp,
7 | resetDatabase,
8 | } from '../util/setup-e2e-test.util';
9 |
10 | describe('Auth (E2E)', () => {
11 | let app: INestApplication;
12 | let orm: MikroORM;
13 |
14 | beforeAll(async () => {
15 | ({ app, orm } = await initializeApp());
16 | await resetDatabase(orm);
17 | });
18 |
19 | afterEach(async () => {
20 | await clearDatabase(orm);
21 | });
22 |
23 | afterAll(async () => {
24 | await app.close();
25 | });
26 |
27 | const credentials = {
28 | email: 'test@example.com',
29 | password: 'Test1234!',
30 | firstName: 'John',
31 | lastName: 'Doe',
32 | };
33 |
34 | const userFactory = (overrides = {}) => ({
35 | ...credentials,
36 | ...overrides,
37 | });
38 |
39 | it('should fail to create a user with a weak password', async () => {
40 | await request(app.getHttpServer())
41 | .post('/auth/signup')
42 | .send(userFactory({ password: '1234' })) // Weak password
43 | .expect(HttpStatus.BAD_REQUEST);
44 | });
45 |
46 | it('should sign up a user successfully', async () => {
47 | const response = await request(app.getHttpServer())
48 | .post('/auth/signup')
49 | .send(userFactory())
50 | .expect(HttpStatus.CREATED);
51 | expect(response.body).toMatchObject({
52 | token: expect.any(String),
53 | expiresIn: expect.any(String),
54 | refreshToken: expect.any(String),
55 | refreshExpiresIn: expect.any(String),
56 | user: {
57 | id: expect.any(String),
58 | email: credentials.email,
59 | role: expect.any(Number),
60 | },
61 | });
62 | });
63 |
64 | it('should not allow duplicate signups', async () => {
65 | await request(app.getHttpServer())
66 | .post('/auth/signup')
67 | .send(userFactory())
68 | .expect(HttpStatus.CREATED);
69 | await request(app.getHttpServer())
70 | .post('/auth/signup')
71 | .send(userFactory())
72 | .expect(HttpStatus.CONFLICT);
73 | });
74 |
75 | it('should return 400 for invalid email format', async () => {
76 | await request(app.getHttpServer())
77 | .post('/auth/login')
78 | .send(userFactory({ email: 'wrong-email' }))
79 | .expect(HttpStatus.BAD_REQUEST);
80 | });
81 |
82 | it('should return 400 for incorrect password format', async () => {
83 | await request(app.getHttpServer())
84 | .post('/auth/login')
85 | .send(userFactory({ password: 'wrong' }))
86 | .expect(HttpStatus.BAD_REQUEST);
87 | });
88 |
89 | it('should return 401 for invalid login credentials', async () => {
90 | await request(app.getHttpServer())
91 | .post('/auth/login')
92 | .send({ email: credentials.email, password: credentials.password })
93 | .expect(HttpStatus.UNAUTHORIZED);
94 | });
95 |
96 | it('should login an existing user', async () => {
97 | await request(app.getHttpServer())
98 | .post('/auth/signup')
99 | .send(userFactory())
100 | .expect(HttpStatus.CREATED);
101 |
102 | const response = await request(app.getHttpServer())
103 | .post('/auth/login')
104 | .send({ email: credentials.email, password: credentials.password })
105 | .expect(HttpStatus.CREATED);
106 | expect(response.body).toMatchObject({
107 | token: expect.any(String),
108 | expiresIn: expect.any(String),
109 | refreshToken: expect.any(String),
110 | refreshExpiresIn: expect.any(String),
111 | user: {
112 | id: expect.any(String),
113 | email: credentials.email,
114 | role: expect.any(Number),
115 | },
116 | });
117 | });
118 | });
119 |
--------------------------------------------------------------------------------
/test/e2e/health/health.e2e-spec.ts:
--------------------------------------------------------------------------------
1 | import { INestApplication } from '@nestjs/common';
2 | import * as request from 'supertest';
3 | import { MikroORM } from '@mikro-orm/core';
4 | import { initializeApp, resetDatabase } from '../util/setup-e2e-test.util';
5 |
6 | describe('HealthCheck (E2E)', () => {
7 | let app: INestApplication;
8 | let orm: MikroORM;
9 |
10 | beforeAll(async () => {
11 | ({ app, orm } = await initializeApp());
12 | await resetDatabase(orm);
13 | });
14 |
15 | afterAll(async () => {
16 | await app.close();
17 | });
18 |
19 | it('should always return 200', () => {
20 | return request(app.getHttpServer()).get('/health').expect(200);
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/test/e2e/user/user.e2e-spec.ts:
--------------------------------------------------------------------------------
1 | import { HttpStatus, INestApplication } from '@nestjs/common';
2 | import { MikroORM } from '@mikro-orm/core';
3 | import { initializeApp, resetDatabase } from '../util/setup-e2e-test.util';
4 | import * as request from 'supertest';
5 | import { CreateUserBody } from '../../../src/modules/user/api/rest/presentation/body/create-user.body';
6 |
7 | describe('User (E2E)', () => {
8 | let app: INestApplication;
9 | let orm: MikroORM;
10 |
11 | beforeAll(async () => {
12 | ({ app, orm } = await initializeApp());
13 | await resetDatabase(orm);
14 | });
15 |
16 | afterAll(async () => {
17 | await app.close();
18 | });
19 |
20 | const user: CreateUserBody = {
21 | email: 'user@email.com',
22 | password: 'Test1234!',
23 | firstName: 'Andrea',
24 | lastName: 'Acampora',
25 | };
26 |
27 | it('should not be possible to create a user without admin permissions', async () => {
28 | await request(app.getHttpServer())
29 | .post('/users')
30 | .send(user)
31 | .expect(HttpStatus.FORBIDDEN);
32 | });
33 |
34 | it('should be possible to create a user by an admin', async () => {
35 | await request(app.getHttpServer())
36 | .post('/users')
37 | .set('Authorization', `Bearer ${await getAdminToken()}`)
38 | .send(user)
39 | .expect(HttpStatus.NO_CONTENT);
40 | });
41 |
42 | it('should be possible to retrieve the users list by an admin', async () => {
43 | const response = await request(app.getHttpServer())
44 | .get('/users')
45 | .set('Authorization', `Bearer ${await getAdminToken()}`)
46 | .expect(HttpStatus.OK);
47 | expect(response.body.data.length).toBeGreaterThan(0);
48 | });
49 |
50 | const getAdminToken = async () => {
51 | const adminLoginResponse = await request(app.getHttpServer())
52 | .post('/auth/login')
53 | .send({ email: 'admin@email.com', password: 'Test1234!' })
54 | .expect(HttpStatus.CREATED);
55 | return adminLoginResponse.body.token;
56 | };
57 | });
58 |
--------------------------------------------------------------------------------
/test/e2e/util/setup-e2e-test.util.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { INestApplication, ValidationPipe } from '@nestjs/common';
3 | import { MikroORM } from '@mikro-orm/core';
4 | import { AppModule } from '../../../src/app.module';
5 |
6 | export async function initializeApp(): Promise<{
7 | app: INestApplication;
8 | orm: MikroORM;
9 | }> {
10 | const moduleFixture: TestingModule = await Test.createTestingModule({
11 | imports: [AppModule],
12 | }).compile();
13 | const app = moduleFixture.createNestApplication();
14 | app.useGlobalPipes(
15 | new ValidationPipe({
16 | transform: true,
17 | whitelist: true,
18 | forbidNonWhitelisted: true,
19 | }),
20 | );
21 | await app.init();
22 | const orm = app.get(MikroORM);
23 | return { app, orm };
24 | }
25 |
26 | export async function resetDatabase(orm: MikroORM): Promise {
27 | await clearDatabase(orm);
28 | await orm.getMigrator().up();
29 | }
30 |
31 | export async function clearDatabase(orm: MikroORM): Promise {
32 | await orm.getSchemaGenerator().refreshDatabase();
33 | await orm.getSchemaGenerator().updateSchema();
34 | await orm.getMigrator().down();
35 | await orm.getSchemaGenerator().updateSchema();
36 | }
37 |
--------------------------------------------------------------------------------
/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
4 | }
5 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "declaration": true,
5 | "removeComments": true,
6 | "emitDecoratorMetadata": true,
7 | "experimentalDecorators": true,
8 | "allowSyntheticDefaultImports": true,
9 | "target": "ES2021",
10 | "sourceMap": true,
11 | "outDir": "./dist",
12 | "baseUrl": "./",
13 | "incremental": true,
14 | "skipLibCheck": true,
15 | "strictBindCallApply": false,
16 | "forceConsistentCasingInFileNames": false,
17 | "noFallthroughCasesInSwitch": false,
18 | "strict": true,
19 | "noEmitOnError": false,
20 | "noImplicitAny": false,
21 | "noImplicitThis": true,
22 | "alwaysStrict": true,
23 | "strictNullChecks": true,
24 | "strictFunctionTypes": true,
25 | "strictPropertyInitialization": true,
26 | "noUnusedLocals": true,
27 | "noUnusedParameters": true,
28 | "noImplicitReturns": true,
29 | "resolveJsonModule": true
30 | }
31 | }
32 |
--------------------------------------------------------------------------------