├── .dockerignore ├── .gitattributes ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── .vscode └── settings.json ├── README.md ├── common ├── config │ └── rush │ │ ├── .npmrc │ │ ├── .npmrc-publish │ │ ├── artifactory.json │ │ ├── command-line.json │ │ ├── common-versions.json │ │ ├── deploy.json │ │ ├── experiments.json │ │ ├── pnpm-lock.yaml │ │ ├── pnpmfile.js │ │ └── version-policies.json ├── git-hooks │ └── commit-msg.sample └── scripts │ ├── install-run-rush.js │ ├── install-run-rushx.js │ └── install-run.js ├── docker-compose.override.yaml.dist ├── docker-compose.yaml ├── docker ├── Dockerfile └── Dockerfile.scheduler ├── docs ├── C4Diagram.drawio ├── adr │ ├── 0001-record-architecture-decisions.md │ ├── 0002-use-modular-monolith-architecture.md │ ├── 0003-di-container-for-each-module.md │ ├── 0004-share-infrastructure-between-all-modules.md │ └── 0005-use-local-contract-for-integration-events.md ├── c4 │ ├── components.jpg │ ├── containers.jpg │ └── system-context.jpg └── images │ ├── es-booking-module.jpg │ ├── es-couch-module.jpg │ ├── es-overview.jpg │ ├── es-review-module.jpg │ └── es-user-module.jpg ├── rush.json ├── src ├── app │ ├── scheduler │ │ ├── .env.dist │ │ ├── .eslintignore │ │ ├── .eslintrc.js │ │ ├── .swcrc │ │ ├── crontab │ │ ├── package.json │ │ ├── src │ │ │ ├── config │ │ │ │ └── config.ts │ │ │ ├── http.client.ts │ │ │ ├── index.ts │ │ │ └── jobs │ │ │ │ └── finish-bookings.job.ts │ │ └── tsconfig.json │ └── travelhoop │ │ ├── .env.dist │ │ ├── .eslintignore │ │ ├── .eslintrc.js │ │ ├── .swcrc │ │ ├── package.json │ │ ├── src │ │ ├── app.ts │ │ ├── config │ │ │ ├── config.ts │ │ │ ├── db-config.ts │ │ │ └── index.ts │ │ ├── container.ts │ │ ├── database │ │ │ └── migrator │ │ │ │ ├── create.ts │ │ │ │ ├── down.ts │ │ │ │ └── up.ts │ │ ├── migrations │ │ │ ├── migration-20210503185713.ts │ │ │ ├── migration-20210504171406.ts │ │ │ ├── migration-20210604132200.ts │ │ │ └── migration-20210605185254.ts │ │ ├── module.loader.ts │ │ ├── server.ts │ │ └── tests │ │ │ └── bootstrap.ts │ │ └── tsconfig.json ├── modules │ ├── booking │ │ ├── .eslintignore │ │ ├── .eslintrc.js │ │ ├── .swcrc │ │ ├── booking.rest │ │ ├── package.json │ │ ├── src │ │ │ ├── api │ │ │ │ ├── booking.module.ts │ │ │ │ └── routes │ │ │ │ │ ├── bookable-couch.router.ts │ │ │ │ │ ├── couch-booking-request.router.ts │ │ │ │ │ └── router.ts │ │ │ ├── application │ │ │ │ ├── bookable-couch │ │ │ │ │ ├── events │ │ │ │ │ │ └── couch-created.event.ts │ │ │ │ │ ├── handlers │ │ │ │ │ │ ├── archive-bookable-couch │ │ │ │ │ │ │ ├── archive-bookable-couch.command.ts │ │ │ │ │ │ │ └── archive-bookable-couch.handler.ts │ │ │ │ │ │ ├── cancel-booking │ │ │ │ │ │ │ ├── cancel-booking.command.ts │ │ │ │ │ │ │ ├── cancel-booking.handler.ts │ │ │ │ │ │ │ └── cancel-booking.validator.ts │ │ │ │ │ │ ├── create-booking │ │ │ │ │ │ │ ├── create-booking.command.ts │ │ │ │ │ │ │ ├── create-booking.handler.ts │ │ │ │ │ │ │ └── create-booking.validator.ts │ │ │ │ │ │ └── finish-bookings │ │ │ │ │ │ │ ├── finish-bookings.command.ts │ │ │ │ │ │ │ ├── finish-bookings.handler.ts │ │ │ │ │ │ │ └── finish-bookings.validator.ts │ │ │ │ │ └── subscribers │ │ │ │ │ │ └── couch-created.subscriber.ts │ │ │ │ └── couch-booking-request │ │ │ │ │ ├── handlers │ │ │ │ │ ├── reject-couch-booking-request │ │ │ │ │ │ ├── reject-couch-booking-request.command.ts │ │ │ │ │ │ ├── reject-couch-booking-request.handler.ts │ │ │ │ │ │ └── reject-couch-booking-request.validator.ts │ │ │ │ │ └── request-couch-booking │ │ │ │ │ │ ├── request-couch-booking.command.ts │ │ │ │ │ │ ├── request-couch-booking.handler.ts │ │ │ │ │ │ └── request-couch-booking.validator.ts │ │ │ │ │ └── subscribers │ │ │ │ │ └── couch-booking-created.subscriber.ts │ │ │ ├── domain │ │ │ │ ├── bookable-couch │ │ │ │ │ ├── entity │ │ │ │ │ │ ├── bookable-couch.ts │ │ │ │ │ │ ├── booking.ts │ │ │ │ │ │ ├── couch-booking.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── unavailable-booking.ts │ │ │ │ │ ├── enum │ │ │ │ │ │ ├── bookable-couch-state.enum.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── error │ │ │ │ │ │ ├── all-couches-are-reserved.error.ts │ │ │ │ │ │ ├── booking-not-found.error.ts │ │ │ │ │ │ ├── booking-unavailable.error.ts │ │ │ │ │ │ ├── cannot-archive-couch.error.ts │ │ │ │ │ │ ├── cannot-book-couch.error.ts │ │ │ │ │ │ ├── cannot-cancel-booking.error.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── event │ │ │ │ │ │ ├── bookable-couch-archived.event.ts │ │ │ │ │ │ ├── bookings-finished.event.ts │ │ │ │ │ │ ├── couch-booking-cancelled.event.ts │ │ │ │ │ │ ├── couch-booking-created.event.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── policy │ │ │ │ │ │ ├── couch-booking-cancellation.policy.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ └── repository │ │ │ │ │ │ ├── bookable-couch.repository.ts │ │ │ │ │ │ └── index.ts │ │ │ │ ├── booking-cancellation │ │ │ │ │ ├── entity │ │ │ │ │ │ ├── booking-cancellation.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── repository │ │ │ │ │ │ ├── booking-cancellation.repository.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ └── subscribers │ │ │ │ │ │ ├── couch-booking-cancelled.subscriber.ts │ │ │ │ │ │ └── index.ts │ │ │ │ ├── couch-booking-request │ │ │ │ │ ├── entity │ │ │ │ │ │ ├── couch-booking-request.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── request-status.ts │ │ │ │ │ ├── error │ │ │ │ │ │ ├── cannot-accept-booking.error.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── event │ │ │ │ │ │ ├── couch-booking-request-created.event.ts │ │ │ │ │ │ ├── couch-booking-status-changed.event.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── repository │ │ │ │ │ │ ├── couch-booking-request.repository.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ └── service │ │ │ │ │ │ ├── couch-booking-request.domain-service.ts │ │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ └── tests │ │ │ │ │ ├── bookable-couch.test.ts │ │ │ │ │ ├── couch-booking-request-domain-service.test.ts │ │ │ │ │ ├── couch-booking-request.test.ts │ │ │ │ │ └── helpers │ │ │ │ │ └── create-couch-booking-request.ts │ │ │ ├── index.ts │ │ │ └── infrastructure │ │ │ │ ├── config.ts │ │ │ │ ├── container.ts │ │ │ │ └── mikro-orm │ │ │ │ ├── entity-schemas │ │ │ │ ├── bookable-couch.entity.ts │ │ │ │ ├── booking-cancellation.entity.ts │ │ │ │ ├── booking.ts │ │ │ │ ├── couch-booking-request.entity.ts │ │ │ │ ├── couch-booking.ts │ │ │ │ └── unavailable-booking.ts │ │ │ │ └── repositories │ │ │ │ ├── bookable-couch.repository.ts │ │ │ │ ├── booking-cancellation.repository.ts │ │ │ │ └── couch-booking-request.repository.ts │ │ └── tsconfig.json │ ├── couch │ │ ├── .eslintignore │ │ ├── .eslintrc.js │ │ ├── .swcrc │ │ ├── couch.rest │ │ ├── package.json │ │ ├── src │ │ │ ├── api │ │ │ │ ├── container.ts │ │ │ │ ├── couch.module.ts │ │ │ │ └── routes │ │ │ │ │ ├── couch.router.ts │ │ │ │ │ └── router.ts │ │ │ ├── core │ │ │ │ ├── config.ts │ │ │ │ ├── dto │ │ │ │ │ ├── couch.dto.ts │ │ │ │ │ ├── create-couch.dto.ts │ │ │ │ │ └── update-couch.dto.ts │ │ │ │ ├── entities │ │ │ │ │ └── couch.ts │ │ │ │ ├── error │ │ │ │ │ └── couch-not-found.error.ts │ │ │ │ ├── events │ │ │ │ │ └── couch-created.event.ts │ │ │ │ ├── repositories │ │ │ │ │ └── couch.repository.ts │ │ │ │ └── services │ │ │ │ │ └── couch.service.ts │ │ │ └── index.ts │ │ └── tsconfig.json │ ├── review │ │ ├── .eslintignore │ │ ├── .eslintrc.js │ │ ├── .swcrc │ │ ├── package.json │ │ ├── review.rest │ │ ├── src │ │ │ ├── api │ │ │ │ ├── container.ts │ │ │ │ ├── review.module.ts │ │ │ │ └── routes │ │ │ │ │ ├── booking-review.router.ts │ │ │ │ │ └── router.ts │ │ │ ├── core │ │ │ │ ├── config.ts │ │ │ │ ├── dto │ │ │ │ │ ├── booking-review.dto.ts │ │ │ │ │ ├── create-couch.dto.ts │ │ │ │ │ └── update-booking-review.dto.ts │ │ │ │ ├── entities │ │ │ │ │ ├── booking-review.ts │ │ │ │ │ └── review-details.ts │ │ │ │ ├── error │ │ │ │ │ └── booking-review-not-found.error.ts │ │ │ │ ├── events │ │ │ │ │ └── external │ │ │ │ │ │ └── booking-finished.event.ts │ │ │ │ ├── repositories │ │ │ │ │ └── booking-review.repository.ts │ │ │ │ ├── services │ │ │ │ │ └── booking-review.service.ts │ │ │ │ └── subscribers │ │ │ │ │ └── booking-finished.subscriber.ts │ │ │ └── index.ts │ │ └── tsconfig.json │ └── user │ │ ├── .eslintignore │ │ ├── .eslintrc.js │ │ ├── .swcrc │ │ ├── package.json │ │ ├── src │ │ ├── api │ │ │ ├── container.ts │ │ │ ├── routes │ │ │ │ ├── router.ts │ │ │ │ └── user.router.ts │ │ │ └── user.module.ts │ │ ├── core │ │ │ ├── config.ts │ │ │ ├── dto │ │ │ │ ├── login.dto.ts │ │ │ │ ├── register.dto.ts │ │ │ │ ├── update-user.dto.ts │ │ │ │ └── user.dto.ts │ │ │ ├── entities │ │ │ │ ├── profile.ts │ │ │ │ └── user.ts │ │ │ ├── error │ │ │ │ ├── invalid-email-or-password.error.ts │ │ │ │ ├── user-exists.error.ts │ │ │ │ └── user-not-found.error.ts │ │ │ ├── events │ │ │ │ └── user-created.event.ts │ │ │ ├── repositories │ │ │ │ ├── profile.repository.ts │ │ │ │ └── user.repository.ts │ │ │ ├── services │ │ │ │ ├── auth.service.ts │ │ │ │ ├── password-hasher.ts │ │ │ │ └── user.service.ts │ │ │ └── subscribers │ │ │ │ └── user-created.subscriber.ts │ │ └── index.ts │ │ ├── tsconfig.json │ │ └── user.rest └── shared │ ├── abstract-core │ ├── .eslintignore │ ├── .eslintrc.js │ ├── .swcrc │ ├── package.json │ ├── src │ │ ├── auth │ │ │ ├── index.ts │ │ │ └── user.ts │ │ ├── command │ │ │ ├── command-dispatcher.ts │ │ │ ├── command-handler.ts │ │ │ ├── command.ts │ │ │ └── index.ts │ │ ├── error │ │ │ ├── index.ts │ │ │ └── travelhoop.error.ts │ │ ├── event │ │ │ ├── event.dispatcher.ts │ │ │ ├── event.subscriber.ts │ │ │ ├── event.ts │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── logger │ │ │ ├── index.ts │ │ │ └── logger.ts │ │ ├── messaging │ │ │ ├── index.ts │ │ │ ├── message-dispatcher.ts │ │ │ └── message.ts │ │ └── queue │ │ │ ├── index.ts │ │ │ └── queue.ts │ └── tsconfig.json │ ├── infrastructure │ ├── .eslintignore │ ├── .eslintrc.js │ ├── .swcrc │ ├── package.json │ ├── src │ │ ├── command │ │ │ ├── command-dispatcher.ts │ │ │ └── index.ts │ │ ├── config │ │ │ ├── index.ts │ │ │ └── load-env.ts │ │ ├── container │ │ │ ├── as-array.ts │ │ │ ├── as-dictionary.ts │ │ │ ├── container-builder.ts │ │ │ ├── index.ts │ │ │ └── middleware │ │ │ │ └── scope-per-request.ts │ │ ├── event │ │ │ └── event.dispatcher.ts │ │ ├── express │ │ │ ├── index.ts │ │ │ ├── middleware │ │ │ │ ├── auth.ts │ │ │ │ ├── error-handler.ts │ │ │ │ ├── index.ts │ │ │ │ ├── middleware.type.ts │ │ │ │ ├── request-context.ts │ │ │ │ └── scheduler-token.ts │ │ │ ├── request.ts │ │ │ └── response.ts │ │ ├── index.ts │ │ ├── logger │ │ │ ├── index.ts │ │ │ ├── logger.ts │ │ │ └── types.ts │ │ ├── messaging │ │ │ ├── background.message-dispatcher.ts │ │ │ ├── index.ts │ │ │ ├── message-broker.ts │ │ │ └── redis.message-dispatcher.ts │ │ ├── mikro-orm │ │ │ ├── db-connection.ts │ │ │ ├── decorators │ │ │ │ └── transactional-command-dispatcher.decorator.ts │ │ │ ├── entity-schema │ │ │ │ ├── aggregate-root.ts │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── repository.ts │ │ │ └── types │ │ │ │ ├── aggregate-id.ts │ │ │ │ ├── guid-type.ts │ │ │ │ └── index.ts │ │ ├── module │ │ │ ├── app-module.factory.ts │ │ │ ├── app-module.ts │ │ │ └── index.ts │ │ ├── redis │ │ │ └── redis.queue.ts │ │ └── typings │ │ │ └── global.d.ts │ └── tsconfig.json │ └── kernel │ ├── .eslintignore │ ├── .eslintrc.js │ ├── .swcrc │ ├── package.json │ ├── src │ ├── aggregate │ │ ├── aggregate-id.ts │ │ ├── aggregate-root.ts │ │ └── index.ts │ ├── domain-event │ │ ├── domain-event.dispatcher.ts │ │ ├── domain-event.subscriber.ts │ │ ├── domain.event.ts │ │ └── index.ts │ └── index.ts │ └── tsconfig.json └── tools └── toolchain ├── .eslintignore ├── .swcrc ├── includes ├── .eslintrc.js └── tsconfig.web.json ├── package.json └── patch └── modern-module-resolution.js /.dockerignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | *.log 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | 7 | # Runtime data 8 | *.pid 9 | *.seed 10 | *.pid.lock 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # nyc test coverage 19 | .nyc_output 20 | 21 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 22 | .grunt 23 | 24 | # Bower dependency directory (https://bower.io/) 25 | bower_components 26 | 27 | # node-waf configuration 28 | .lock-wscript 29 | 30 | # Compiled binary addons (https://nodejs.org/api/addons.html) 31 | build/Release 32 | 33 | # Dependency directories 34 | node_modules/ 35 | jspm_packages/ 36 | 37 | # Optional npm cache directory 38 | .npm 39 | 40 | # Optional eslint cache 41 | .eslintcache 42 | 43 | # Optional REPL history 44 | .node_repl_history 45 | 46 | # Output of 'npm pack' 47 | *.tgz 48 | 49 | # Yarn Integrity file 50 | .yarn-integrity 51 | 52 | # dotenv environment variables file 53 | .env 54 | 55 | # next.js build output 56 | .next 57 | 58 | # OS X temporary files 59 | .DS_Store 60 | 61 | # Rush temporary files 62 | common/deploy/ 63 | common/temp/ 64 | common/autoinstallers/*/.npmrc 65 | **/.rush/temp/ 66 | 67 | # Generated js files 68 | */**/build 69 | */**/node_modules 70 | 71 | # .env 72 | .env -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Don't allow people to merge changes to these generated files, because the result 2 | # may be invalid. You need to run "rush update" again. 3 | pnpm-lock.yaml merge=binary 4 | shrinkwrap.yaml merge=binary 5 | npm-shrinkwrap.json merge=binary 6 | yarn.lock merge=binary 7 | 8 | # Rush's JSON config files use JavaScript-style code comments. The rule below prevents pedantic 9 | # syntax highlighters such as GitHub's from highlighting these comments as errors. Your text editor 10 | # may also require a special configuration to allow comments in JSON. 11 | # 12 | # For more information, see this issue: https://github.com/microsoft/rushstack/issues/1088 13 | # 14 | *.json linguist-language=JSON-with-Comments 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | *.log 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | 7 | # Runtime data 8 | *.pid 9 | *.seed 10 | *.pid.lock 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # nyc test coverage 19 | .nyc_output 20 | 21 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 22 | .grunt 23 | 24 | # Bower dependency directory (https://bower.io/) 25 | bower_components 26 | 27 | # node-waf configuration 28 | .lock-wscript 29 | 30 | # Compiled binary addons (https://nodejs.org/api/addons.html) 31 | build/Release 32 | 33 | # Dependency directories 34 | node_modules/ 35 | jspm_packages/ 36 | 37 | # Optional npm cache directory 38 | .npm 39 | 40 | # Optional eslint cache 41 | .eslintcache 42 | 43 | # Optional REPL history 44 | .node_repl_history 45 | 46 | # Output of 'npm pack' 47 | *.tgz 48 | 49 | # Yarn Integrity file 50 | .yarn-integrity 51 | 52 | # dotenv environment variables file 53 | .env 54 | 55 | # next.js build output 56 | .next 57 | 58 | # OS X temporary files 59 | .DS_Store 60 | 61 | # Rush temporary files 62 | common/deploy/ 63 | common/temp/ 64 | common/autoinstallers/*/.npmrc 65 | **/.rush/temp/ 66 | 67 | # Generated js files 68 | */**/build 69 | */**/node_modules 70 | 71 | # .env 72 | .env 73 | .env.prod 74 | 75 | # docker-compose override 76 | docker-compose.override.yaml -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------------------------------------------- 2 | # Keep this section in sync with .gitignore 3 | #------------------------------------------------------------------------------------------------------------------- 4 | 5 | 👋 (copy + paste your .gitignore file contents here) 👋 6 | 7 | # Logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | 13 | # Runtime data 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | 24 | # nyc test coverage 25 | .nyc_output 26 | 27 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 28 | .grunt 29 | 30 | # Bower dependency directory (https://bower.io/) 31 | bower_components 32 | 33 | # node-waf configuration 34 | .lock-wscript 35 | 36 | # Compiled binary addons (https://nodejs.org/api/addons.html) 37 | build/Release 38 | 39 | # Dependency directories 40 | node_modules/ 41 | jspm_packages/ 42 | 43 | # Optional npm cache directory 44 | .npm 45 | 46 | # Optional eslint cache 47 | .eslintcache 48 | 49 | # Optional REPL history 50 | .node_repl_history 51 | 52 | # Output of 'npm pack' 53 | *.tgz 54 | 55 | # Yarn Integrity file 56 | .yarn-integrity 57 | 58 | # dotenv environment variables file 59 | .env 60 | 61 | # next.js build output 62 | .next 63 | 64 | # OS X temporary files 65 | .DS_Store 66 | 67 | # Rush temporary files 68 | common/deploy/ 69 | common/temp/ 70 | common/autoinstallers/*/.npmrc 71 | **/.rush/temp/ 72 | 73 | #------------------------------------------------------------------------------------------------------------------- 74 | # Prettier-specific overrides 75 | #------------------------------------------------------------------------------------------------------------------- 76 | 77 | # Rush files 78 | common/changes/ 79 | common/scripts/ 80 | common/config/ 81 | CHANGELOG.* 82 | 83 | # Package manager files 84 | pnpm-lock.yaml 85 | yarn.lock 86 | package-lock.json 87 | shrinkwrap.json 88 | 89 | # Build outputs 90 | dist 91 | lib 92 | 93 | # Prettier reformats code blocks inside Markdown, which affects rendered output 94 | *.md -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "parser": "typescript", 4 | "printWidth": 120, 5 | "arrowParens": "avoid" 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.validate": [ 3 | "javascript", 4 | "typescript" 5 | ], 6 | } -------------------------------------------------------------------------------- /common/config/rush/.npmrc: -------------------------------------------------------------------------------- 1 | # Rush uses this file to configure the NPM package registry during installation. It is applicable 2 | # to PNPM, NPM, and Yarn package managers. It is used by operations such as "rush install", 3 | # "rush update", and the "install-run.js" scripts. 4 | # 5 | # NOTE: The "rush publish" command uses .npmrc-publish instead. 6 | # 7 | # Before invoking the package manager, Rush will copy this file to the folder where installation 8 | # is performed. The copied file will omit any config lines that reference environment variables 9 | # that are undefined in that session; this avoids problems that would otherwise result due to 10 | # a missing variable being replaced by an empty string. 11 | # 12 | # * * * SECURITY WARNING * * * 13 | # 14 | # It is NOT recommended to store authentication tokens in a text file on a lab machine, because 15 | # other unrelated processes may be able to read the file. Also, the file may persist indefinitely, 16 | # for example if the machine loses power. A safer practice is to pass the token via an 17 | # environment variable, which can be referenced from .npmrc using ${} expansion. For example: 18 | # 19 | # //registry.npmjs.org/:_authToken=${NPM_AUTH_TOKEN} 20 | # 21 | registry=https://registry.npmjs.org/ 22 | always-auth=false 23 | -------------------------------------------------------------------------------- /common/config/rush/.npmrc-publish: -------------------------------------------------------------------------------- 1 | # This config file is very similar to common/config/rush/.npmrc, except that .npmrc-publish 2 | # is used by the "rush publish" command, as publishing often involves different credentials 3 | # and registries than other operations. 4 | # 5 | # Before invoking the package manager, Rush will copy this file to "common/temp/publish-home/.npmrc" 6 | # and then temporarily map that folder as the "home directory" for the current user account. 7 | # This enables the same settings to apply for each project folder that gets published. The copied file 8 | # will omit any config lines that reference environment variables that are undefined in that session; 9 | # this avoids problems that would otherwise result due to a missing variable being replaced by 10 | # an empty string. 11 | # 12 | # * * * SECURITY WARNING * * * 13 | # 14 | # It is NOT recommended to store authentication tokens in a text file on a lab machine, because 15 | # other unrelated processes may be able to read the file. Also, the file may persist indefinitely, 16 | # for example if the machine loses power. A safer practice is to pass the token via an 17 | # environment variable, which can be referenced from .npmrc using ${} expansion. For example: 18 | # 19 | # //registry.npmjs.org/:_authToken=${NPM_AUTH_TOKEN} 20 | # 21 | -------------------------------------------------------------------------------- /common/config/rush/deploy.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/deploy-scenario.schema.json", 4 | "deploymentProjectNames": ["@travelhoop/app", "@travelhoop/scheduler"], 5 | "projectSettings": [ 6 | { 7 | "projectName": "@travelhoop/app" 8 | }, 9 | { 10 | "projectName": "@travelhoop/scheduler" 11 | 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /common/config/rush/experiments.json: -------------------------------------------------------------------------------- 1 | /** 2 | * This configuration file allows repo maintainers to enable and disable experimental 3 | * Rush features. More documentation is available on the Rush website: https://rushjs.io 4 | */ 5 | { 6 | "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/experiments.schema.json", 7 | 8 | /** 9 | * Rush 5.14.0 improved incremental builds to ignore spurious changes in the pnpm-lock.json file. 10 | * This optimization is enabled by default. If you encounter a problem where "rush build" is neglecting 11 | * to build some projects, please open a GitHub issue. As a workaround you can uncomment this line 12 | * to temporarily restore the old behavior where everything must be rebuilt whenever pnpm-lock.json 13 | * is modified. 14 | */ 15 | // "legacyIncrementalBuildDependencyDetection": true, 16 | 17 | /** 18 | * By default, 'rush install' passes --no-prefer-frozen-lockfile to 'pnpm install'. 19 | * Set this option to true to pass '--frozen-lockfile' instead for faster installs. 20 | */ 21 | // "usePnpmFrozenLockfileForRushInstall": true, 22 | 23 | /** 24 | * By default, 'rush update' passes --no-prefer-frozen-lockfile to 'pnpm install'. 25 | * Set this option to true to pass '--prefer-frozen-lockfile' instead to minimize shrinkwrap changes. 26 | */ 27 | // "usePnpmPreferFrozenLockfileForRushUpdate": true, 28 | 29 | /** 30 | * If using the 'preventManualShrinkwrapChanges' option, restricts the hash to only include the layout of external dependencies. 31 | * Used to allow links between workspace projects or the addition/removal of references to existing dependency versions to not 32 | * cause hash changes. 33 | */ 34 | // "omitImportersFromPreventManualShrinkwrapChanges": true, 35 | 36 | /** 37 | * If true, the chmod field in temporary project tar headers will not be normalized. 38 | * This normalization can help ensure consistent tarball integrity across platforms. 39 | */ 40 | // "noChmodFieldInTarHeaderNormalization": true, 41 | 42 | /** 43 | * If true, the build cache feature is enabled. To use this feature, a common/config/rush/build-cache.json 44 | * file must be created with configuration options. 45 | * 46 | * See https://github.com/microsoft/rushstack/issues/2393 for details about this experimental feature. 47 | */ 48 | // "buildCache": true 49 | } 50 | -------------------------------------------------------------------------------- /common/config/rush/pnpmfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * When using the PNPM package manager, you can use pnpmfile.js to workaround 5 | * dependencies that have mistakes in their package.json file. (This feature is 6 | * functionally similar to Yarn's "resolutions".) 7 | * 8 | * For details, see the PNPM documentation: 9 | * https://pnpm.js.org/docs/en/hooks.html 10 | * 11 | * IMPORTANT: SINCE THIS FILE CONTAINS EXECUTABLE CODE, MODIFYING IT IS LIKELY TO INVALIDATE 12 | * ANY CACHED DEPENDENCY ANALYSIS. After any modification to pnpmfile.js, it's recommended to run 13 | * "rush update --full" so that PNPM will recalculate all version selections. 14 | */ 15 | module.exports = { 16 | hooks: { 17 | readPackage 18 | } 19 | }; 20 | 21 | /** 22 | * This hook is invoked during installation before a package's dependencies 23 | * are selected. 24 | * The `packageJson` parameter is the deserialized package.json 25 | * contents for the package that is about to be installed. 26 | * The `context` parameter provides a log() function. 27 | * The return value is the updated object. 28 | */ 29 | function readPackage(packageJson, context) { 30 | 31 | // // The karma types have a missing dependency on typings from the log4js package. 32 | // if (packageJson.name === '@types/karma') { 33 | // context.log('Fixed up dependencies for @types/karma'); 34 | // packageJson.dependencies['log4js'] = '0.6.38'; 35 | // } 36 | 37 | return packageJson; 38 | } 39 | -------------------------------------------------------------------------------- /common/git-hooks/commit-msg.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # This is an example Git hook for use with Rush. To enable this hook, rename this file 4 | # to "commit-msg" and then run "rush install", which will copy it from common/git-hooks 5 | # to the .git/hooks folder. 6 | # 7 | # TO LEARN MORE ABOUT GIT HOOKS 8 | # 9 | # The Git documentation is here: https://git-scm.com/docs/githooks 10 | # Some helpful resources: https://githooks.com 11 | # 12 | # ABOUT THIS EXAMPLE 13 | # 14 | # The commit-msg hook is called by "git commit" with one argument, the name of the file 15 | # that has the commit message. The hook should exit with non-zero status after issuing 16 | # an appropriate message if it wants to stop the commit. The hook is allowed to edit 17 | # the commit message file. 18 | 19 | # This example enforces that commit message should contain a minimum amount of 20 | # description text. 21 | if [ `cat $1 | wc -w` -lt 3 ]; then 22 | echo "" 23 | echo "Invalid commit message: The message must contain at least 3 words." 24 | exit 1 25 | fi 26 | -------------------------------------------------------------------------------- /common/scripts/install-run-rushx.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. 3 | // See the @microsoft/rush package's LICENSE file for license information. 4 | Object.defineProperty(exports, "__esModule", { value: true }); 5 | // THIS FILE WAS GENERATED BY A TOOL. ANY MANUAL MODIFICATIONS WILL GET OVERWRITTEN WHENEVER RUSH IS UPGRADED. 6 | // 7 | // This script is intended for usage in an automated build environment where the Rush command may not have 8 | // been preinstalled, or may have an unpredictable version. This script will automatically install the version of Rush 9 | // specified in the rush.json configuration file (if not already installed), and then pass a command-line to the 10 | // rushx command. 11 | // 12 | // An example usage would be: 13 | // 14 | // node common/scripts/install-run-rushx.js custom-command 15 | // 16 | // For more information, see: https://rushjs.io/pages/maintainer/setup_new_repo/ 17 | require("./install-run-rush"); 18 | //# sourceMappingURL=install-run-rushx.js.map -------------------------------------------------------------------------------- /docker-compose.override.yaml.dist: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | 3 | services: 4 | travelhoop: 5 | build: 6 | context: . 7 | dockerfile: ./docker/Dockerfile 8 | cache_from: 9 | - travelhoop:latest 10 | ports: 11 | - "3010:3010" 12 | env_file: 13 | - ./src/app/travelhoop/.env.prod 14 | depends_on: 15 | - travelhoop-scheduler 16 | - travelhoop-postgres 17 | - travelhoop-redis -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | 3 | services: 4 | travelhoop-postgres: 5 | image: postgres:10-alpine 6 | container_name: travelhoop-postgres 7 | ports: 8 | - "5440:5432" 9 | environment: 10 | POSTGRES_PASSWORD: password 11 | POSTGRES_USERNAME: postgres 12 | POSTGRES_DB: travelhoop 13 | healthcheck: 14 | test: ["CMD-SHELL", "pg_isready -U postgres"] 15 | interval: 30s 16 | timeout: 30s 17 | retries: 3 18 | 19 | travelhoop-redis: 20 | image: redis:alpine 21 | container_name: travelhoop-redis 22 | ports: 23 | - "6379:6379" 24 | 25 | travelhoop-scheduler: 26 | restart: always 27 | build: 28 | context: . 29 | dockerfile: ./docker/Dockerfile.scheduler 30 | cache_from: 31 | - travelhoop-scheduler:latest 32 | env_file: 33 | - ./src/app/scheduler/.env -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16.16.0-alpine3.15 as builder 2 | 3 | WORKDIR /app 4 | 5 | RUN npm i -g @microsoft/rush 6 | 7 | COPY ./src ./src 8 | COPY ./tools ./tools 9 | COPY ./common ./common 10 | COPY ./rush.json . 11 | 12 | RUN rush update 13 | RUN rush build 14 | RUN rush deploy --project @travelhoop/app 15 | 16 | FROM node:14.15.1-alpine3.10 17 | 18 | WORKDIR /app 19 | 20 | COPY --from=builder /app/common/deploy/ . 21 | 22 | EXPOSE 3010 23 | 24 | CMD ["node", "./src/app/travelhoop/build/server.js"] -------------------------------------------------------------------------------- /docker/Dockerfile.scheduler: -------------------------------------------------------------------------------- 1 | FROM node:16.16.0-alpine3.15 as builder 2 | 3 | RUN npm i -g @microsoft/rush 4 | 5 | WORKDIR /app 6 | 7 | COPY ./src ./src 8 | COPY ./tools ./tools 9 | COPY ./common ./common 10 | COPY ./rush.json . 11 | 12 | RUN rush update 13 | RUN rush build 14 | RUN rush deploy --project @travelhoop/scheduler 15 | 16 | FROM node:14.15.1-alpine3.10 17 | 18 | WORKDIR /app 19 | 20 | COPY --from=builder /app/common/deploy/ . 21 | COPY --from=builder /app/src/app/scheduler/crontab /etc/cron.d/crontab 22 | 23 | RUN chmod 0644 /etc/cron.d/crontab 24 | RUN crontab /etc/cron.d/crontab 25 | RUN touch /var/log/cron.log 26 | 27 | CMD crond && tail -f /var/log/cron.log -------------------------------------------------------------------------------- /docs/adr/0001-record-architecture-decisions.md: -------------------------------------------------------------------------------- 1 | # 1. Record architecture decisions 2 | 3 | Date: 2021-04-01 4 | 5 | ## Status 6 | 7 | Accepted 8 | 9 | ## Context 10 | 11 | We need to record the architectural decisions made on this project. 12 | 13 | ## Decision 14 | 15 | We will use Architecture Decision Records, as [described by Michael Nygard](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions). 16 | 17 | ## Consequences 18 | 19 | See Michael Nygard's article, linked above. For a lightweight ADR toolset, see Nat Pryce's [adr-tools](https://github.com/npryce/adr-tools). 20 | -------------------------------------------------------------------------------- /docs/adr/0002-use-modular-monolith-architecture.md: -------------------------------------------------------------------------------- 1 | # 2. Use modular monolith architecture 2 | 3 | Date: 2021-04-01 4 | 5 | ## Status 6 | 7 | Accepted 8 | 9 | ## Context 10 | 11 | Modular monolith architecture became very popular this days. Unfortunatly there is no good examples how to implement it. 12 | 13 | ## Decision 14 | 15 | Create a fully working application example, to present how the implementation of modualr monoltih can looks like. 16 | 17 | ## Consequences 18 | 19 | - Application will run as a single unit 20 | - Each module will have maximum autonomy -------------------------------------------------------------------------------- /docs/adr/0003-di-container-for-each-module.md: -------------------------------------------------------------------------------- 1 | # 3. DI container for each module 2 | 3 | Date: 2021-04-01 4 | 5 | ## Status 6 | 7 | Accepted 8 | 9 | ## Context 10 | 11 | Each module should be maximum independent. It should take care of all the dependencies by itself. It should be able to create a graph of object. 12 | 13 | ## Decision 14 | 15 | For each module we will create a separate DI container. It support modularity, and makes each module responsible by them dependencies. 16 | 17 | ## Consequences 18 | 19 | - Memory usage of the application can be bigger, because multiple container can take more memory than single one. 20 | - Modules will be more independent 21 | - Modules must be initialized by main application 22 | - Modules will be easier to extract, because it won't have cupling to the rest of codebase. 23 | - Maintain of each container per module 24 | - Duplicated code -------------------------------------------------------------------------------- /docs/adr/0004-share-infrastructure-between-all-modules.md: -------------------------------------------------------------------------------- 1 | # 4. Share infrastrcture between all modules 2 | 3 | Date: 2021-04-01 4 | 5 | ## Status 6 | 7 | Accepted 8 | 9 | ## Context 10 | 11 | Each of the module need some kind of building blocks to bootstrap the application. 12 | 13 | ## Possible solutions 14 | 15 | 1. Make one directory with shared codebase. 16 | Pros: 17 | - less code duplication 18 | - going into microservices it's easy to copy this directory, with all the codebase 19 | - each module can still have module-specific infrastrcture defined only inside a module 20 | - easier to just import needed things than copying it into each module 21 | Cons: 22 | - coupling to shared folder 23 | - different requirements of implementation for each module which will force to 24 | 25 | 2. Define infrastructure in each module 26 | Pros: 27 | - less coupling 28 | Cons: 29 | - more effort to maintain application 30 | - a lot of duplicated code 31 | 32 | ## Decision 33 | 34 | In this application, we will have one building-blocks folder, shared between modules. 35 | 36 | ## Consequences 37 | 38 | Remember to not overuse this shared folder. Everything which is not general, should be keeped in modules separetly. -------------------------------------------------------------------------------- /docs/adr/0005-use-local-contract-for-integration-events.md: -------------------------------------------------------------------------------- 1 | # 5. Use local contract for integration events 2 | 3 | Date: 2021-04-10 4 | 5 | ## Status 6 | 7 | Accepted 8 | 9 | ## Context 10 | 11 | Each of the module expose integration events. Other modules can listen for these events and execute specific action. We need to define events somewhere, to be able to properly handle it in each module. 12 | 13 | ## Possible solutions 14 | 15 | 1. Shared contracts. 16 | Pros: 17 | - less code duplication 18 | - going into microservices it's easy to copy this directory, with all the codebase 19 | - easier to just import needed things instead of defining it multiple times in every module 20 | Cons: 21 | - coupling to mutiple modules 22 | 23 | 1. Local contracts 24 | Pros: 25 | - less coupling 26 | Cons: 27 | - more effort to maintain integration between events e.g. contract testing 28 | - a lot of duplicated code 29 | 30 | ## Decision 31 | 32 | One of the goals of this project is to keep each module as independent as possible, I decided to use local contracts 33 | 34 | ## Consequences 35 | 36 | To maintain contract consistency, contract testing will need to be in place. -------------------------------------------------------------------------------- /docs/c4/components.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgce/modular-monolith-nodejs/83adcbcf87f66668b4509c743a8907a7d35a0743/docs/c4/components.jpg -------------------------------------------------------------------------------- /docs/c4/containers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgce/modular-monolith-nodejs/83adcbcf87f66668b4509c743a8907a7d35a0743/docs/c4/containers.jpg -------------------------------------------------------------------------------- /docs/c4/system-context.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgce/modular-monolith-nodejs/83adcbcf87f66668b4509c743a8907a7d35a0743/docs/c4/system-context.jpg -------------------------------------------------------------------------------- /docs/images/es-booking-module.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgce/modular-monolith-nodejs/83adcbcf87f66668b4509c743a8907a7d35a0743/docs/images/es-booking-module.jpg -------------------------------------------------------------------------------- /docs/images/es-couch-module.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgce/modular-monolith-nodejs/83adcbcf87f66668b4509c743a8907a7d35a0743/docs/images/es-couch-module.jpg -------------------------------------------------------------------------------- /docs/images/es-overview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgce/modular-monolith-nodejs/83adcbcf87f66668b4509c743a8907a7d35a0743/docs/images/es-overview.jpg -------------------------------------------------------------------------------- /docs/images/es-review-module.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgce/modular-monolith-nodejs/83adcbcf87f66668b4509c743a8907a7d35a0743/docs/images/es-review-module.jpg -------------------------------------------------------------------------------- /docs/images/es-user-module.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgce/modular-monolith-nodejs/83adcbcf87f66668b4509c743a8907a7d35a0743/docs/images/es-user-module.jpg -------------------------------------------------------------------------------- /rush.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/rush.schema.json", 4 | "rushVersion": "5.42.4", 5 | "pnpmVersion": "5.15.2", 6 | "pnpmOptions": { 7 | }, 8 | "nodeSupportedVersionRange": ">=16", 9 | "projectFolderMinDepth": 2, 10 | "projectFolderMaxDepth": 3, 11 | "gitPolicy": {}, 12 | "repository": { 13 | "url": "https://github.com/mgce/modular-monolith-nodejs", 14 | "defaultBranch": "master", 15 | "defaultRemote": "origin" 16 | }, 17 | "eventHooks": { 18 | "preRushInstall": [], 19 | "postRushInstall": [], 20 | "preRushBuild": [], 21 | "postRushBuild": [] 22 | }, 23 | "variants": [], 24 | "projects": [ 25 | { 26 | "packageName": "@travelhoop/toolchain", 27 | "projectFolder": "tools/toolchain", 28 | "reviewCategory": "internal" 29 | }, 30 | { 31 | "packageName": "@travelhoop/app", 32 | "projectFolder": "src/app/travelhoop", 33 | "reviewCategory": "production" 34 | }, 35 | { 36 | "packageName": "@travelhoop/user-module", 37 | "projectFolder": "src/modules/user", 38 | "reviewCategory": "production" 39 | }, 40 | { 41 | "packageName": "@travelhoop/couch-module", 42 | "projectFolder": "src/modules/couch", 43 | "reviewCategory": "production" 44 | }, 45 | { 46 | "packageName": "@travelhoop/review-module", 47 | "projectFolder": "src/modules/review", 48 | "reviewCategory": "production" 49 | }, 50 | { 51 | "packageName": "@travelhoop/booking-module", 52 | "projectFolder": "src/modules/booking", 53 | "reviewCategory": "production" 54 | }, 55 | { 56 | "packageName": "@travelhoop/infrastructure", 57 | "projectFolder": "src/shared/infrastructure", 58 | "reviewCategory": "production" 59 | }, 60 | { 61 | "packageName": "@travelhoop/shared-kernel", 62 | "projectFolder": "src/shared/kernel", 63 | "reviewCategory": "production" 64 | }, 65 | { 66 | "packageName": "@travelhoop/scheduler", 67 | "projectFolder": "src/app/scheduler", 68 | "reviewCategory": "tools" 69 | }, 70 | { 71 | "packageName": "@travelhoop/abstract-core", 72 | "projectFolder": "src/shared/abstract-core", 73 | "reviewCategory": "tools" 74 | } 75 | ] 76 | } 77 | -------------------------------------------------------------------------------- /src/app/scheduler/.env.dist: -------------------------------------------------------------------------------- 1 | API_URL=https://localhost:3010 2 | SCHEDULER_SECURITY_TOKEN=6701e22f61e248708568c95a1e1563d4 3 | -------------------------------------------------------------------------------- /src/app/scheduler/.eslintignore: -------------------------------------------------------------------------------- 1 | **/*.d.ts 2 | *.json -------------------------------------------------------------------------------- /src/app/scheduler/.eslintrc.js: -------------------------------------------------------------------------------- 1 | require('@travelhoop/toolchain/patch/modern-module-resolution'); 2 | 3 | module.exports = { 4 | extends: [ "./node_modules/@travelhoop/toolchain/includes/.eslintrc" ], // <---- put your profile string here 5 | parserOptions: { 6 | project: "tsconfig.json", 7 | tsconfigRootDir: __dirname 8 | } 9 | }; -------------------------------------------------------------------------------- /src/app/scheduler/.swcrc: -------------------------------------------------------------------------------- 1 | { 2 | "jsc": { 3 | "parser": { 4 | "syntax": "typescript", 5 | "decorators": true, 6 | "dynamicImport": true 7 | }, 8 | "transform": { 9 | "legacyDecorator": true, 10 | "decoratorMetadata": true 11 | }, 12 | "target": "es2018", 13 | "keepClassNames": true, 14 | "loose": true 15 | }, 16 | "module": { 17 | "type": "commonjs", 18 | "strict": true, 19 | "strictMode": true, 20 | "lazy": false, 21 | "noInterop": false 22 | }, 23 | "sourceMaps": "inline" 24 | } -------------------------------------------------------------------------------- /src/app/scheduler/crontab: -------------------------------------------------------------------------------- 1 | * * * * * node /app/src/app/scheduler/build/jobs/finish-bookings.job.js >> /var/log/cron.log 2>&1 2 | -------------------------------------------------------------------------------- /src/app/scheduler/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@travelhoop/scheduler", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main":"./build/index.js", 6 | "typings":"./build/index.d.ts", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "build": "rimraf build && tsc", 10 | "build:watch":"swc src -d build", 11 | "lint": "eslint './**/*.ts'", 12 | "lint:fix": "eslint './**/*.ts' --fix" 13 | }, 14 | "author": "", 15 | "license": "ISC", 16 | "dependencies": { 17 | "rimraf": "~3.0.2", 18 | "winston": "~3.3.3", 19 | "dotenv": "~10.0.0", 20 | "node-fetch": "~2.6.1" 21 | }, 22 | "devDependencies": { 23 | "@travelhoop/toolchain":"1.0.0", 24 | "@types/node": "~14.14.37", 25 | "eslint": "~7.23.0", 26 | "typescript": "~4.2.3", 27 | "@swc/core": "~1.2.51", 28 | "@swc/cli": "~0.1.36", 29 | "@types/node-fetch": "~2.5.10" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/app/scheduler/src/config/config.ts: -------------------------------------------------------------------------------- 1 | const dotenv = require("dotenv"); 2 | 3 | export const loadEnvs = () => { 4 | dotenv.config(); 5 | }; 6 | 7 | export interface EnvVariables extends NodeJS.Process { 8 | API_URL: string; 9 | SCHEDULER_SECURITY_TOKEN: string; 10 | } 11 | 12 | export const appConfigFactory = (env: EnvVariables) => ({ 13 | apiUrl: env.API_URL, 14 | schedulerToken: env.SCHEDULER_SECURITY_TOKEN, 15 | }); 16 | 17 | export type AppConfig = ReturnType; 18 | -------------------------------------------------------------------------------- /src/app/scheduler/src/http.client.ts: -------------------------------------------------------------------------------- 1 | import fetch from "node-fetch"; 2 | import { appConfigFactory } from "./config/config"; 3 | 4 | const httpClientInitializer = () => { 5 | const config = appConfigFactory(process.env as any); 6 | 7 | return (urlPath: string) => { 8 | const requestPath = `${config.apiUrl}/${urlPath}`; 9 | return fetch(requestPath, { 10 | method: "POST", 11 | headers: { 12 | "x-scheduler-token": config.schedulerToken, 13 | }, 14 | }).then(() => { 15 | // eslint-disable-next-line no-console 16 | console.log("[REQUEST]", new Date(), requestPath); 17 | }); 18 | }; 19 | }; 20 | 21 | export const httpClient = httpClientInitializer(); 22 | -------------------------------------------------------------------------------- /src/app/scheduler/src/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgce/modular-monolith-nodejs/83adcbcf87f66668b4509c743a8907a7d35a0743/src/app/scheduler/src/index.ts -------------------------------------------------------------------------------- /src/app/scheduler/src/jobs/finish-bookings.job.ts: -------------------------------------------------------------------------------- 1 | import { httpClient } from "../http.client"; 2 | 3 | const URL_PATH = "booking/finish-bookings"; 4 | 5 | (async () => { 6 | await httpClient(URL_PATH); 7 | })(); 8 | -------------------------------------------------------------------------------- /src/app/scheduler/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/@travelhoop/toolchain/includes/tsconfig.web.json" 3 | } -------------------------------------------------------------------------------- /src/app/travelhoop/.env.dist: -------------------------------------------------------------------------------- 1 | # APPLICATION VARIABLES 2 | SERVER_PORT=3010 3 | POSTGRES_URL=postgres://postgres:password@localhost:5440/travelhoop 4 | REDIS_URL=redis://localhost:6379/ 5 | JWT_SECRET_KEY=400b9d516c3e49afa44341b40233f63d 6 | ASYNC_MESSAGE_BROKER_QUEUE=async-queue 7 | SCHEDULER_SECURITY_TOKEN=6701e22f61e248708568c95a1e1563d4 8 | 9 | # USER MODULE VARIABLES 10 | USER_MODULE_JWT_EXPIRES_IN_MINUTES=30 11 | 12 | # HOST MODULE VARIABLES 13 | USER_MODULE_JWT_SECRET_KEY=400b9d516c3e49afa44341b40233f63d 14 | HOST_MODULE_MESSAGE_BROKER_QUEUE=async-queue -------------------------------------------------------------------------------- /src/app/travelhoop/.eslintignore: -------------------------------------------------------------------------------- 1 | **/*.d.ts 2 | *.json -------------------------------------------------------------------------------- /src/app/travelhoop/.eslintrc.js: -------------------------------------------------------------------------------- 1 | require('@travelhoop/toolchain/patch/modern-module-resolution'); 2 | 3 | module.exports = { 4 | extends: [ "./node_modules/@travelhoop/toolchain/includes/.eslintrc" ], // <---- put your profile string here 5 | parserOptions: { 6 | project: "tsconfig.json", 7 | tsconfigRootDir: __dirname 8 | } 9 | }; -------------------------------------------------------------------------------- /src/app/travelhoop/.swcrc: -------------------------------------------------------------------------------- 1 | { 2 | "jsc": { 3 | "parser": { 4 | "syntax": "typescript", 5 | "decorators": true, 6 | "dynamicImport": true 7 | }, 8 | "transform": { 9 | "legacyDecorator": true, 10 | "decoratorMetadata": true 11 | }, 12 | "target": "es2018", 13 | "keepClassNames": true, 14 | "loose": true 15 | }, 16 | "module": { 17 | "type": "commonjs", 18 | "strict": true, 19 | "strictMode": true, 20 | "lazy": false, 21 | "noInterop": false 22 | }, 23 | "sourceMaps": "inline" 24 | } -------------------------------------------------------------------------------- /src/app/travelhoop/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@travelhoop/app", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "test": "mocha --lazyLoadFiles=true --file=\"build/tests/bootstrap.js\" \"./node_modules/@travelhoop/*/build/**/*.test.js\"", 7 | "build": "rimraf build && tsc", 8 | "build:watch":"swc src -d build --config-file ./node_modules/@travelhoop/toolchain/.swcrc", 9 | "lint": "eslint './**/*.ts'", 10 | "lint:fix": "eslint './**/*.ts' --fix", 11 | "start-dev": "node-dev build/server.js", 12 | "migration:up":"node ./build/database/migrator/up.js", 13 | "migration:down":"node ./build/database/migrator/down.js", 14 | "migration:create":"node ./build/database/migrator/create.js" 15 | }, 16 | "author": "", 17 | "license": "ISC", 18 | "dependencies": { 19 | "@travelhoop/abstract-core": "1.0.0", 20 | "@travelhoop/infrastructure": "1.0.0", 21 | "@travelhoop/user-module": "1.0.0", 22 | "@travelhoop/couch-module": "1.0.0", 23 | "@travelhoop/booking-module": "1.0.0", 24 | "@travelhoop/review-module": "1.0.0", 25 | "express": "~4.17.1", 26 | "rimraf": "~3.0.2", 27 | "awilix": "~4.3.3", 28 | "dotenv": "~8.2.0", 29 | "@mikro-orm/core": "~4.5.5", 30 | "@mikro-orm/postgresql": "~4.5.5", 31 | "reflect-metadata": "~0.1.13", 32 | "@mikro-orm/migrations": "~4.5.2", 33 | "redis": "~3.1.1", 34 | "mocha": "~8.4.0" 35 | }, 36 | "devDependencies": { 37 | "@travelhoop/toolchain":"1.0.0", 38 | "@types/node": "~14.14.37", 39 | "@types/express": "~4.17.11", 40 | "eslint": "~7.23.0", 41 | "typescript": "~4.2.3", 42 | "node-dev": "~6.6.0", 43 | "@swc/core": "~1.2.51", 44 | "@swc/cli": "~0.1.36", 45 | "@types/redis": "~2.8.28", 46 | "@types/mocha": "~8.2.2" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/app/travelhoop/src/app.ts: -------------------------------------------------------------------------------- 1 | import { AppModule, DbConnection, MiddlewareType } from "@travelhoop/infrastructure"; 2 | import express, { Application } from "express"; 3 | import { RedisClient as Redis } from "redis"; 4 | 5 | interface AppDependencies { 6 | errorHandler: MiddlewareType; 7 | requestContext: MiddlewareType; 8 | modules: AppModule[]; 9 | dbConnection: DbConnection; 10 | redis: Redis; 11 | } 12 | 13 | export const createApp = ({ 14 | errorHandler, 15 | requestContext, 16 | modules, 17 | dbConnection, 18 | redis, 19 | }: AppDependencies): Application => { 20 | const app = express(); 21 | 22 | app.use(express.json()); 23 | 24 | app.use(requestContext); 25 | 26 | modules.forEach(m => m.use(app, { dbConnection, redis })); 27 | 28 | app.get("/", (_req, res) => { 29 | res.json("Travelhoop!"); 30 | }); 31 | 32 | app.use(errorHandler); 33 | 34 | return app; 35 | }; 36 | -------------------------------------------------------------------------------- /src/app/travelhoop/src/config/config.ts: -------------------------------------------------------------------------------- 1 | export interface EnvVariables extends NodeJS.Process { 2 | SERVER_PORT: string; 3 | POSTGRES_URL: string; 4 | REDIS_URL: string; 5 | ASYNC_MESSAGE_BROKER_QUEUE: string; 6 | } 7 | 8 | export const appConfigFactory = (env: EnvVariables) => ({ 9 | app: { 10 | port: parseInt(env.SERVER_PORT, 10), 11 | }, 12 | database: { 13 | url: env.POSTGRES_URL, 14 | }, 15 | redis: { 16 | url: env.REDIS_URL, 17 | }, 18 | queues: { 19 | messageBroker: env.ASYNC_MESSAGE_BROKER_QUEUE, 20 | }, 21 | }); 22 | 23 | export type AppConfig = ReturnType; 24 | -------------------------------------------------------------------------------- /src/app/travelhoop/src/config/db-config.ts: -------------------------------------------------------------------------------- 1 | import { Options, EntityCaseNamingStrategy, LoadStrategy } from "@mikro-orm/core"; 2 | import { join } from "path"; 3 | import { loadEnvs } from "@travelhoop/infrastructure"; 4 | import { EnvVariables } from "."; 5 | 6 | loadEnvs(); 7 | 8 | const infrastructureEntityPath = join( 9 | __dirname, 10 | "../../node_modules/@travelhoop/infrastructure/build/mikro-orm/entity-schema/*.js", 11 | ); 12 | 13 | const entityPathFactory = (moduleName: string) => 14 | join(__dirname, `../../node_modules/@travelhoop/${moduleName}/build/core/entities/*.js`); 15 | 16 | const entitySchemaPathFactory = (moduleName: string) => 17 | join(__dirname, `../../node_modules/@travelhoop/${moduleName}/build/infrastructure/mikro-orm/entity-schemas/*.js`); 18 | 19 | export const dbConfigFactory = (env: EnvVariables, modulesNames: string[]): Options => { 20 | return { 21 | type: "postgresql", 22 | clientUrl: env.POSTGRES_URL, 23 | entities: [ 24 | infrastructureEntityPath, 25 | ...modulesNames.map(entityPathFactory), 26 | ...modulesNames.map(entitySchemaPathFactory), 27 | ], 28 | forceUndefined: true, 29 | debug: false, 30 | namingStrategy: EntityCaseNamingStrategy, 31 | loadStrategy: LoadStrategy.JOINED, 32 | migrations: { 33 | tableName: "mikro_orm_migrations", // name of database table with log of executed transactions 34 | path: join(__dirname, "../../build/migrations"), // path to the folder with migrations 35 | pattern: /^[\w-]+\d+\.js$/, // regex pattern for the migration files 36 | transactional: true, // wrap each migration in a transaction 37 | disableForeignKeys: true, // wrap statements with `set foreign_key_checks = 0` or equivalent 38 | allOrNothing: true, // wrap all migrations in master transaction 39 | dropTables: true, // allow to disable table dropping 40 | safe: false, // allow to disable table and column dropping 41 | emit: "ts", // migration generation mode 42 | fileName: timestamp => `migration-${timestamp}`, 43 | }, 44 | }; 45 | }; 46 | 47 | export type DbConfig = ReturnType; 48 | -------------------------------------------------------------------------------- /src/app/travelhoop/src/config/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./config"; 2 | -------------------------------------------------------------------------------- /src/app/travelhoop/src/container.ts: -------------------------------------------------------------------------------- 1 | import { asFunction, asValue, createContainer } from "awilix"; 2 | import { createLogger, registerAsArray, AppModule, errorHandler, requestContext } from "@travelhoop/infrastructure"; 3 | import { Application } from "express"; 4 | import * as http from "http"; 5 | import { MikroORM } from "@mikro-orm/core"; 6 | import { RedisClient as Redis } from "redis"; 7 | import { createApp } from "./app"; 8 | import { AppConfig } from "./config/config"; 9 | import { DbConfig } from "./config/db-config"; 10 | 11 | interface ContainerDependencies { 12 | appConfig: AppConfig; 13 | dbConfig: DbConfig; 14 | appModules: AppModule[]; 15 | redis: Redis; 16 | } 17 | 18 | export const setupContainer = async ({ appConfig, appModules, dbConfig, redis }: ContainerDependencies) => { 19 | const container = createContainer(); 20 | 21 | const dbConnection = await MikroORM.init(dbConfig); 22 | 23 | container.register({ 24 | port: asValue(appConfig.app.port), 25 | app: asFunction(createApp), 26 | logger: asValue(createLogger(process.env)), 27 | errorHandler: asFunction(errorHandler), 28 | modules: registerAsArray(appModules.map(appModule => asValue(appModule))), 29 | dbConnection: asValue(dbConnection), 30 | redis: asValue(redis), 31 | requestContext: asFunction(requestContext), 32 | }); 33 | 34 | container.register({ 35 | app: asFunction(createApp).singleton(), 36 | }); 37 | 38 | const app: Application = container.resolve("app"); 39 | 40 | container.register({ 41 | server: asValue(http.createServer(app)), 42 | }); 43 | 44 | return container; 45 | }; 46 | -------------------------------------------------------------------------------- /src/app/travelhoop/src/database/migrator/create.ts: -------------------------------------------------------------------------------- 1 | import { MikroORM } from "@mikro-orm/core"; 2 | import { loadEnvs } from "@travelhoop/infrastructure"; 3 | import { dbConfigFactory } from "../../config/db-config"; 4 | import { loadModules } from "../../module.loader"; 5 | 6 | loadEnvs(); 7 | 8 | (async () => { 9 | const modules = loadModules(); 10 | const config = dbConfigFactory( 11 | process.env as any, 12 | modules.map(appModule => appModule.name), 13 | ); 14 | const orm = await MikroORM.init(config); 15 | const migrator = orm.getMigrator(); 16 | const pendingMigrations = await migrator.getPendingMigrations(); 17 | const executedMigrations = await migrator.getExecutedMigrations(); 18 | const initial = !(Boolean(pendingMigrations.length) || Boolean(executedMigrations.length)); 19 | await migrator.createMigration("./src/migrations", false, initial); 20 | await orm.close(true); 21 | })(); 22 | -------------------------------------------------------------------------------- /src/app/travelhoop/src/database/migrator/down.ts: -------------------------------------------------------------------------------- 1 | import { MikroORM } from "@mikro-orm/core"; 2 | import { loadEnvs } from "@travelhoop/infrastructure"; 3 | import { dbConfigFactory } from "../../config/db-config"; 4 | import { loadModules } from "../../module.loader"; 5 | 6 | loadEnvs(); 7 | 8 | (async () => { 9 | const modules = loadModules(); 10 | const config = dbConfigFactory( 11 | process.env as any, 12 | modules.map(appModule => appModule.name), 13 | ); 14 | const orm = await MikroORM.init(config); 15 | const migrator = orm.getMigrator(); 16 | await migrator.down({ to: 0 }); 17 | await orm.close(true); 18 | })(); 19 | -------------------------------------------------------------------------------- /src/app/travelhoop/src/database/migrator/up.ts: -------------------------------------------------------------------------------- 1 | import { MikroORM } from "@mikro-orm/core"; 2 | import { loadEnvs } from "@travelhoop/infrastructure"; 3 | import { dbConfigFactory } from "../../config/db-config"; 4 | import { loadModules } from "../../module.loader"; 5 | 6 | (async () => { 7 | loadEnvs(); 8 | const modules = loadModules(); 9 | const config = dbConfigFactory( 10 | process.env as any, 11 | modules.map(appModule => appModule.name), 12 | ); 13 | 14 | const orm = await MikroORM.init(config); 15 | const migrator = orm.getMigrator(); 16 | await migrator.up(); // runs migrations up to the latest 17 | await orm.close(true); 18 | })(); 19 | -------------------------------------------------------------------------------- /src/app/travelhoop/src/migrations/migration-20210504171406.ts: -------------------------------------------------------------------------------- 1 | import { Migration } from '@mikro-orm/migrations'; 2 | 3 | export class Migration20210504171406 extends Migration { 4 | 5 | async up(): Promise { 6 | this.addSql('alter table "CouchBookingRequest" drop constraint if exists "CouchBookingRequest_decisionDate_check";'); 7 | this.addSql('alter table "CouchBookingRequest" alter column "decisionDate" type timestamptz(0) using ("decisionDate"::timestamptz(0));'); 8 | this.addSql('alter table "CouchBookingRequest" alter column "decisionDate" drop not null;'); 9 | this.addSql('alter table "CouchBookingRequest" drop constraint if exists "CouchBookingRequest_rejectionReason_check";'); 10 | this.addSql('alter table "CouchBookingRequest" alter column "rejectionReason" type varchar(255) using ("rejectionReason"::varchar(255));'); 11 | this.addSql('alter table "CouchBookingRequest" alter column "rejectionReason" drop not null;'); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/app/travelhoop/src/migrations/migration-20210604132200.ts: -------------------------------------------------------------------------------- 1 | import { Migration } from "@mikro-orm/migrations"; 2 | 3 | export class Migration20210604132200 extends Migration { 4 | async up(): Promise { 5 | this.addSql( 6 | 'create table "ReviewDetails" ("id" uuid not null, "comment" varchar(255) not null, "rate" int4 not null, "fullfiledAt" timestamptz(0) not null);', 7 | ); 8 | this.addSql('alter table "ReviewDetails" add constraint "ReviewDetails_pkey" primary key ("id");'); 9 | 10 | this.addSql( 11 | 'create table "BookingReview" ("id" uuid not null, "reviewerId" uuid not null, "revieweeId" uuid not null, "reviewDetailsId" uuid null);', 12 | ); 13 | this.addSql('alter table "BookingReview" add constraint "BookingReview_pkey" primary key ("id");'); 14 | this.addSql( 15 | 'alter table "BookingReview" add constraint "BookingReview_reviewDetailsId_unique" unique ("reviewDetailsId");', 16 | ); 17 | 18 | this.addSql( 19 | 'alter table "BookingReview" add constraint "BookingReview_reviewDetailsId_foreign" foreign key ("reviewDetailsId") references "ReviewDetails" ("id") on update cascade on delete set null;', 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/app/travelhoop/src/migrations/migration-20210605185254.ts: -------------------------------------------------------------------------------- 1 | import { Migration } from '@mikro-orm/migrations'; 2 | 3 | export class Migration20210605185254 extends Migration { 4 | 5 | async up(): Promise { 6 | this.addSql('alter table "BookableCouch" add column "state" text check ("state" in (\'Active\', \'Archived\')) not null;'); 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/app/travelhoop/src/module.loader.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require */ 2 | /* eslint-disable import/no-dynamic-require */ 3 | import { join } from "path"; 4 | import { readdirSync } from "fs"; 5 | import { AppModule } from "@travelhoop/infrastructure"; 6 | 7 | export const loadModules = (): AppModule[] => { 8 | const modulePath = join(__dirname, "../node_modules/@travelhoop"); 9 | const fileNames = readdirSync(modulePath).filter(fileName => fileName.includes("-module")); 10 | const modules = fileNames.map(fileName => { 11 | const path = join(modulePath, fileName, "build", "index.js"); 12 | return require(path).default; 13 | }); 14 | return modules; 15 | }; 16 | -------------------------------------------------------------------------------- /src/app/travelhoop/src/server.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | import { Logger } from "@travelhoop/abstract-core"; 3 | import { createBackgroundMessageDispatcher, DbConnection } from "@travelhoop/infrastructure"; 4 | import { Server } from "http"; 5 | import { loadEnvs } from "@travelhoop/infrastructure"; 6 | import { createClient } from "redis"; 7 | import { appConfigFactory } from "./config/config"; 8 | import { dbConfigFactory } from "./config/db-config"; 9 | import { setupContainer } from "./container"; 10 | import { loadModules } from "./module.loader"; 11 | 12 | loadEnvs(); 13 | 14 | (async () => { 15 | const appConfig = appConfigFactory(process.env as any); 16 | const appModules = loadModules(); 17 | const dbConfig = dbConfigFactory( 18 | process.env as any, 19 | appModules.map(appModule => appModule.name), 20 | ); 21 | const redis = createClient(appConfig.redis.url); 22 | const container = await setupContainer({ appConfig, dbConfig, appModules, redis }); 23 | const logger = container.resolve("logger"); 24 | const dbConnection = container.resolve("dbConnection"); 25 | 26 | await dbConnection.getMigrator().up(); 27 | 28 | process.on("uncaughtException", err => { 29 | logger.error(`Uncaught: ${err.toString()}`, err); 30 | process.exit(1); 31 | }); 32 | 33 | process.on("unhandledRejection", err => { 34 | if (err) { 35 | logger.error(`Unhandled: ${err.toString()}`, err); 36 | } 37 | process.exit(1); 38 | }); 39 | 40 | const server: Server = container.resolve("server"); 41 | const port = container.resolve("port"); 42 | 43 | createBackgroundMessageDispatcher({ redis, logger, modules: appModules, queueName: appConfig.queues.messageBroker }); 44 | server.listen(port); 45 | 46 | logger.info(`listening on port: ${port}`); 47 | })(); 48 | -------------------------------------------------------------------------------- /src/app/travelhoop/src/tests/bootstrap.ts: -------------------------------------------------------------------------------- 1 | import { before } from "mocha"; 2 | import { loadEnvs } from "@travelhoop/infrastructure"; 3 | import "reflect-metadata"; 4 | import { MikroORM } from "@mikro-orm/core"; 5 | import { dbConfigFactory } from "../config/db-config"; 6 | import { loadModules } from "../module.loader"; 7 | 8 | loadEnvs(); 9 | 10 | before(() => { 11 | const appModules = loadModules(); 12 | 13 | const dbConfig = dbConfigFactory( 14 | process.env as any, 15 | appModules.map(appModule => appModule.name), 16 | ); 17 | 18 | return MikroORM.init(dbConfig, false); 19 | }); 20 | -------------------------------------------------------------------------------- /src/app/travelhoop/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/@travelhoop/toolchain/includes/tsconfig.web.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "typeRoots": ["./node_modules/@types"] 6 | } 7 | } -------------------------------------------------------------------------------- /src/modules/booking/.eslintignore: -------------------------------------------------------------------------------- 1 | **/*.d.ts 2 | *.json -------------------------------------------------------------------------------- /src/modules/booking/.eslintrc.js: -------------------------------------------------------------------------------- 1 | require('@travelhoop/toolchain/patch/modern-module-resolution'); 2 | 3 | module.exports = { 4 | extends: [ "./node_modules/@travelhoop/toolchain/includes/.eslintrc" ], // <---- put your profile string here 5 | parserOptions: { 6 | project: "tsconfig.json", 7 | tsconfigRootDir: __dirname 8 | } 9 | }; -------------------------------------------------------------------------------- /src/modules/booking/.swcrc: -------------------------------------------------------------------------------- 1 | { 2 | "jsc": { 3 | "parser": { 4 | "syntax": "typescript", 5 | "decorators": true, 6 | "dynamicImport": true 7 | }, 8 | "transform": { 9 | "legacyDecorator": true, 10 | "decoratorMetadata": true 11 | }, 12 | "target": "es2018", 13 | "keepClassNames": true, 14 | "loose": true 15 | }, 16 | "module": { 17 | "type": "commonjs", 18 | "strict": true, 19 | "strictMode": true, 20 | "lazy": false, 21 | "noInterop": false 22 | }, 23 | "sourceMaps": "inline" 24 | } -------------------------------------------------------------------------------- /src/modules/booking/booking.rest: -------------------------------------------------------------------------------- 1 | @url = http://localhost:3010/booking 2 | 3 | @bookableCouchId=618791b8-0674-8cec-bcaa-f310da8f5534 4 | 5 | @couchBookingRequestId=dece7048-280a-0cb8-a401-926610aca82c 6 | 7 | @guestAccessToken=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6eyJ2YWx1ZSI6IjY5YjBkNjViLTNhYjMtMTg3Yy0xZTM4LTk4ZDI5MDJiYTk4YyJ9LCJleHAiOjE2MjAxNDk3NTksImlhdCI6MTYyMDE0Nzk1OX0.X5v8yZj8KL3yGRYs-b-Lxoc-Wm4Zrgz6gJwNWcXwyWc 8 | 9 | @hostAccessToken=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6eyJ2YWx1ZSI6ImFlZjcxODdlLTgyOWItNzJmYi1iNDczLTZlNDFiNWFmMTkxOSJ9LCJleHAiOjE2MjI5MjU4NzMsImlhdCI6MTYyMjkyNDA3M30.c570z0UpXYG3pcCX2gFsPrKyO-7qVMqZbWKdX5ZBc1k 10 | ### 11 | POST {{url}}/request-booking HTTP/1.1 12 | content-type: application/json 13 | authorization: Bearer {{guestAccessToken}} 14 | 15 | { 16 | "bookableCouchId":"{{bookableCouchId}}", 17 | "dateFrom": "2021-10-23T00:00:00.000Z", 18 | "dateTo": "2021-10-25T00:00:00.000Z", 19 | "quantity": 1 20 | } 21 | ### 22 | POST {{url}}/create-booking HTTP/1.1 23 | content-type: application/json 24 | authorization: Bearer {{hostAccessToken}} 25 | 26 | { 27 | "couchBookingRequestId":"{{couchBookingRequestId}}" 28 | } 29 | ### 30 | POST {{url}}/reject-booking-request HTTP/1.1 31 | content-type: application/json 32 | authorization: Bearer {{hostAccessToken}} 33 | 34 | { 35 | "couchBookingRequestId":"{{couchBookingRequestId}}", 36 | } 37 | ### 38 | POST {{url}}/finish-bookings HTTP/1.1 39 | content-type: application/json 40 | authorization: Bearer {{hostAccessToken}} 41 | ### 42 | POST {{url}}/archive HTTP/1.1 43 | content-type: application/json 44 | authorization: Bearer {{hostAccessToken}} 45 | 46 | { 47 | "bookableCouchId":"{{bookableCouchId}}" 48 | } -------------------------------------------------------------------------------- /src/modules/booking/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@travelhoop/booking-module", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main":"./build/index.js", 6 | "typings":"./build/index.d.ts", 7 | "scripts": { 8 | "test": "mocha --file=\"build/domain/tests/bootstrap.js\" \"build/**/*.test.js\"", 9 | "build": "rimraf build && tsc", 10 | "build:watch":"swc src -d build", 11 | "lint": "eslint './**/*.ts'", 12 | "lint:fix": "eslint './**/*.ts' --fix" 13 | }, 14 | "author": "", 15 | "license": "ISC", 16 | "dependencies": { 17 | "@travelhoop/abstract-core": "1.0.0", 18 | "@travelhoop/infrastructure": "1.0.0", 19 | "@travelhoop/shared-kernel": "1.0.0", 20 | "express": "~4.17.1", 21 | "rimraf": "~3.0.2", 22 | "guid-typescript": "~1.0.9", 23 | "class-validator": "~0.13.1", 24 | "awilix": "~4.3.3", 25 | "awilix-express": "~4.0.0", 26 | "express-async-handler": "~1.1.4", 27 | "@mikro-orm/core": "~4.5.5", 28 | "@mikro-orm/postgresql": "~4.5.5", 29 | "date-fns": "~2.20.1", 30 | "http-status-codes": "~2.1.4", 31 | "yup": "~0.32.9", 32 | "reflect-metadata": "~0.1.13" 33 | }, 34 | "devDependencies": { 35 | "eslint": "~7.23.0", 36 | "typescript": "~4.2.3", 37 | "@travelhoop/toolchain":"1.0.0", 38 | "@types/express": "~4.17.11", 39 | "@types/node": "~14.14.37", 40 | "@swc/core": "~1.2.51", 41 | "@swc/cli": "~0.1.36", 42 | "mocha": "~8.3.2", 43 | "chai": "~4.3.4", 44 | "@types/mocha": "~8.2.2", 45 | "@types/chai": "~4.2.17", 46 | "ts-mockito": "~2.6.1" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/modules/booking/src/api/booking.module.ts: -------------------------------------------------------------------------------- 1 | import { standardAppModuleFactory, AppModule } from "@travelhoop/infrastructure"; 2 | import { createContainer } from "../infrastructure/container"; 3 | 4 | export const bookingModule: AppModule = standardAppModuleFactory({ 5 | basePath: "booking", 6 | name: "booking-module", 7 | createContainer, 8 | }); 9 | -------------------------------------------------------------------------------- /src/modules/booking/src/api/routes/bookable-couch.router.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "@travelhoop/infrastructure" 2 | import { makeInvoker } from "awilix-express"; 3 | import { validateOrReject } from "class-validator"; 4 | import asyncHandler from "express-async-handler"; 5 | import { CommandDispatcher } from "@travelhoop/abstract-core"; 6 | import { FinishBookingsCommand } from "../../application/bookable-couch/handlers/finish-bookings/finish-bookings.command"; 7 | import { CreateBookingCommand } from "../../application/bookable-couch/handlers/create-booking/create-booking.command"; 8 | import { RequestCouchBookingCommand } from "../../application/couch-booking-request/handlers/request-couch-booking/request-couch-booking.command"; 9 | import { ArchiveBookableCouchCommand } from "../../application/bookable-couch/handlers/archive-bookable-couch/archive-bookable-couch.command"; 10 | 11 | interface BookableCouchApiDependencies { 12 | commandDispatcher: CommandDispatcher; 13 | } 14 | 15 | const api = ({ commandDispatcher }: BookableCouchApiDependencies) => ({ 16 | requestCouchBooking: asyncHandler(async (req: Request, res: Response) => { 17 | const command = new RequestCouchBookingCommand({ 18 | ...req.body, 19 | guestId: req.user?.id!, 20 | dateFrom: new Date(req.body.dateFrom), 21 | dateTo: new Date(req.body.dateTo), 22 | }); 23 | await validateOrReject(command); 24 | res.json(await commandDispatcher.execute(command)); 25 | }), 26 | createBooking: asyncHandler(async (req: Request, res: Response) => { 27 | const command = new CreateBookingCommand(req.body); 28 | await validateOrReject(command); 29 | res.json(await commandDispatcher.execute(command)); 30 | }), 31 | finishBookings: asyncHandler(async (_req: Request, res: Response) => { 32 | const command = new FinishBookingsCommand({}); 33 | res.json(await commandDispatcher.execute(command)); 34 | }), 35 | archive: asyncHandler(async (req: Request, res: Response) => { 36 | const command = new ArchiveBookableCouchCommand({ ...req.body }); 37 | await validateOrReject(command); 38 | res.json(await commandDispatcher.execute(command)); 39 | }), 40 | }); 41 | 42 | export const bookableCouchApi = makeInvoker(api); 43 | -------------------------------------------------------------------------------- /src/modules/booking/src/api/routes/couch-booking-request.router.ts: -------------------------------------------------------------------------------- 1 | import { CommandDispatcher } from "@travelhoop/abstract-core"; 2 | import { Request, Response } from "@travelhoop/infrastructure"; 3 | import { makeInvoker } from "awilix-express"; 4 | import { validateOrReject } from "class-validator"; 5 | import asyncHandler from "express-async-handler"; 6 | import { RejectCouchBookingRequestCommand } from "../../application/couch-booking-request/handlers/reject-couch-booking-request/reject-couch-booking-request.command"; 7 | 8 | interface CouchBookingRequestApiDependencies { 9 | commandDispatcher: CommandDispatcher; 10 | } 11 | 12 | const api = ({ commandDispatcher }: CouchBookingRequestApiDependencies) => ({ 13 | rejectRequest: asyncHandler(async (req: Request, res: Response) => { 14 | const command = new RejectCouchBookingRequestCommand({ ...req.body, userId: req.user?.id! }); 15 | await validateOrReject(command); 16 | res.json(await commandDispatcher.execute(command)); 17 | }), 18 | }); 19 | 20 | export const couchBookingRequestApi = makeInvoker(api); 21 | -------------------------------------------------------------------------------- /src/modules/booking/src/api/routes/router.ts: -------------------------------------------------------------------------------- 1 | import { MiddlewareType } from "@travelhoop/infrastructure"; 2 | import express, { Router } from "express"; 3 | import { bookableCouchApi } from "./bookable-couch.router"; 4 | import { couchBookingRequestApi } from "./couch-booking-request.router"; 5 | 6 | export const createRouter = ({ 7 | auth, 8 | checkSchedulerToken, 9 | }: { 10 | auth: MiddlewareType; 11 | checkSchedulerToken: MiddlewareType; 12 | }): Router => { 13 | const router = express.Router(); 14 | 15 | router.post("/request-booking", auth, bookableCouchApi("requestCouchBooking")); 16 | router.post("/create-booking", auth, bookableCouchApi("createBooking")); 17 | router.post("/finish-bookings", auth, bookableCouchApi("finishBookings")); 18 | router.post("/archive", auth, bookableCouchApi("archive")); 19 | router.post("/reject-booking-request", checkSchedulerToken, couchBookingRequestApi("rejectRequest")); 20 | 21 | return router; 22 | }; 23 | -------------------------------------------------------------------------------- /src/modules/booking/src/application/bookable-couch/events/couch-created.event.ts: -------------------------------------------------------------------------------- 1 | import { Event } from "@travelhoop/abstract-core"; 2 | 3 | interface CouchCreatedPayload { 4 | id: string; 5 | hostId: string; 6 | quantity: number; 7 | } 8 | 9 | export class CouchCreated implements Event { 10 | name = this.constructor.name; 11 | 12 | constructor(public payload: CouchCreatedPayload) {} 13 | } 14 | -------------------------------------------------------------------------------- /src/modules/booking/src/application/bookable-couch/handlers/archive-bookable-couch/archive-bookable-couch.command.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "@travelhoop/abstract-core"; 2 | import { AggregateId } from "@travelhoop/shared-kernel"; 3 | 4 | interface ArchiveBookableCouchCommandPayload { 5 | bookableCouchId: AggregateId; 6 | } 7 | 8 | export class ArchiveBookableCouchCommand implements Command { 9 | constructor(public payload: ArchiveBookableCouchCommandPayload) {} 10 | } 11 | -------------------------------------------------------------------------------- /src/modules/booking/src/application/bookable-couch/handlers/archive-bookable-couch/archive-bookable-couch.handler.ts: -------------------------------------------------------------------------------- 1 | import { CommandHandler } from "@travelhoop/abstract-core"; 2 | import { DomainEventDispatcher } from "@travelhoop/shared-kernel"; 3 | import { BookableCouchRepository, CouchBookingRequestRepository } from "../../../../domain"; 4 | import { ArchiveBookableCouchCommand } from "./archive-bookable-couch.command"; 5 | 6 | interface ArchiveBookableCouchCommandHandlerDependencies { 7 | couchBookingRequestRepository: CouchBookingRequestRepository; 8 | bookableCouchRepository: BookableCouchRepository; 9 | domainEventDispatcher: DomainEventDispatcher; 10 | } 11 | 12 | export class ArchiveBookableCouchCommandHandler implements CommandHandler { 13 | constructor(private readonly deps: ArchiveBookableCouchCommandHandlerDependencies) {} 14 | 15 | async execute({ payload }: ArchiveBookableCouchCommand) { 16 | const bookableCouch = await this.deps.bookableCouchRepository.get(payload.bookableCouchId); 17 | 18 | bookableCouch.archive(); 19 | 20 | await this.deps.bookableCouchRepository.save(bookableCouch); 21 | await this.deps.domainEventDispatcher.dispatch(bookableCouch.events); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/modules/booking/src/application/bookable-couch/handlers/cancel-booking/cancel-booking.command.ts: -------------------------------------------------------------------------------- 1 | import { Guid } from "guid-typescript"; 2 | import { Command } from "@travelhoop/abstract-core"; 3 | import { AggregateId } from "@travelhoop/shared-kernel"; 4 | 5 | interface CancelBookingCommandPayload { 6 | bookableCouchId: AggregateId; 7 | couchBookingId: Guid; 8 | reason: string; 9 | } 10 | 11 | export class CancelBookingCommand implements Command { 12 | constructor(public payload: CancelBookingCommandPayload) {} 13 | } 14 | -------------------------------------------------------------------------------- /src/modules/booking/src/application/bookable-couch/handlers/cancel-booking/cancel-booking.handler.ts: -------------------------------------------------------------------------------- 1 | import { DomainEventDispatcher } from "@travelhoop/shared-kernel"; 2 | import { CommandHandler } from "@travelhoop/abstract-core"; 3 | import { 4 | CouchBookingCancellationPolicy, 5 | BookableCouchRepository, 6 | CouchBookingRequestRepository, 7 | } from "../../../../domain"; 8 | import { CancelBookingCommand } from "./cancel-booking.command"; 9 | 10 | interface CancelBookingCommandHandlerDependencies { 11 | couchBookingRequestRepository: CouchBookingRequestRepository; 12 | bookableCouchRepository: BookableCouchRepository; 13 | domainEventDispatcher: DomainEventDispatcher; 14 | } 15 | 16 | export class CancelBookingCommandHandler implements CommandHandler { 17 | constructor(private readonly deps: CancelBookingCommandHandlerDependencies) {} 18 | 19 | async execute({ payload }: CancelBookingCommand) { 20 | const bookableCouch = await this.deps.bookableCouchRepository.get(payload.bookableCouchId); 21 | 22 | bookableCouch.cancelBooking( 23 | payload.couchBookingId, 24 | payload.reason, 25 | new CouchBookingCancellationPolicy({ maxDaysBeforeCancellation: 3 }), 26 | ); 27 | 28 | await this.deps.domainEventDispatcher.dispatch(bookableCouch.events); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/modules/booking/src/application/bookable-couch/handlers/cancel-booking/cancel-booking.validator.ts: -------------------------------------------------------------------------------- 1 | // import { Guid } from "guid-typescript"; 2 | // import { Command } from "@travelhoop/abstract-core"; 3 | // import { AggregateId } from "@travelhoop/shared-kernel"; 4 | // import { object, date, SchemaOf, number } from "yup"; 5 | 6 | // interface RequestCouchBookingCommandPayload { 7 | // couchId: AggregateId; 8 | // guestId: Guid; 9 | // dateFrom: Date; 10 | // dateTo: Date; 11 | // quantity: number; 12 | // } 13 | 14 | // export class RequestCouchBookingCommand implements Command { 15 | // constructor(public payload: RequestCouchBookingCommandPayload) {} 16 | // } 17 | 18 | // const aggregateIdValidator: SchemaOf = object().test("is-aggregate-id", "It is not an aggregate id", value => 19 | // AggregateId.isAggregateId((value as unknown) as AggregateId), 20 | // ); 21 | 22 | // const guidValidator: SchemaOf = object().test("is-aggregate-id", "It is not an aggregate id", value => 23 | // Guid.isGuid((value as unknown) as Guid), 24 | // ); 25 | 26 | // export const requestCouchBookingCommandValidator: SchemaOf = object({ 27 | // payload: object({ 28 | // couchId: aggregateIdValidator.required(), 29 | // guestId: guidValidator.required(), 30 | // dateFrom: date().required(), 31 | // dateTo: date().required(), 32 | // quantity: number().min(1).required(), 33 | // }).required(), 34 | // }); 35 | -------------------------------------------------------------------------------- /src/modules/booking/src/application/bookable-couch/handlers/create-booking/create-booking.command.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "@travelhoop/abstract-core"; 2 | import { AggregateId } from "@travelhoop/shared-kernel"; 3 | 4 | interface CreateBookingCommandPayload { 5 | couchBookingRequestId: AggregateId; 6 | } 7 | 8 | export class CreateBookingCommand implements Command { 9 | constructor(public payload: CreateBookingCommandPayload) {} 10 | } 11 | -------------------------------------------------------------------------------- /src/modules/booking/src/application/bookable-couch/handlers/create-booking/create-booking.handler.ts: -------------------------------------------------------------------------------- 1 | import { DomainEventDispatcher } from "@travelhoop/shared-kernel"; 2 | import { CommandHandler } from "@travelhoop/abstract-core"; 3 | import { BookableCouchRepository, CouchBookingRequestRepository } from "../../../../domain"; 4 | import { CreateBookingCommand } from "./create-booking.command"; 5 | 6 | interface CreateBookingCommandHandlerDependencies { 7 | couchBookingRequestRepository: CouchBookingRequestRepository; 8 | bookableCouchRepository: BookableCouchRepository; 9 | domainEventDispatcher: DomainEventDispatcher; 10 | } 11 | 12 | export class CreateBookingCommandHandler implements CommandHandler { 13 | constructor(private readonly deps: CreateBookingCommandHandlerDependencies) {} 14 | 15 | async execute({ payload }: CreateBookingCommand) { 16 | const couchBookingRequest = await this.deps.couchBookingRequestRepository.get(payload.couchBookingRequestId); 17 | 18 | const couchBookingRequestDto = couchBookingRequest.toDto(); 19 | 20 | const bookableCouch = await this.deps.bookableCouchRepository.get(couchBookingRequestDto.bookableCouchId); 21 | 22 | bookableCouch.createBooking(couchBookingRequestDto); 23 | 24 | await this.deps.bookableCouchRepository.save(bookableCouch); 25 | 26 | await this.deps.domainEventDispatcher.dispatch(bookableCouch.events); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/modules/booking/src/application/bookable-couch/handlers/create-booking/create-booking.validator.ts: -------------------------------------------------------------------------------- 1 | // import { Guid } from "guid-typescript"; 2 | // import { Command } from "@travelhoop/abstract-core"; 3 | // import { AggregateId } from "@travelhoop/shared-kernel"; 4 | // import { object, date, SchemaOf, number } from "yup"; 5 | 6 | // interface RequestCouchBookingCommandPayload { 7 | // couchId: AggregateId; 8 | // guestId: Guid; 9 | // dateFrom: Date; 10 | // dateTo: Date; 11 | // quantity: number; 12 | // } 13 | 14 | // export class RequestCouchBookingCommand implements Command { 15 | // constructor(public payload: RequestCouchBookingCommandPayload) {} 16 | // } 17 | 18 | // const aggregateIdValidator: SchemaOf = object().test("is-aggregate-id", "It is not an aggregate id", value => 19 | // AggregateId.isAggregateId((value as unknown) as AggregateId), 20 | // ); 21 | 22 | // const guidValidator: SchemaOf = object().test("is-aggregate-id", "It is not an aggregate id", value => 23 | // Guid.isGuid((value as unknown) as Guid), 24 | // ); 25 | 26 | // export const requestCouchBookingCommandValidator: SchemaOf = object({ 27 | // payload: object({ 28 | // couchId: aggregateIdValidator.required(), 29 | // guestId: guidValidator.required(), 30 | // dateFrom: date().required(), 31 | // dateTo: date().required(), 32 | // quantity: number().min(1).required(), 33 | // }).required(), 34 | // }); 35 | -------------------------------------------------------------------------------- /src/modules/booking/src/application/bookable-couch/handlers/finish-bookings/finish-bookings.command.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "@travelhoop/abstract-core"; 2 | 3 | interface FinishBookingsCommandPayload {} 4 | 5 | export class FinishBookingsCommand implements Command { 6 | constructor(public payload: FinishBookingsCommandPayload) {} 7 | } 8 | -------------------------------------------------------------------------------- /src/modules/booking/src/application/bookable-couch/handlers/finish-bookings/finish-bookings.handler.ts: -------------------------------------------------------------------------------- 1 | import { CommandHandler } from "@travelhoop/abstract-core"; 2 | import { DomainEventDispatcher } from "@travelhoop/shared-kernel"; 3 | import { BookableCouchRepository, CouchBookingRequestRepository } from "../../../../domain"; 4 | import { FinishBookingsCommand } from "./finish-bookings.command"; 5 | 6 | interface FinishBookingsCommandHandlerDependencies { 7 | couchBookingRequestRepository: CouchBookingRequestRepository; 8 | bookableCouchRepository: BookableCouchRepository; 9 | domainEventDispatcher: DomainEventDispatcher; 10 | } 11 | 12 | export class FinishBookingsCommandHandler implements CommandHandler { 13 | constructor(private readonly deps: FinishBookingsCommandHandlerDependencies) {} 14 | 15 | async execute(_command: FinishBookingsCommand) { 16 | const bookableCouches = await this.deps.bookableCouchRepository.findWithFinishedBookings(); 17 | 18 | bookableCouches.forEach(bookableCouch => bookableCouch.finishBookings()); 19 | 20 | await Promise.all( 21 | bookableCouches.map(bookableCouch => this.deps.domainEventDispatcher.dispatch(bookableCouch.events)), 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/modules/booking/src/application/bookable-couch/handlers/finish-bookings/finish-bookings.validator.ts: -------------------------------------------------------------------------------- 1 | // import { Guid } from "guid-typescript"; 2 | // import { Command } from "@travelhoop/abstract-core"; 3 | // import { AggregateId } from "@travelhoop/shared-kernel"; 4 | // import { object, date, SchemaOf, number } from "yup"; 5 | 6 | // interface RequestCouchBookingCommandPayload { 7 | // couchId: AggregateId; 8 | // guestId: Guid; 9 | // dateFrom: Date; 10 | // dateTo: Date; 11 | // quantity: number; 12 | // } 13 | 14 | // export class RequestCouchBookingCommand implements Command { 15 | // constructor(public payload: RequestCouchBookingCommandPayload) {} 16 | // } 17 | 18 | // const aggregateIdValidator: SchemaOf = object().test("is-aggregate-id", "It is not an aggregate id", value => 19 | // AggregateId.isAggregateId((value as unknown) as AggregateId), 20 | // ); 21 | 22 | // const guidValidator: SchemaOf = object().test("is-aggregate-id", "It is not an aggregate id", value => 23 | // Guid.isGuid((value as unknown) as Guid), 24 | // ); 25 | 26 | // export const requestCouchBookingCommandValidator: SchemaOf = object({ 27 | // payload: object({ 28 | // couchId: aggregateIdValidator.required(), 29 | // guestId: guidValidator.required(), 30 | // dateFrom: date().required(), 31 | // dateTo: date().required(), 32 | // quantity: number().min(1).required(), 33 | // }).required(), 34 | // }); 35 | -------------------------------------------------------------------------------- /src/modules/booking/src/application/bookable-couch/subscribers/couch-created.subscriber.ts: -------------------------------------------------------------------------------- 1 | import { Guid } from "guid-typescript"; 2 | import { EventSubscriber, EventSubscribersMeta } from "@travelhoop/abstract-core"; 3 | import { CouchCreated } from "../events/couch-created.event"; 4 | import { BookableCouch, BookableCouchRepository } from "../../../domain"; 5 | 6 | interface CouchCreatedSubscriberDependencies { 7 | bookableCouchRepository: BookableCouchRepository; 8 | } 9 | 10 | export class CouchCreatedSubscriber implements EventSubscriber { 11 | constructor(private readonly deps: CouchCreatedSubscriberDependencies) {} 12 | 13 | public getSubscribedEvents(): EventSubscribersMeta[] { 14 | return [{ name: CouchCreated.name, method: "onCouchCreated" }]; 15 | } 16 | 17 | async onCouchCreated({ payload: { id, hostId, quantity } }: CouchCreated) { 18 | const bookableCouch = BookableCouch.create({ id: Guid.parse(id), hostId: Guid.parse(hostId), quantity }); 19 | if (await this.deps.bookableCouchRepository.find(bookableCouch.id)) { 20 | return; 21 | } 22 | return this.deps.bookableCouchRepository.add(bookableCouch); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/modules/booking/src/application/couch-booking-request/handlers/reject-couch-booking-request/reject-couch-booking-request.command.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "@travelhoop/abstract-core"; 2 | import { AggregateId } from "@travelhoop/shared-kernel"; 3 | 4 | interface RejectCouchBookingRequestCommandPayload { 5 | couchBookingRequestId: AggregateId; 6 | rejectionReasong: string; 7 | } 8 | 9 | export class RejectCouchBookingRequestCommand implements Command { 10 | constructor(public payload: RejectCouchBookingRequestCommandPayload) {} 11 | } 12 | -------------------------------------------------------------------------------- /src/modules/booking/src/application/couch-booking-request/handlers/reject-couch-booking-request/reject-couch-booking-request.handler.ts: -------------------------------------------------------------------------------- 1 | import { CommandHandler } from "@travelhoop/abstract-core"; 2 | import { CouchBookingRequestRepository } from "../../../../domain"; 3 | import { RejectCouchBookingRequestCommand } from "./reject-couch-booking-request.command"; 4 | 5 | interface RejectCouchBookingRequestCommandHandlerDependencies { 6 | couchBookingRequestRepository: CouchBookingRequestRepository; 7 | } 8 | 9 | export class RejectCouchBookingRequestCommandHandler implements CommandHandler { 10 | constructor(private readonly deps: RejectCouchBookingRequestCommandHandlerDependencies) {} 11 | 12 | async execute({ payload }: RejectCouchBookingRequestCommand) { 13 | const couchBookingRequest = await this.deps.couchBookingRequestRepository.get(payload.couchBookingRequestId); 14 | couchBookingRequest.reject(payload.rejectionReasong); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/modules/booking/src/application/couch-booking-request/handlers/reject-couch-booking-request/reject-couch-booking-request.validator.ts: -------------------------------------------------------------------------------- 1 | // import { Guid } from "guid-typescript"; 2 | // import { Command } from "@travelhoop/abstract-core"; 3 | // import { AggregateId } from "@travelhoop/shared-kernel"; 4 | // import { object, date, SchemaOf, number } from "yup"; 5 | 6 | // interface RequestCouchBookingCommandPayload { 7 | // couchId: AggregateId; 8 | // guestId: Guid; 9 | // dateFrom: Date; 10 | // dateTo: Date; 11 | // quantity: number; 12 | // } 13 | 14 | // export class RequestCouchBookingCommand implements Command { 15 | // constructor(public payload: RequestCouchBookingCommandPayload) {} 16 | // } 17 | 18 | // const aggregateIdValidator: SchemaOf = object().test("is-aggregate-id", "It is not an aggregate id", value => 19 | // AggregateId.isAggregateId((value as unknown) as AggregateId), 20 | // ); 21 | 22 | // const guidValidator: SchemaOf = object().test("is-aggregate-id", "It is not an aggregate id", value => 23 | // Guid.isGuid((value as unknown) as Guid), 24 | // ); 25 | 26 | // export const requestCouchBookingCommandValidator: SchemaOf = object({ 27 | // payload: object({ 28 | // couchId: aggregateIdValidator.required(), 29 | // guestId: guidValidator.required(), 30 | // dateFrom: date().required(), 31 | // dateTo: date().required(), 32 | // quantity: number().min(1).required(), 33 | // }).required(), 34 | // }); 35 | -------------------------------------------------------------------------------- /src/modules/booking/src/application/couch-booking-request/handlers/request-couch-booking/request-couch-booking.command.ts: -------------------------------------------------------------------------------- 1 | import { Guid } from "guid-typescript"; 2 | import { Command } from "@travelhoop/abstract-core"; 3 | import { AggregateId } from "@travelhoop/shared-kernel"; 4 | 5 | interface RequestCouchBookingCommandPayload { 6 | bookableCouchId: AggregateId; 7 | guestId: Guid; 8 | dateFrom: Date; 9 | dateTo: Date; 10 | quantity: number; 11 | } 12 | 13 | export class RequestCouchBookingCommand implements Command { 14 | constructor(public payload: RequestCouchBookingCommandPayload) {} 15 | } 16 | -------------------------------------------------------------------------------- /src/modules/booking/src/application/couch-booking-request/handlers/request-couch-booking/request-couch-booking.handler.ts: -------------------------------------------------------------------------------- 1 | import { CommandHandler } from "@travelhoop/abstract-core"; 2 | import { CouchBookingRequestDomainService } from "../../../../domain"; 3 | import { RequestCouchBookingCommand } from "./request-couch-booking.command"; 4 | 5 | interface RequestCouchBookingCommandHandlerDependencies { 6 | couchBookingRequestDomainService: CouchBookingRequestDomainService; 7 | } 8 | 9 | export class RequestCouchBookingCommandHandler implements CommandHandler { 10 | constructor(private readonly deps: RequestCouchBookingCommandHandlerDependencies) {} 11 | 12 | async execute({ payload }: RequestCouchBookingCommand) { 13 | return this.deps.couchBookingRequestDomainService.createRequest(payload); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/modules/booking/src/application/couch-booking-request/handlers/request-couch-booking/request-couch-booking.validator.ts: -------------------------------------------------------------------------------- 1 | // import { Guid } from "guid-typescript"; 2 | // import { Command } from "@travelhoop/abstract-core"; 3 | // import { AggregateId } from "@travelhoop/shared-kernel"; 4 | // import { object, date, SchemaOf, number } from "yup"; 5 | 6 | // interface RequestCouchBookingCommandPayload { 7 | // couchId: AggregateId; 8 | // guestId: Guid; 9 | // dateFrom: Date; 10 | // dateTo: Date; 11 | // quantity: number; 12 | // } 13 | 14 | // export class RequestCouchBookingCommand implements Command { 15 | // constructor(public payload: RequestCouchBookingCommandPayload) {} 16 | // } 17 | 18 | // const aggregateIdValidator: SchemaOf = object().test("is-aggregate-id", "It is not an aggregate id", value => 19 | // AggregateId.isAggregateId((value as unknown) as AggregateId), 20 | // ); 21 | 22 | // const guidValidator: SchemaOf = object().test("is-aggregate-id", "It is not an aggregate id", value => 23 | // Guid.isGuid((value as unknown) as Guid), 24 | // ); 25 | 26 | // export const requestCouchBookingCommandValidator: SchemaOf = object({ 27 | // payload: object({ 28 | // couchId: aggregateIdValidator.required(), 29 | // guestId: guidValidator.required(), 30 | // dateFrom: date().required(), 31 | // dateTo: date().required(), 32 | // quantity: number().min(1).required(), 33 | // }).required(), 34 | // }); 35 | -------------------------------------------------------------------------------- /src/modules/booking/src/application/couch-booking-request/subscribers/couch-booking-created.subscriber.ts: -------------------------------------------------------------------------------- 1 | import { EventSubscriber, EventSubscribersMeta } from "@travelhoop/abstract-core"; 2 | import { CouchBookingCreated, CouchBookingRequestRepository } from "../../../domain"; 3 | 4 | interface CouchBookingCreatedSubscriberDependencies { 5 | couchBookingRequestRepository: CouchBookingRequestRepository; 6 | } 7 | 8 | export class CouchBookingCreatedSubscriber implements EventSubscriber { 9 | constructor(private readonly deps: CouchBookingCreatedSubscriberDependencies) {} 10 | 11 | public getSubscribedEvents(): EventSubscribersMeta[] { 12 | return [{ name: CouchBookingCreated.name, method: "onCouchBookingCreated" }]; 13 | } 14 | 15 | async onCouchBookingCreated({ payload: { couchBookingRequestId } }: CouchBookingCreated) { 16 | const couchBookingRequest = await this.deps.couchBookingRequestRepository.get(couchBookingRequestId); 17 | 18 | couchBookingRequest.accept(); 19 | 20 | await this.deps.couchBookingRequestRepository.save(couchBookingRequest); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/modules/booking/src/domain/bookable-couch/entity/booking.ts: -------------------------------------------------------------------------------- 1 | import { Guid } from "guid-typescript"; 2 | import { BookableCouch } from "./bookable-couch"; 3 | 4 | export interface BookingProps { 5 | id: Guid; 6 | dateFrom: Date; 7 | dateTo: Date; 8 | bookableCouch: BookableCouch; 9 | } 10 | 11 | export abstract class Booking { 12 | id: Guid; 13 | 14 | dateFrom: Date; 15 | 16 | dateTo: Date; 17 | 18 | bookableCouch: BookableCouch; 19 | } 20 | -------------------------------------------------------------------------------- /src/modules/booking/src/domain/bookable-couch/entity/couch-booking.ts: -------------------------------------------------------------------------------- 1 | import { Guid } from "guid-typescript"; 2 | import { Booking, BookingProps } from "./booking"; 3 | 4 | export interface CouchBookingProps extends BookingProps { 5 | guestId: Guid; 6 | quantity: number; 7 | } 8 | 9 | export class CouchBooking extends Booking { 10 | guestId: Guid; 11 | 12 | quantity: number; 13 | 14 | static create(props: Omit) { 15 | return new CouchBooking(props); 16 | } 17 | 18 | private constructor(props: Omit) { 19 | super(); 20 | this.id = props.id; 21 | this.guestId = props.guestId; 22 | this.dateFrom = props.dateFrom; 23 | this.dateTo = props.dateTo; 24 | this.quantity = props.quantity; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/modules/booking/src/domain/bookable-couch/entity/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./bookable-couch"; 2 | 3 | export * from "./booking"; 4 | 5 | export * from "./couch-booking"; 6 | 7 | export * from "./unavailable-booking"; 8 | -------------------------------------------------------------------------------- /src/modules/booking/src/domain/bookable-couch/entity/unavailable-booking.ts: -------------------------------------------------------------------------------- 1 | import { Booking, BookingProps } from "./booking"; 2 | 3 | export interface UnavailableBookingProps extends BookingProps {} 4 | 5 | export class UnavailableBooking extends Booking {} 6 | -------------------------------------------------------------------------------- /src/modules/booking/src/domain/bookable-couch/enum/bookable-couch-state.enum.ts: -------------------------------------------------------------------------------- 1 | export enum BookableCouchState { 2 | Active = "Active", 3 | Archived = "Archived", 4 | } 5 | -------------------------------------------------------------------------------- /src/modules/booking/src/domain/bookable-couch/enum/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./bookable-couch-state.enum"; 2 | -------------------------------------------------------------------------------- /src/modules/booking/src/domain/bookable-couch/error/all-couches-are-reserved.error.ts: -------------------------------------------------------------------------------- 1 | import { TravelhoopError } from "@travelhoop/abstract-core"; 2 | import StatusCodes from "http-status-codes"; 3 | 4 | export class AllCouchesAreReservedError extends TravelhoopError { 5 | constructor() { 6 | super("All couches are reserved", StatusCodes.CONFLICT); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/modules/booking/src/domain/bookable-couch/error/booking-not-found.error.ts: -------------------------------------------------------------------------------- 1 | import { TravelhoopError } from "@travelhoop/abstract-core"; 2 | import StatusCodes from "http-status-codes"; 3 | 4 | export class BookingNotFoundError extends TravelhoopError { 5 | constructor() { 6 | super("Booking not found", StatusCodes.NOT_FOUND); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/modules/booking/src/domain/bookable-couch/error/booking-unavailable.error.ts: -------------------------------------------------------------------------------- 1 | import { TravelhoopError } from "@travelhoop/abstract-core"; 2 | import StatusCodes from "http-status-codes"; 3 | 4 | export class BookingUnavailableError extends TravelhoopError { 5 | constructor(startDate: Date, endDate: Date) { 6 | super(`Between ${startDate} and ${endDate}, you cannot make a booking.`, StatusCodes.CONFLICT); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/modules/booking/src/domain/bookable-couch/error/cannot-archive-couch.error.ts: -------------------------------------------------------------------------------- 1 | import { TravelhoopError } from "@travelhoop/abstract-core"; 2 | import StatusCodes from "http-status-codes"; 3 | 4 | export class CannotArchiveCouchError extends TravelhoopError { 5 | constructor() { 6 | super("Cannot archive couch", StatusCodes.BAD_REQUEST); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/modules/booking/src/domain/bookable-couch/error/cannot-book-couch.error.ts: -------------------------------------------------------------------------------- 1 | import { TravelhoopError } from "@travelhoop/abstract-core"; 2 | import StatusCodes from "http-status-codes"; 3 | 4 | export class CannotBookCouchError extends TravelhoopError { 5 | constructor() { 6 | super("Cannot book couch", StatusCodes.BAD_REQUEST); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/modules/booking/src/domain/bookable-couch/error/cannot-cancel-booking.error.ts: -------------------------------------------------------------------------------- 1 | import { TravelhoopError } from "@travelhoop/abstract-core"; 2 | import StatusCodes from "http-status-codes"; 3 | 4 | export class CannotCancelBookingError extends TravelhoopError { 5 | constructor() { 6 | super("Cannot cancel booking", StatusCodes.BAD_REQUEST); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/modules/booking/src/domain/bookable-couch/error/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./booking-not-found.error"; 2 | 3 | export * from "./cannot-cancel-booking.error"; 4 | 5 | export * from "./cannot-book-couch.error"; 6 | 7 | export * from "./all-couches-are-reserved.error"; 8 | 9 | export * from "./booking-unavailable.error"; 10 | 11 | export * from "./cannot-archive-couch.error"; 12 | -------------------------------------------------------------------------------- /src/modules/booking/src/domain/bookable-couch/event/bookable-couch-archived.event.ts: -------------------------------------------------------------------------------- 1 | import { AggregateId, DomainEvent } from "@travelhoop/shared-kernel"; 2 | 3 | interface BookableCouchArchiveddPayload { 4 | bookableCouchId: AggregateId; 5 | } 6 | 7 | export class BookableCouchArchived implements DomainEvent { 8 | constructor(public payload: BookableCouchArchiveddPayload) {} 9 | } 10 | -------------------------------------------------------------------------------- /src/modules/booking/src/domain/bookable-couch/event/bookings-finished.event.ts: -------------------------------------------------------------------------------- 1 | import { DomainEvent } from "@travelhoop/shared-kernel"; 2 | import { Booking } from ".."; 3 | 4 | interface BookingsFinishedPayload { 5 | bookings: Booking[]; 6 | } 7 | 8 | export class BookingsFinished implements DomainEvent { 9 | constructor(public payload: BookingsFinishedPayload) {} 10 | } 11 | -------------------------------------------------------------------------------- /src/modules/booking/src/domain/bookable-couch/event/couch-booking-cancelled.event.ts: -------------------------------------------------------------------------------- 1 | import { DomainEvent } from "@travelhoop/shared-kernel"; 2 | import { CouchBooking } from ".."; 3 | 4 | interface CouchBookingCancelledPayload { 5 | couchBooking: CouchBooking; 6 | reason: string; 7 | } 8 | 9 | export class CouchBookingCancelled implements DomainEvent { 10 | constructor(public payload: CouchBookingCancelledPayload) {} 11 | } 12 | -------------------------------------------------------------------------------- /src/modules/booking/src/domain/bookable-couch/event/couch-booking-created.event.ts: -------------------------------------------------------------------------------- 1 | import { AggregateId, DomainEvent } from "@travelhoop/shared-kernel"; 2 | 3 | interface CouchBookingCreatedPayload { 4 | couchBookingRequestId: AggregateId; 5 | } 6 | 7 | export class CouchBookingCreated implements DomainEvent { 8 | name = this.constructor.name; 9 | 10 | constructor(public payload: CouchBookingCreatedPayload) {} 11 | } 12 | -------------------------------------------------------------------------------- /src/modules/booking/src/domain/bookable-couch/event/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./couch-booking-created.event"; 2 | 3 | export * from "./couch-booking-cancelled.event"; 4 | 5 | export * from "./bookable-couch-archived.event"; 6 | 7 | export * from "./bookings-finished.event"; 8 | -------------------------------------------------------------------------------- /src/modules/booking/src/domain/bookable-couch/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./entity"; 2 | 3 | export * from "./enum"; 4 | 5 | export * from "./event"; 6 | 7 | export * from "./repository"; 8 | 9 | export * from "./policy"; 10 | -------------------------------------------------------------------------------- /src/modules/booking/src/domain/bookable-couch/policy/couch-booking-cancellation.policy.ts: -------------------------------------------------------------------------------- 1 | import { addDays } from "date-fns"; 2 | import { CouchBooking } from "../entity"; 3 | 4 | export interface BookingCancellationPolicy { 5 | canCancel: (couchBooking: CouchBooking) => boolean; 6 | } 7 | 8 | interface CouchBookingCancellationPolicyDependencies { 9 | maxDaysBeforeCancellation: number; 10 | } 11 | 12 | export class CouchBookingCancellationPolicy implements BookingCancellationPolicy { 13 | constructor(private readonly deps: CouchBookingCancellationPolicyDependencies) {} 14 | 15 | canCancel(couchBooking: CouchBooking) { 16 | if (couchBooking.dateFrom > addDays(new Date(), this.deps.maxDaysBeforeCancellation)) { 17 | return true; 18 | } 19 | return false; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/modules/booking/src/domain/bookable-couch/policy/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./couch-booking-cancellation.policy"; 2 | -------------------------------------------------------------------------------- /src/modules/booking/src/domain/bookable-couch/repository/bookable-couch.repository.ts: -------------------------------------------------------------------------------- 1 | import { AggregateId } from "@travelhoop/shared-kernel"; 2 | import { BookableCouch } from "../entity/bookable-couch"; 3 | 4 | export interface BookableCouchRepository { 5 | get: (id: AggregateId) => Promise; 6 | find: (id: AggregateId) => Promise; 7 | findWithFinishedBookings: () => Promise; 8 | add: (bookableCouch: BookableCouch) => Promise; 9 | save: (bookableCouch: BookableCouch) => Promise; 10 | } 11 | -------------------------------------------------------------------------------- /src/modules/booking/src/domain/bookable-couch/repository/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./bookable-couch.repository"; 2 | -------------------------------------------------------------------------------- /src/modules/booking/src/domain/booking-cancellation/entity/booking-cancellation.ts: -------------------------------------------------------------------------------- 1 | import { Guid } from "guid-typescript"; 2 | import { AggregateId, AggregateRoot } from "@travelhoop/shared-kernel"; 3 | 4 | export interface BookingCancellationProps { 5 | id: AggregateId; 6 | guestId: Guid; 7 | quantity: number; 8 | dateFrom: Date; 9 | dateTo: Date; 10 | reason: string; 11 | } 12 | 13 | export class BookingCancellation extends AggregateRoot { 14 | private guestId: Guid; 15 | 16 | private quantity: number; 17 | 18 | private dateFrom: Date; 19 | 20 | private dateTo: Date; 21 | 22 | private reason: string; 23 | 24 | static create(props: BookingCancellationProps) { 25 | return new BookingCancellation(props); 26 | } 27 | 28 | constructor({ id, guestId, quantity, dateFrom, dateTo, reason }: BookingCancellationProps) { 29 | super(); 30 | this.id = id; 31 | this.guestId = guestId; 32 | this.quantity = quantity; 33 | this.dateFrom = dateFrom; 34 | this.dateTo = dateTo; 35 | this.reason = reason; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/modules/booking/src/domain/booking-cancellation/entity/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./booking-cancellation"; 2 | -------------------------------------------------------------------------------- /src/modules/booking/src/domain/booking-cancellation/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./entity"; 2 | 3 | export * from "./repository"; 4 | 5 | export * from "./subscribers"; 6 | -------------------------------------------------------------------------------- /src/modules/booking/src/domain/booking-cancellation/repository/booking-cancellation.repository.ts: -------------------------------------------------------------------------------- 1 | import { BookingCancellation } from "../entity/booking-cancellation"; 2 | 3 | export interface BookingCancellationRepository { 4 | add: (bookingCancellation: BookingCancellation) => Promise; 5 | } 6 | -------------------------------------------------------------------------------- /src/modules/booking/src/domain/booking-cancellation/repository/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./booking-cancellation.repository"; 2 | -------------------------------------------------------------------------------- /src/modules/booking/src/domain/booking-cancellation/subscribers/couch-booking-cancelled.subscriber.ts: -------------------------------------------------------------------------------- 1 | import { AggregateId, DomainEventSubscriber, DomainEventSubscribersMeta } from "@travelhoop/shared-kernel"; 2 | import { CouchBookingCancelled } from "../../bookable-couch"; 3 | import { BookingCancellation } from "../entity/booking-cancellation"; 4 | import { BookingCancellationRepository } from "../repository"; 5 | 6 | interface CouchBookingCancelledSubscriberDependencies { 7 | bookingCancellationRepository: BookingCancellationRepository; 8 | } 9 | 10 | export class CouchBookingCancelledSubscriber implements DomainEventSubscriber { 11 | constructor(private readonly deps: CouchBookingCancelledSubscriberDependencies) {} 12 | 13 | public getSubscribedEvents(): DomainEventSubscribersMeta[] { 14 | return [{ name: CouchBookingCancelled.name, method: "onCouchBookingCancelled" }]; 15 | } 16 | 17 | async onCouchBookingCancelled({ payload }: CouchBookingCancelled) { 18 | return this.deps.bookingCancellationRepository.add( 19 | BookingCancellation.create({ 20 | ...payload.couchBooking, 21 | reason: payload.reason, 22 | id: AggregateId.parse(payload.couchBooking.id.toString()), 23 | }), 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/modules/booking/src/domain/booking-cancellation/subscribers/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./couch-booking-cancelled.subscriber"; 2 | -------------------------------------------------------------------------------- /src/modules/booking/src/domain/couch-booking-request/entity/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./couch-booking-request"; 2 | 3 | export * from "./request-status"; 4 | -------------------------------------------------------------------------------- /src/modules/booking/src/domain/couch-booking-request/entity/request-status.ts: -------------------------------------------------------------------------------- 1 | export enum RequestStatus { 2 | Pending = "Pending", 3 | Accepted = "Accepted", 4 | Rejected = "Rejected", 5 | } 6 | -------------------------------------------------------------------------------- /src/modules/booking/src/domain/couch-booking-request/error/cannot-accept-booking.error.ts: -------------------------------------------------------------------------------- 1 | import { TravelhoopError } from "@travelhoop/abstract-core"; 2 | import StatusCodes from "http-status-codes"; 3 | import { RequestStatus } from ".."; 4 | 5 | export class CannotAcceptBookingError extends TravelhoopError { 6 | constructor(status: RequestStatus) { 7 | super(`Cannot accept this booking request because current state is ${status}`, StatusCodes.BAD_REQUEST); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/modules/booking/src/domain/couch-booking-request/error/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./cannot-accept-booking.error"; 2 | -------------------------------------------------------------------------------- /src/modules/booking/src/domain/couch-booking-request/event/couch-booking-request-created.event.ts: -------------------------------------------------------------------------------- 1 | import { AggregateId, DomainEvent } from "@travelhoop/shared-kernel"; 2 | 3 | interface CouchBookingRequestCreatedPayload { 4 | couchBookingRequestId: AggregateId; 5 | } 6 | 7 | export class CouchBookingRequestCreated implements DomainEvent { 8 | constructor(public payload: CouchBookingRequestCreatedPayload) {} 9 | } 10 | -------------------------------------------------------------------------------- /src/modules/booking/src/domain/couch-booking-request/event/couch-booking-status-changed.event.ts: -------------------------------------------------------------------------------- 1 | import { AggregateId, DomainEvent } from "@travelhoop/shared-kernel"; 2 | import { RequestStatus } from "../entity/request-status"; 3 | 4 | interface CouchBookingStatusChangedPayload { 5 | couchBookingRequestId: AggregateId; 6 | status: RequestStatus; 7 | decisionDate: Date; 8 | rejectionReason?: string; 9 | } 10 | 11 | export class CouchBookingStatusChanged implements DomainEvent { 12 | constructor(public payload: CouchBookingStatusChangedPayload) {} 13 | } 14 | -------------------------------------------------------------------------------- /src/modules/booking/src/domain/couch-booking-request/event/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./couch-booking-request-created.event"; 2 | 3 | export * from "./couch-booking-status-changed.event"; 4 | -------------------------------------------------------------------------------- /src/modules/booking/src/domain/couch-booking-request/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./entity"; 2 | 3 | export * from "./event"; 4 | 5 | export * from "./repository"; 6 | 7 | export * from "./service"; 8 | -------------------------------------------------------------------------------- /src/modules/booking/src/domain/couch-booking-request/repository/couch-booking-request.repository.ts: -------------------------------------------------------------------------------- 1 | import { AggregateId } from "@travelhoop/shared-kernel"; 2 | import { CouchBookingRequest } from "../entity/couch-booking-request"; 3 | 4 | export interface CouchBookingRequestRepository { 5 | get: (id: AggregateId) => Promise; 6 | add: (bookableCouch: CouchBookingRequest) => Promise; 7 | save: (bookableCouch: CouchBookingRequest) => Promise; 8 | } 9 | -------------------------------------------------------------------------------- /src/modules/booking/src/domain/couch-booking-request/repository/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./couch-booking-request.repository"; 2 | -------------------------------------------------------------------------------- /src/modules/booking/src/domain/couch-booking-request/service/couch-booking-request.domain-service.ts: -------------------------------------------------------------------------------- 1 | import { AggregateId } from "../../../../../../shared/kernel/build"; 2 | import { BookableCouchRepository } from "../../bookable-couch"; 3 | import { CouchBookingRequest, CreateCouchBookingRequest } from "../entity/couch-booking-request"; 4 | import { CouchBookingRequestRepository } from "../repository/couch-booking-request.repository"; 5 | 6 | interface CouchBookingRequestDomainServiceDependencies { 7 | bookableCouchRepository: BookableCouchRepository; 8 | couchBookingRequestRepository: CouchBookingRequestRepository; 9 | } 10 | 11 | export class CouchBookingRequestDomainService { 12 | constructor(private readonly deps: CouchBookingRequestDomainServiceDependencies) {} 13 | 14 | async createRequest(bookingRequestProps: Omit) { 15 | const bookableCouch = await this.deps.bookableCouchRepository.get(bookingRequestProps.bookableCouchId); 16 | 17 | bookableCouch.canBook(bookingRequestProps); 18 | 19 | const bookingRequest = CouchBookingRequest.create({ id: AggregateId.create(), ...bookingRequestProps }); 20 | 21 | await this.deps.couchBookingRequestRepository.add(bookingRequest); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/modules/booking/src/domain/couch-booking-request/service/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./couch-booking-request.domain-service"; 2 | -------------------------------------------------------------------------------- /src/modules/booking/src/domain/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./bookable-couch"; 2 | 3 | export * from "./booking-cancellation"; 4 | 5 | export * from "./couch-booking-request"; 6 | -------------------------------------------------------------------------------- /src/modules/booking/src/domain/tests/helpers/create-couch-booking-request.ts: -------------------------------------------------------------------------------- 1 | import { addDays } from "date-fns"; 2 | import { Guid } from "guid-typescript"; 3 | import { AggregateId } from "../../../../../../shared/kernel/build"; 4 | import { CouchBookingRequest } from "../../couch-booking-request"; 5 | 6 | interface CreateCouchBookingRequestOptions { 7 | dateFrom: Date; 8 | dateTo: Date; 9 | } 10 | 11 | export const createCouchBookingRequest = (options?: CreateCouchBookingRequestOptions) => { 12 | const bookingRequest = CouchBookingRequest.create({ 13 | id: AggregateId.create(), 14 | quantity: 2, 15 | dateFrom: options?.dateFrom || addDays(new Date(), 1), 16 | dateTo: options?.dateFrom || addDays(new Date(), 3), 17 | bookableCouchId: AggregateId.create(), 18 | guestId: Guid.create(), 19 | }); 20 | 21 | bookingRequest.clearEvents(); 22 | 23 | return bookingRequest; 24 | }; 25 | -------------------------------------------------------------------------------- /src/modules/booking/src/index.ts: -------------------------------------------------------------------------------- 1 | import { bookingModule } from "./api/booking.module"; 2 | 3 | export default bookingModule; 4 | -------------------------------------------------------------------------------- /src/modules/booking/src/infrastructure/config.ts: -------------------------------------------------------------------------------- 1 | export interface EnvVariables extends NodeJS.Process { 2 | JWT_SECRET_KEY: string; 3 | ASYNC_MESSAGE_BROKER_QUEUE: string; 4 | SCHEDULER_SECURITY_TOKEN: string; 5 | } 6 | 7 | export const bookingModuleConfigFactory = (env: EnvVariables) => ({ 8 | jwt: { 9 | secretKey: env.JWT_SECRET_KEY, 10 | }, 11 | securityTokens: { 12 | schedulerToken: env.SCHEDULER_SECURITY_TOKEN, 13 | }, 14 | queues: { 15 | messageBroker: env.ASYNC_MESSAGE_BROKER_QUEUE, 16 | }, 17 | }); 18 | 19 | export type BookingModuleConfig = ReturnType; 20 | -------------------------------------------------------------------------------- /src/modules/booking/src/infrastructure/mikro-orm/entity-schemas/bookable-couch.entity.ts: -------------------------------------------------------------------------------- 1 | import { GuidType } from "@travelhoop/infrastructure"; 2 | import { EntitySchema } from "@mikro-orm/core"; 3 | import { AggregateRoot } from "@travelhoop/shared-kernel"; 4 | import { BookableCouchProps, BookableCouch, Booking, BookableCouchState } from "../../../domain"; 5 | 6 | export const bookableCouchEntitySchema = new EntitySchema({ 7 | name: "BookableCouch", 8 | class: BookableCouch as any, 9 | extends: "AggregateRoot", 10 | properties: { 11 | quantity: { type: "number" }, 12 | hostId: { type: GuidType }, 13 | bookings: { reference: "1:m", entity: () => Booking, mappedBy: booking => booking.bookableCouch }, 14 | state: { enum: true, items: () => BookableCouchState }, 15 | }, 16 | }); 17 | -------------------------------------------------------------------------------- /src/modules/booking/src/infrastructure/mikro-orm/entity-schemas/booking-cancellation.entity.ts: -------------------------------------------------------------------------------- 1 | import { GuidType } from "@travelhoop/infrastructure"; 2 | import { EntitySchema } from "@mikro-orm/core"; 3 | import { AggregateRoot } from "@travelhoop/shared-kernel"; 4 | import { BookingCancellation, BookingCancellationProps } from "../../../domain"; 5 | 6 | export const bookingCancellationEntitySchema = new EntitySchema({ 7 | name: "BookingCancellation", 8 | class: BookingCancellation as any, 9 | extends: "AggregateRoot", 10 | properties: { 11 | guestId: { type: GuidType }, 12 | quantity: { type: "number" }, 13 | dateFrom: { type: Date }, 14 | dateTo: { type: Date }, 15 | reason: { type: "string" }, 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /src/modules/booking/src/infrastructure/mikro-orm/entity-schemas/booking.ts: -------------------------------------------------------------------------------- 1 | import { GuidType } from "@travelhoop/infrastructure"; 2 | import { EntitySchema } from "@mikro-orm/core"; 3 | import { BookingProps, BookableCouch } from "../../../domain"; 4 | 5 | export const bookingEntitySchema = new EntitySchema({ 6 | name: "Booking", 7 | discriminatorColumn: "discr", 8 | discriminatorMap: { 9 | couchBooking: "CouchBooking", 10 | unavailableBooking: "UnavailableBooking", 11 | }, 12 | properties: { 13 | id: { type: GuidType, primary: true }, 14 | dateFrom: { type: Date }, 15 | dateTo: { type: Date }, 16 | bookableCouch: { 17 | reference: "m:1", 18 | entity: () => BookableCouch, 19 | }, 20 | }, 21 | }); 22 | -------------------------------------------------------------------------------- /src/modules/booking/src/infrastructure/mikro-orm/entity-schemas/couch-booking-request.entity.ts: -------------------------------------------------------------------------------- 1 | import { GuidType } from "@travelhoop/infrastructure"; 2 | import { EntitySchema } from "@mikro-orm/core"; 3 | import { AggregateRoot } from "@travelhoop/shared-kernel"; 4 | import { AggregateIdType } from "@travelhoop/infrastructure"; 5 | import { CouchBookingRequestProps, CouchBookingRequest, RequestStatus } from "../../../domain"; 6 | 7 | export const couchBookingRequestEntitySchema = new EntitySchema({ 8 | name: "CouchBookingRequest", 9 | class: CouchBookingRequest as any, 10 | extends: "AggregateRoot", 11 | properties: { 12 | bookableCouchId: { type: AggregateIdType }, 13 | guestId: { type: GuidType }, 14 | quantity: { type: "number" }, 15 | dateFrom: { type: Date }, 16 | dateTo: { type: Date }, 17 | decisionDate: { type: Date, nullable: true }, 18 | status: { enum: true, items: () => RequestStatus }, 19 | rejectionReason: { type: "string", nullable: true }, 20 | }, 21 | }); 22 | -------------------------------------------------------------------------------- /src/modules/booking/src/infrastructure/mikro-orm/entity-schemas/couch-booking.ts: -------------------------------------------------------------------------------- 1 | import { EntitySchema } from "@mikro-orm/core"; 2 | import { GuidType } from "@travelhoop/infrastructure"; 3 | import { CouchBooking } from "../../../domain/bookable-couch/entity/couch-booking"; 4 | import { BookingProps, CouchBookingProps } from "../../../domain"; 5 | 6 | export const couchBookingEntitySchema = new EntitySchema({ 7 | name: "CouchBooking", 8 | class: CouchBooking as any, 9 | extends: "Booking", 10 | properties: { 11 | guestId: { type: GuidType }, 12 | quantity: { type: "number" }, 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /src/modules/booking/src/infrastructure/mikro-orm/entity-schemas/unavailable-booking.ts: -------------------------------------------------------------------------------- 1 | import { EntitySchema } from "@mikro-orm/core"; 2 | import { BookingProps } from "../../../domain"; 3 | import { UnavailableBookingProps, UnavailableBooking } from "../../../domain"; 4 | 5 | export const unavailableBookingEntitySchema = new EntitySchema({ 6 | name: "UnavailableBooking", 7 | class: UnavailableBooking as any, 8 | extends: "Booking", 9 | properties: {}, 10 | }); 11 | -------------------------------------------------------------------------------- /src/modules/booking/src/infrastructure/mikro-orm/repositories/bookable-couch.repository.ts: -------------------------------------------------------------------------------- 1 | import { AggregateId } from "@travelhoop/shared-kernel"; 2 | import { MikroOrmRepository, MikroOrmRepositoryDependencies } from "@travelhoop/infrastructure"; 3 | import { BookableCouch, BookableCouchProps, BookableCouchRepository } from "../../../domain"; 4 | import { bookableCouchEntitySchema } from "../entity-schemas/bookable-couch.entity"; 5 | 6 | export class MikroOrmBookableCouchRepository 7 | extends MikroOrmRepository 8 | implements BookableCouchRepository 9 | { 10 | constructor({ dbConnection }: MikroOrmRepositoryDependencies) { 11 | super({ dbConnection, entitySchema: bookableCouchEntitySchema }); 12 | } 13 | 14 | findWithFinishedBookings() { 15 | return this.repo.find({ bookings: { $ne: null } }, { populate: ["bookings"] }) as unknown as Promise< 16 | BookableCouch[] 17 | >; 18 | } 19 | 20 | async find(id: AggregateId) { 21 | return this.repo.findOne({ id }) as unknown as Promise; 22 | } 23 | 24 | async get(id: AggregateId) { 25 | return this.repo.findOneOrFail({ id }, ["bookings"]) as unknown as BookableCouch; 26 | } 27 | 28 | async add(bookableCouch: BookableCouch) { 29 | await this.repo.persistAndFlush(bookableCouch); 30 | } 31 | 32 | async save(bookableCouch: BookableCouch) { 33 | await this.repo.persistAndFlush(bookableCouch); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/modules/booking/src/infrastructure/mikro-orm/repositories/booking-cancellation.repository.ts: -------------------------------------------------------------------------------- 1 | import { AggregateId } from "@travelhoop/shared-kernel"; 2 | import { MikroOrmRepository, MikroOrmRepositoryDependencies } from "@travelhoop/infrastructure"; 3 | import { BookingCancellation, BookingCancellationProps, BookingCancellationRepository } from "../../../domain"; 4 | import { bookingCancellationEntitySchema } from "../entity-schemas/booking-cancellation.entity"; 5 | 6 | export class MikroOrmBookingCancellationRepository 7 | extends MikroOrmRepository 8 | implements BookingCancellationRepository { 9 | constructor({ dbConnection }: MikroOrmRepositoryDependencies) { 10 | super({ dbConnection, entitySchema: bookingCancellationEntitySchema }); 11 | } 12 | 13 | async get(id: AggregateId) { 14 | return (this.repo.findOneOrFail({ id }) as unknown) as BookingCancellation; 15 | } 16 | 17 | async add(bookingCancellation: BookingCancellation) { 18 | await this.repo.persistAndFlush(bookingCancellation); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/modules/booking/src/infrastructure/mikro-orm/repositories/couch-booking-request.repository.ts: -------------------------------------------------------------------------------- 1 | import { MikroOrmRepository, MikroOrmRepositoryDependencies } from "@travelhoop/infrastructure"; 2 | import { AggregateId } from "@travelhoop/shared-kernel"; 3 | import { CouchBookingRequestRepository, CouchBookingRequest, CouchBookingRequestProps } from "../../../domain"; 4 | import { couchBookingRequestEntitySchema } from "../entity-schemas/couch-booking-request.entity"; 5 | 6 | export class MikroOrmCouchBookingRequestRepository 7 | extends MikroOrmRepository 8 | implements CouchBookingRequestRepository { 9 | constructor({ dbConnection }: MikroOrmRepositoryDependencies) { 10 | super({ dbConnection, entitySchema: couchBookingRequestEntitySchema }); 11 | } 12 | 13 | async get(id: AggregateId) { 14 | return (this.repo.findOneOrFail({ id }) as unknown) as CouchBookingRequest; 15 | } 16 | 17 | async add(couchBookingRequest: CouchBookingRequest) { 18 | await this.repo.persistAndFlush(couchBookingRequest); 19 | } 20 | 21 | async save(couchBookingRequest: CouchBookingRequest) { 22 | await this.repo.persistAndFlush(couchBookingRequest); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/modules/booking/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/@travelhoop/toolchain/includes/tsconfig.web.json" 3 | } -------------------------------------------------------------------------------- /src/modules/couch/.eslintignore: -------------------------------------------------------------------------------- 1 | **/*.d.ts 2 | *.json -------------------------------------------------------------------------------- /src/modules/couch/.eslintrc.js: -------------------------------------------------------------------------------- 1 | require('@travelhoop/toolchain/patch/modern-module-resolution'); 2 | 3 | module.exports = { 4 | extends: [ "./node_modules/@travelhoop/toolchain/includes/.eslintrc" ], // <---- put your profile string here 5 | parserOptions: { 6 | project: "tsconfig.json", 7 | tsconfigRootDir: __dirname 8 | } 9 | }; -------------------------------------------------------------------------------- /src/modules/couch/.swcrc: -------------------------------------------------------------------------------- 1 | { 2 | "jsc": { 3 | "parser": { 4 | "syntax": "typescript", 5 | "decorators": true, 6 | "dynamicImport": true 7 | }, 8 | "transform": { 9 | "legacyDecorator": true, 10 | "decoratorMetadata": true 11 | }, 12 | "target": "es2018", 13 | "keepClassNames": true, 14 | "loose": true 15 | }, 16 | "module": { 17 | "type": "commonjs", 18 | "strict": true, 19 | "strictMode": true, 20 | "lazy": false, 21 | "noInterop": false 22 | }, 23 | "sourceMaps": "inline" 24 | } -------------------------------------------------------------------------------- /src/modules/couch/couch.rest: -------------------------------------------------------------------------------- 1 | @url = http://localhost:3010/couch 2 | 3 | @couchId=dd8ec02f-c65f-42b8-408a-034a6948448f 4 | 5 | @accessToken=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6eyJ2YWx1ZSI6Ijk1MTVlNWQ2LTgxZmMtMzAwYS0zYzJkLWQzZjNhYTQ5ODkxMSJ9LCJleHAiOjE2MjAwNzIyODUsImlhdCI6MTYyMDA3MDQ4NX0.VZ2tLEvc-V9xCQBxYlavlbdd6kjMtEjDVwsCIPWm1TA 6 | ### 7 | POST {{url}} HTTP/1.1 8 | content-type: application/json 9 | authorization: Bearer {{accessToken}} 10 | 11 | { 12 | "name":"Example couch", 13 | "description": "Lorem ipsum", 14 | "quantity": 2 15 | } 16 | 17 | ### 18 | POST {{url}}/{{couchId}} HTTP/1.1 19 | content-type: application/json 20 | authorization: Bearer {{accessToken}} 21 | 22 | { 23 | "name":"Example couch 22", 24 | "description": "Lorem ipsum", 25 | "quantity": 2 26 | } 27 | 28 | ### 29 | GET {{url}}/{{couchId}} HTTP/1.1 30 | content-type: application/json 31 | authorization: Bearer {{accessToken}} 32 | 33 | ### 34 | GET {{url}}/ HTTP/1.1 35 | content-type: application/json 36 | authorization: Bearer {{accessToken}} 37 | 38 | -------------------------------------------------------------------------------- /src/modules/couch/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@travelhoop/couch-module", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main":"./build/index.js", 6 | "typings":"./build/index.d.ts", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "build": "rimraf build && tsc", 10 | "build:watch":"swc src -d build", 11 | "lint": "eslint './**/*.ts'", 12 | "lint:fix": "eslint './**/*.ts' --fix" 13 | }, 14 | "author": "", 15 | "license": "ISC", 16 | "dependencies": { 17 | "@travelhoop/abstract-core": "1.0.0", 18 | "@travelhoop/infrastructure": "1.0.0", 19 | "express": "~4.17.1", 20 | "rimraf": "~3.0.2", 21 | "guid-typescript": "~1.0.9", 22 | "class-validator": "~0.13.1", 23 | "awilix": "~4.3.3", 24 | "awilix-express": "~4.0.0", 25 | "express-async-handler": "~1.1.4", 26 | "@mikro-orm/core": "~4.5.5", 27 | "@mikro-orm/postgresql": "~4.5.5", 28 | "date-fns": "~2.20.1", 29 | "http-status-codes": "~2.1.4" 30 | }, 31 | "devDependencies": { 32 | "eslint": "~7.23.0", 33 | "typescript": "~4.2.3", 34 | "@travelhoop/toolchain":"1.0.0", 35 | "@types/express": "~4.17.11", 36 | "@types/node": "~14.14.37", 37 | "@swc/core": "~1.2.51", 38 | "@swc/cli": "~0.1.36" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/modules/couch/src/api/container.ts: -------------------------------------------------------------------------------- 1 | import { ContainerBuilder, StandardCreateContainerDependencies } from "@travelhoop/infrastructure"; 2 | import { asClass } from "awilix"; 3 | import { couchModuleConfigFactory } from "../core/config"; 4 | import { CouchRepository } from "../core/repositories/couch.repository"; 5 | import { CouchService } from "../core/services/couch.service"; 6 | import { createRouter } from "./routes/router"; 7 | 8 | export const createContainer = ({ dbConnection, redis }: StandardCreateContainerDependencies) => { 9 | const config = couchModuleConfigFactory(process.env as any); 10 | return new ContainerBuilder() 11 | .addCommon() 12 | .addAuth({ secretKey: config.jwt.secretKey }) 13 | .addRedis(redis) 14 | .addRouting(createRouter) 15 | .addDbConnection(dbConnection) 16 | .addEventSubscribers({ 17 | messageBrokerQueueName: config.queues.messageBroker, 18 | eventSubscribers: [], 19 | }) 20 | .register({ 21 | couchService: asClass(CouchService), 22 | couchRepository: asClass(CouchRepository), 23 | }) 24 | .build(); 25 | }; 26 | -------------------------------------------------------------------------------- /src/modules/couch/src/api/couch.module.ts: -------------------------------------------------------------------------------- 1 | import { standardAppModuleFactory } from "@travelhoop/infrastructure"; 2 | import { createContainer } from "./container"; 3 | 4 | export const couchModule = standardAppModuleFactory({ 5 | basePath: "couch", 6 | name: "couch-module", 7 | createContainer, 8 | }); 9 | -------------------------------------------------------------------------------- /src/modules/couch/src/api/routes/couch.router.ts: -------------------------------------------------------------------------------- 1 | import { Guid } from "guid-typescript"; 2 | import { makeInvoker } from "awilix-express"; 3 | import { validateOrReject } from "class-validator"; 4 | import { Request, Response } from "@travelhoop/infrastructure"; 5 | import asyncHandler from "express-async-handler"; 6 | import { UpdateCouchDto } from "../../core/dto/update-couch.dto"; 7 | import { CreateCouchDto } from "../../core/dto/create-couch.dto"; 8 | import { CouchService } from "../../core/services/couch.service"; 9 | 10 | interface CouchApiDependencies { 11 | couchService: CouchService; 12 | } 13 | 14 | const api = ({ couchService }: CouchApiDependencies) => ({ 15 | create: asyncHandler(async (req: Request, res: Response) => { 16 | const dto = new CreateCouchDto({ ...req.body, hostId: req.user?.id! }); 17 | await validateOrReject(dto); 18 | res.json(await couchService.create(dto)); 19 | }), 20 | update: asyncHandler(async (req: Request, res: Response) => { 21 | const dto = new UpdateCouchDto({ id: req.params.id, hostId: req.user?.id!, ...req.body }); 22 | await validateOrReject(dto); 23 | res.json(await couchService.update(dto)); 24 | }), 25 | getByUserId: asyncHandler(async (req: Request, res: Response) => { 26 | res.json(await couchService.getByUserId(req.user?.id!)); 27 | }), 28 | getById: asyncHandler(async (req: Request, res: Response) => { 29 | res.json(await couchService.getById(Guid.parse(req.params.couchId))); 30 | }), 31 | }); 32 | 33 | export const couchApi = makeInvoker(api); 34 | -------------------------------------------------------------------------------- /src/modules/couch/src/api/routes/router.ts: -------------------------------------------------------------------------------- 1 | import { MiddlewareType } from "@travelhoop/infrastructure"; 2 | import express, { Router } from "express"; 3 | import { couchApi } from "./couch.router"; 4 | 5 | export const createRouter = ({ auth }: { auth: MiddlewareType }): Router => { 6 | const router = express.Router(); 7 | 8 | router.post("/:id", auth, couchApi("update")); 9 | router.get("/:couchId", auth, couchApi("getById")); 10 | router.get("/", auth, couchApi("getByUserId")); 11 | router.post("/", auth, couchApi("create")); 12 | 13 | return router; 14 | }; 15 | -------------------------------------------------------------------------------- /src/modules/couch/src/core/config.ts: -------------------------------------------------------------------------------- 1 | export interface EnvVariables extends NodeJS.Process { 2 | JWT_SECRET_KEY: string; 3 | ASYNC_MESSAGE_BROKER_QUEUE: string; 4 | } 5 | 6 | export const couchModuleConfigFactory = (env: EnvVariables) => ({ 7 | jwt: { 8 | secretKey: env.JWT_SECRET_KEY, 9 | }, 10 | queues: { 11 | messageBroker: env.ASYNC_MESSAGE_BROKER_QUEUE, 12 | }, 13 | }); 14 | 15 | export type CouchModuleConfig = ReturnType; 16 | -------------------------------------------------------------------------------- /src/modules/couch/src/core/dto/couch.dto.ts: -------------------------------------------------------------------------------- 1 | import { Guid } from "guid-typescript"; 2 | 3 | export class CouchDto { 4 | id: Guid; 5 | 6 | hostId: Guid; 7 | 8 | name: string; 9 | 10 | description: string; 11 | 12 | quantity: number; 13 | 14 | createdAt: Date; 15 | 16 | constructor(props: CouchDto) { 17 | Object.assign(this, props); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/modules/couch/src/core/dto/create-couch.dto.ts: -------------------------------------------------------------------------------- 1 | import { Guid } from "guid-typescript"; 2 | import { IsNumber, IsString, Min, IsDefined } from "class-validator"; 3 | 4 | export class CreateCouchDto { 5 | @IsDefined() 6 | hostId: Guid; 7 | 8 | @IsString() 9 | name: string; 10 | 11 | @IsString() 12 | description: string; 13 | 14 | @IsNumber() 15 | @Min(1) 16 | quantity: number; 17 | 18 | constructor(props: CreateCouchDto) { 19 | Object.assign(this, props); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/modules/couch/src/core/dto/update-couch.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsString, IsNumber, Min, IsDefined } from "class-validator"; 2 | 3 | export class UpdateCouchDto { 4 | @IsDefined() 5 | id: string; 6 | 7 | @IsDefined() 8 | hostId: string; 9 | 10 | @IsString() 11 | name: string; 12 | 13 | @IsString() 14 | description: string; 15 | 16 | @IsNumber() 17 | @Min(1) 18 | quantity: number; 19 | 20 | constructor(props: UpdateCouchDto) { 21 | Object.assign(this, props); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/modules/couch/src/core/entities/couch.ts: -------------------------------------------------------------------------------- 1 | import { Entity, PrimaryKey, Property } from "@mikro-orm/core"; 2 | import { GuidType } from "@travelhoop/infrastructure"; 3 | import { Guid } from "guid-typescript"; 4 | 5 | @Entity() 6 | export class Couch { 7 | @PrimaryKey({ type: GuidType }) 8 | id: Guid; 9 | 10 | @Property({ type: GuidType }) 11 | hostId: Guid; 12 | 13 | @Property() 14 | name: string; 15 | 16 | @Property() 17 | description: string; 18 | 19 | @Property() 20 | quantity: number; 21 | 22 | @Property() 23 | createdAt: Date = new Date(); 24 | 25 | static create(data: Partial) { 26 | const model = new Couch(); 27 | 28 | Object.assign(model, data); 29 | 30 | return model; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/modules/couch/src/core/error/couch-not-found.error.ts: -------------------------------------------------------------------------------- 1 | import { TravelhoopError } from "@travelhoop/abstract-core"; 2 | import StatusCodes from "http-status-codes"; 3 | 4 | export class CouchNotFoundError extends TravelhoopError { 5 | constructor() { 6 | super("Couch not found", StatusCodes.NOT_FOUND); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/modules/couch/src/core/events/couch-created.event.ts: -------------------------------------------------------------------------------- 1 | import { Event } from "@travelhoop/abstract-core"; 2 | 3 | interface CouchCreatedPayload { 4 | id: string; 5 | hostId: string; 6 | quantity: number; 7 | } 8 | 9 | export class CouchCreated implements Event { 10 | name = this.constructor.name; 11 | 12 | constructor(public payload: CouchCreatedPayload) {} 13 | } 14 | -------------------------------------------------------------------------------- /src/modules/couch/src/core/repositories/couch.repository.ts: -------------------------------------------------------------------------------- 1 | import { Guid } from "guid-typescript"; 2 | import { DbConnection } from "@travelhoop/infrastructure"; 3 | import { Couch } from "../entities/couch"; 4 | import { CouchNotFoundError } from "../error/couch-not-found.error"; 5 | 6 | interface CouchRepositoryDependencies { 7 | dbConnection: DbConnection; 8 | } 9 | export class CouchRepository { 10 | constructor(private readonly deps: CouchRepositoryDependencies) {} 11 | 12 | async add(couch: Couch): Promise { 13 | await this.deps.dbConnection.em.persistAndFlush(couch); 14 | } 15 | 16 | async update(couch: Couch): Promise { 17 | await this.deps.dbConnection.em.persistAndFlush(couch); 18 | } 19 | 20 | async get(id: Guid) { 21 | const couch = await this.deps.dbConnection.em.getRepository(Couch).findOne({ id }); 22 | 23 | if (!couch) { 24 | throw new CouchNotFoundError(); 25 | } 26 | 27 | return couch; 28 | } 29 | 30 | async getByHostId(hostId: Guid) { 31 | return this.deps.dbConnection.em.getRepository(Couch).find({ hostId }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/modules/couch/src/core/services/couch.service.ts: -------------------------------------------------------------------------------- 1 | import { MessageBroker } from "@travelhoop/infrastructure"; 2 | import { Guid } from "guid-typescript"; 3 | import { Couch } from "../entities/couch"; 4 | import { CreateCouchDto } from "../dto/create-couch.dto"; 5 | import { CouchRepository } from "../repositories/couch.repository"; 6 | import { UpdateCouchDto } from "../dto/update-couch.dto"; 7 | import { CouchDto } from "../dto/couch.dto"; 8 | import { CouchCreated } from "../events/couch-created.event"; 9 | 10 | interface CouchServiceDependencies { 11 | couchRepository: CouchRepository; 12 | messageBroker: MessageBroker; 13 | } 14 | 15 | export class CouchService { 16 | constructor(private readonly deps: CouchServiceDependencies) {} 17 | 18 | async create(dto: CreateCouchDto) { 19 | const id = Guid.create(); 20 | const couch = Couch.create({ id, ...dto }); 21 | await this.deps.couchRepository.add(couch); 22 | 23 | await this.deps.messageBroker.publish( 24 | new CouchCreated({ id: id.toString(), hostId: couch.hostId.toString(), quantity: couch.quantity }), 25 | ); 26 | } 27 | 28 | async update(dto: UpdateCouchDto) { 29 | const couch = await this.deps.couchRepository.get(Guid.parse(dto.id)); 30 | 31 | couch.description = dto.description; 32 | couch.name = dto.name; 33 | couch.quantity = dto.quantity; 34 | } 35 | 36 | async getByUserId(userId: Guid) { 37 | const couches = await this.deps.couchRepository.getByHostId(userId); 38 | return couches.map(couch => new CouchDto(couch)); 39 | } 40 | 41 | async getById(couchId: Guid) { 42 | const couch = await this.deps.couchRepository.get(couchId); 43 | return new CouchDto(couch); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/modules/couch/src/index.ts: -------------------------------------------------------------------------------- 1 | import { couchModule } from "./api/couch.module"; 2 | 3 | export default couchModule; 4 | -------------------------------------------------------------------------------- /src/modules/couch/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/@travelhoop/toolchain/includes/tsconfig.web.json" 3 | } -------------------------------------------------------------------------------- /src/modules/review/.eslintignore: -------------------------------------------------------------------------------- 1 | **/*.d.ts 2 | *.json -------------------------------------------------------------------------------- /src/modules/review/.eslintrc.js: -------------------------------------------------------------------------------- 1 | require('@travelhoop/toolchain/patch/modern-module-resolution'); 2 | 3 | module.exports = { 4 | extends: [ "./node_modules/@travelhoop/toolchain/includes/.eslintrc" ], // <---- put your profile string here 5 | parserOptions: { 6 | project: "tsconfig.json", 7 | tsconfigRootDir: __dirname 8 | } 9 | }; -------------------------------------------------------------------------------- /src/modules/review/.swcrc: -------------------------------------------------------------------------------- 1 | { 2 | "jsc": { 3 | "parser": { 4 | "syntax": "typescript", 5 | "decorators": true, 6 | "dynamicImport": true 7 | }, 8 | "transform": { 9 | "legacyDecorator": true, 10 | "decoratorMetadata": true 11 | }, 12 | "target": "es2018", 13 | "keepClassNames": true, 14 | "loose": true 15 | }, 16 | "module": { 17 | "type": "commonjs", 18 | "strict": true, 19 | "strictMode": true, 20 | "lazy": false, 21 | "noInterop": false 22 | }, 23 | "sourceMaps": "inline" 24 | } -------------------------------------------------------------------------------- /src/modules/review/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@travelhoop/review-module", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main":"./build/index.js", 6 | "typings":"./build/index.d.ts", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "build": "rimraf build && tsc", 10 | "build:watch":"swc src -d build", 11 | "lint": "eslint './**/*.ts'", 12 | "lint:fix": "eslint './**/*.ts' --fix" 13 | }, 14 | "author": "", 15 | "license": "ISC", 16 | "dependencies": { 17 | "@travelhoop/abstract-core": "1.0.0", 18 | "@travelhoop/infrastructure": "1.0.0", 19 | "express": "~4.17.1", 20 | "rimraf": "~3.0.2", 21 | "guid-typescript": "~1.0.9", 22 | "class-validator": "~0.13.1", 23 | "awilix": "~4.3.3", 24 | "awilix-express": "~4.0.0", 25 | "express-async-handler": "~1.1.4", 26 | "@mikro-orm/core": "~4.5.5", 27 | "@mikro-orm/postgresql": "~4.5.5", 28 | "date-fns": "~2.20.1", 29 | "http-status-codes": "~2.1.4" 30 | }, 31 | "devDependencies": { 32 | "eslint": "~7.23.0", 33 | "typescript": "~4.2.3", 34 | "@travelhoop/toolchain":"1.0.0", 35 | "@types/express": "~4.17.11", 36 | "@types/node": "~14.14.37", 37 | "@swc/core": "~1.2.51", 38 | "@swc/cli": "~0.1.36" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/modules/review/review.rest: -------------------------------------------------------------------------------- 1 | @url = http://localhost:3010/couch 2 | 3 | @couchId=dd8ec02f-c65f-42b8-408a-034a6948448f 4 | 5 | @accessToken=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6eyJ2YWx1ZSI6Ijk1MTVlNWQ2LTgxZmMtMzAwYS0zYzJkLWQzZjNhYTQ5ODkxMSJ9LCJleHAiOjE2MjAwNzIyODUsImlhdCI6MTYyMDA3MDQ4NX0.VZ2tLEvc-V9xCQBxYlavlbdd6kjMtEjDVwsCIPWm1TA 6 | ### 7 | POST {{url}} HTTP/1.1 8 | content-type: application/json 9 | authorization: Bearer {{accessToken}} 10 | 11 | { 12 | "name":"Example couch", 13 | "description": "Lorem ipsum", 14 | "quantity": 2 15 | } 16 | 17 | ### 18 | POST {{url}}/{{couchId}} HTTP/1.1 19 | content-type: application/json 20 | authorization: Bearer {{accessToken}} 21 | 22 | { 23 | "name":"Example couch 22", 24 | "description": "Lorem ipsum", 25 | "quantity": 2 26 | } 27 | 28 | ### 29 | GET {{url}}/{{couchId}} HTTP/1.1 30 | content-type: application/json 31 | authorization: Bearer {{accessToken}} 32 | 33 | ### 34 | GET {{url}}/ HTTP/1.1 35 | content-type: application/json 36 | authorization: Bearer {{accessToken}} 37 | 38 | -------------------------------------------------------------------------------- /src/modules/review/src/api/container.ts: -------------------------------------------------------------------------------- 1 | import { ContainerBuilder, StandardCreateContainerDependencies } from "@travelhoop/infrastructure"; 2 | import { asClass } from "awilix"; 3 | import { reviewModuleConfigFactory } from "../core/config"; 4 | import { BookingReviewRepository } from "../core/repositories/booking-review.repository"; 5 | import { BookingReviewService } from "../core/services/booking-review.service"; 6 | import { BookingFinishedSubscriber } from "../core/subscribers/booking-finished.subscriber"; 7 | import { createRouter } from "./routes/router"; 8 | 9 | export const createContainer = ({ dbConnection, redis }: StandardCreateContainerDependencies) => { 10 | const config = reviewModuleConfigFactory(process.env as any); 11 | return new ContainerBuilder() 12 | .addCommon() 13 | .addAuth({ secretKey: config.jwt.secretKey }) 14 | .addRedis(redis) 15 | .addRouting(createRouter) 16 | .addDbConnection(dbConnection) 17 | .addEventSubscribers({ 18 | messageBrokerQueueName: config.queues.messageBroker, 19 | eventSubscribers: [BookingFinishedSubscriber], 20 | }) 21 | .register({ 22 | bookingReviewService: asClass(BookingReviewService), 23 | bookingReviewRepository: asClass(BookingReviewRepository), 24 | }) 25 | .build(); 26 | }; 27 | -------------------------------------------------------------------------------- /src/modules/review/src/api/review.module.ts: -------------------------------------------------------------------------------- 1 | import { standardAppModuleFactory } from "@travelhoop/infrastructure"; 2 | import { createContainer } from "./container"; 3 | 4 | export const reviewModule = standardAppModuleFactory({ 5 | basePath: "review", 6 | name: "review-module", 7 | createContainer, 8 | }); 9 | -------------------------------------------------------------------------------- /src/modules/review/src/api/routes/booking-review.router.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "@travelhoop/infrastructure"; 2 | import { makeInvoker } from "awilix-express"; 3 | import { validateOrReject } from "class-validator"; 4 | import asyncHandler from "express-async-handler"; 5 | import { Guid } from "guid-typescript"; 6 | import { UpdateBookingReviewDto } from "../../core/dto/update-booking-review.dto"; 7 | import { BookingReviewService } from "../../core/services/booking-review.service"; 8 | 9 | interface CouchApiDependencies { 10 | bookingReviewService: BookingReviewService; 11 | } 12 | 13 | const api = ({ bookingReviewService }: CouchApiDependencies) => ({ 14 | update: asyncHandler(async (req: Request, res: Response) => { 15 | const dto = new UpdateBookingReviewDto({ id: req.params.id, reviewerId: req.user?.id!, ...req.body }); 16 | await validateOrReject(dto); 17 | res.json(await bookingReviewService.update(dto)); 18 | }), 19 | getById: asyncHandler(async (req: Request, res: Response) => { 20 | res.json(await bookingReviewService.getById(Guid.parse(req.params.bookingReviewId))); 21 | }), 22 | }); 23 | 24 | export const bookingReviewApi = makeInvoker(api); 25 | -------------------------------------------------------------------------------- /src/modules/review/src/api/routes/router.ts: -------------------------------------------------------------------------------- 1 | import { MiddlewareType } from "@travelhoop/infrastructure"; 2 | import express, { Router } from "express"; 3 | import { bookingReviewApi } from "./booking-review.router"; 4 | 5 | export const createRouter = ({ auth }: { auth: MiddlewareType }): Router => { 6 | const router = express.Router(); 7 | 8 | router.post("/booking-review/:id", auth, bookingReviewApi("update")); 9 | router.get("/booking-review/:bookingReviewId", auth, bookingReviewApi("getById")); 10 | 11 | return router; 12 | }; 13 | -------------------------------------------------------------------------------- /src/modules/review/src/core/config.ts: -------------------------------------------------------------------------------- 1 | export interface EnvVariables extends NodeJS.Process { 2 | JWT_SECRET_KEY: string; 3 | ASYNC_MESSAGE_BROKER_QUEUE: string; 4 | } 5 | 6 | export const reviewModuleConfigFactory = (env: EnvVariables) => ({ 7 | jwt: { 8 | secretKey: env.JWT_SECRET_KEY, 9 | }, 10 | queues: { 11 | messageBroker: env.ASYNC_MESSAGE_BROKER_QUEUE, 12 | }, 13 | }); 14 | 15 | export type ReviewModuleConfig = ReturnType; 16 | -------------------------------------------------------------------------------- /src/modules/review/src/core/dto/booking-review.dto.ts: -------------------------------------------------------------------------------- 1 | import { BookingReview } from "../entities/booking-review"; 2 | 3 | export class BookingReviewDto { 4 | id: string; 5 | 6 | reviewerId: string; 7 | 8 | revieweeId: string; 9 | 10 | comment?: string; 11 | 12 | rate?: number; 13 | 14 | fullfiledAt?: Date; 15 | 16 | constructor(props: BookingReviewDto) { 17 | Object.assign(this, props); 18 | } 19 | 20 | static createFromBookingReview(bookingReview: BookingReview) { 21 | return new BookingReviewDto({ 22 | id: bookingReview.id.toString(), 23 | reviewerId: bookingReview.reviewerId.toString(), 24 | revieweeId: bookingReview.revieweeId.toString(), 25 | comment: bookingReview.reviewDetails?.comment, 26 | rate: bookingReview.reviewDetails?.rate, 27 | fullfiledAt: bookingReview.reviewDetails?.fullfiledAt, 28 | }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/modules/review/src/core/dto/create-couch.dto.ts: -------------------------------------------------------------------------------- 1 | import { Guid } from "guid-typescript"; 2 | import { IsNumber, IsString, Min, IsDefined } from "class-validator"; 3 | 4 | export class CreateCouchDto { 5 | @IsDefined() 6 | hostId: Guid; 7 | 8 | @IsString() 9 | name: string; 10 | 11 | @IsString() 12 | description: string; 13 | 14 | @IsNumber() 15 | @Min(1) 16 | quantity: number; 17 | 18 | constructor(props: CreateCouchDto) { 19 | Object.assign(this, props); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/modules/review/src/core/dto/update-booking-review.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsString, IsNumber, Min, IsDefined, Max } from "class-validator"; 2 | 3 | export class UpdateBookingReviewDto { 4 | @IsDefined() 5 | id: string; 6 | 7 | @IsDefined() 8 | reviewerId: string; 9 | 10 | @IsString() 11 | comment: string; 12 | 13 | @IsNumber() 14 | @Min(1) 15 | @Max(10) 16 | rate: number; 17 | 18 | constructor(props: UpdateBookingReviewDto) { 19 | Object.assign(this, props); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/modules/review/src/core/entities/booking-review.ts: -------------------------------------------------------------------------------- 1 | import { Entity, PrimaryKey, Property, OneToOne } from "@mikro-orm/core"; 2 | import { GuidType } from "@travelhoop/infrastructure"; 3 | import { Guid } from "guid-typescript"; 4 | import { ReviewDetails } from "./review-details"; 5 | 6 | @Entity() 7 | export class BookingReview { 8 | @PrimaryKey({ type: GuidType }) 9 | id: Guid; 10 | 11 | @Property({ type: GuidType }) 12 | reviewerId: Guid; 13 | 14 | @Property({ type: GuidType }) 15 | revieweeId: Guid; 16 | 17 | @OneToOne({ fieldName: "reviewDetailsId", nullable: true }) 18 | reviewDetails?: ReviewDetails; 19 | 20 | static create(data: Partial) { 21 | const model = new BookingReview(); 22 | 23 | Object.assign(model, data); 24 | 25 | return model; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/modules/review/src/core/entities/review-details.ts: -------------------------------------------------------------------------------- 1 | import { Entity, PrimaryKey, Property } from "@mikro-orm/core"; 2 | import { GuidType } from "@travelhoop/infrastructure"; 3 | import { Guid } from "guid-typescript"; 4 | 5 | @Entity() 6 | export class ReviewDetails { 7 | @PrimaryKey({ type: GuidType }) 8 | id: Guid; 9 | 10 | @Property() 11 | comment: string; 12 | 13 | @Property() 14 | rate: number; 15 | 16 | @Property() 17 | fullfiledAt: Date = new Date(); 18 | 19 | static create(data: Partial) { 20 | const model = new ReviewDetails(); 21 | 22 | Object.assign(model, data); 23 | 24 | return model; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/modules/review/src/core/error/booking-review-not-found.error.ts: -------------------------------------------------------------------------------- 1 | import { TravelhoopError } from "@travelhoop/abstract-core"; 2 | import StatusCodes from "http-status-codes"; 3 | 4 | export class BookingReviewNotFoundError extends TravelhoopError { 5 | constructor() { 6 | super("Booking review not found", StatusCodes.NOT_FOUND); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/modules/review/src/core/events/external/booking-finished.event.ts: -------------------------------------------------------------------------------- 1 | import { Event } from "@travelhoop/abstract-core"; 2 | 3 | interface BookingFinishedPayload { 4 | id: string; 5 | guestId: string; 6 | hostId: string; 7 | } 8 | 9 | export class BookingFinished implements Event { 10 | name = this.constructor.name; 11 | 12 | constructor(public payload: BookingFinishedPayload) {} 13 | } 14 | -------------------------------------------------------------------------------- /src/modules/review/src/core/repositories/booking-review.repository.ts: -------------------------------------------------------------------------------- 1 | import { Guid } from "guid-typescript"; 2 | import { DbConnection } from "@travelhoop/infrastructure"; 3 | import { BookingReview } from "../entities/booking-review"; 4 | import { BookingReviewNotFoundError } from "../error/booking-review-not-found.error"; 5 | 6 | interface BookingReviewRepositoryDependencies { 7 | dbConnection: DbConnection; 8 | } 9 | export class BookingReviewRepository { 10 | constructor(private readonly deps: BookingReviewRepositoryDependencies) {} 11 | 12 | async add(...bookingReview: BookingReview[]): Promise { 13 | await this.deps.dbConnection.em.persistAndFlush(bookingReview); 14 | } 15 | 16 | async update(bookingReview: BookingReview): Promise { 17 | await this.deps.dbConnection.em.persistAndFlush(bookingReview); 18 | } 19 | 20 | async get(bookingReviewId: Guid) { 21 | const bookingReview = await this.deps.dbConnection.em.getRepository(BookingReview).findOne({ id: bookingReviewId }); 22 | 23 | if (!bookingReview) { 24 | throw new BookingReviewNotFoundError(); 25 | } 26 | 27 | return bookingReview; 28 | } 29 | 30 | async findByReviewerIdAndRevieweeId(reviewerId: Guid, revieweeId: Guid) { 31 | return this.deps.dbConnection.em.getRepository(BookingReview).findOneOrFail({ reviewerId, revieweeId }); 32 | } 33 | 34 | async getByIdAndReviewerId(bookingReviewId: Guid, reviewerId: Guid) { 35 | const bookingReview = await this.deps.dbConnection.em 36 | .getRepository(BookingReview) 37 | .findOneOrFail({ id: bookingReviewId, reviewerId }); 38 | 39 | if (!bookingReview) { 40 | throw new BookingReviewNotFoundError(); 41 | } 42 | 43 | return bookingReview; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/modules/review/src/core/services/booking-review.service.ts: -------------------------------------------------------------------------------- 1 | import { MessageBroker } from "@travelhoop/infrastructure"; 2 | import { Guid } from "guid-typescript"; 3 | import { BookingReviewDto } from "../dto/booking-review.dto"; 4 | import { UpdateBookingReviewDto } from "../dto/update-booking-review.dto"; 5 | import { ReviewDetails } from "../entities/review-details"; 6 | import { BookingReviewRepository } from "../repositories/booking-review.repository"; 7 | 8 | interface BookingReviewServiceDependencies { 9 | bookingReviewRepository: BookingReviewRepository; 10 | messageBroker: MessageBroker; 11 | } 12 | 13 | export class BookingReviewService { 14 | constructor(private readonly deps: BookingReviewServiceDependencies) {} 15 | 16 | async update(dto: UpdateBookingReviewDto) { 17 | const bookingReview = await this.deps.bookingReviewRepository.getByIdAndReviewerId( 18 | Guid.parse(dto.id), 19 | Guid.parse(dto.reviewerId), 20 | ); 21 | 22 | if (bookingReview.reviewDetails) { 23 | throw new Error("Cannot fulfill review twice."); 24 | } 25 | 26 | bookingReview.reviewDetails = ReviewDetails.create({ 27 | id: Guid.create(), 28 | comment: dto.comment, 29 | rate: dto.rate, 30 | }); 31 | 32 | await this.deps.bookingReviewRepository.update(bookingReview); 33 | } 34 | 35 | async getById(bookingReviewId: Guid) { 36 | const bookingReview = await this.deps.bookingReviewRepository.get(bookingReviewId); 37 | return BookingReviewDto.createFromBookingReview(bookingReview); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/modules/review/src/core/subscribers/booking-finished.subscriber.ts: -------------------------------------------------------------------------------- 1 | import { EventSubscriber, EventSubscribersMeta } from "@travelhoop/abstract-core"; 2 | import { Guid } from "guid-typescript"; 3 | import { BookingFinished } from "../events/external/booking-finished.event"; 4 | import { BookingReviewRepository } from "../repositories/booking-review.repository"; 5 | import { BookingReview } from "./../entities/booking-review"; 6 | 7 | interface BookingFinishedSubscriberDependencies { 8 | bookingReviewRepository: BookingReviewRepository; 9 | } 10 | 11 | export class BookingFinishedSubscriber implements EventSubscriber { 12 | constructor(private readonly deps: BookingFinishedSubscriberDependencies) {} 13 | 14 | public getSubscribedEvents(): EventSubscribersMeta[] { 15 | return [{ name: BookingFinished.name, method: "onBookingFinished" }]; 16 | } 17 | 18 | async onBookingFinished({ payload: { hostId, guestId } }: BookingFinished) { 19 | const existingHostReview = await this.deps.bookingReviewRepository.findByReviewerIdAndRevieweeId( 20 | Guid.parse(hostId), 21 | Guid.parse(guestId), 22 | ); 23 | 24 | if (existingHostReview) { 25 | return; 26 | } 27 | 28 | const hostReview = BookingReview.create({ 29 | id: Guid.create(), 30 | reviewerId: Guid.parse(guestId), 31 | revieweeId: Guid.parse(hostId), 32 | }); 33 | 34 | const guestReview = BookingReview.create({ 35 | id: Guid.create(), 36 | reviewerId: Guid.parse(guestId), 37 | revieweeId: Guid.parse(hostId), 38 | }); 39 | 40 | return this.deps.bookingReviewRepository.add(hostReview, guestReview); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/modules/review/src/index.ts: -------------------------------------------------------------------------------- 1 | import { reviewModule } from "./api/review.module"; 2 | 3 | export default reviewModule; 4 | -------------------------------------------------------------------------------- /src/modules/review/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/@travelhoop/toolchain/includes/tsconfig.web.json" 3 | } -------------------------------------------------------------------------------- /src/modules/user/.eslintignore: -------------------------------------------------------------------------------- 1 | **/*.d.ts 2 | *.json -------------------------------------------------------------------------------- /src/modules/user/.eslintrc.js: -------------------------------------------------------------------------------- 1 | require('@travelhoop/toolchain/patch/modern-module-resolution'); 2 | 3 | module.exports = { 4 | extends: [ "./node_modules/@travelhoop/toolchain/includes/.eslintrc" ], // <---- put your profile string here 5 | parserOptions: { 6 | project: "tsconfig.json", 7 | tsconfigRootDir: __dirname 8 | } 9 | }; -------------------------------------------------------------------------------- /src/modules/user/.swcrc: -------------------------------------------------------------------------------- 1 | { 2 | "jsc": { 3 | "parser": { 4 | "syntax": "typescript", 5 | "decorators": true, 6 | "dynamicImport": true 7 | }, 8 | "transform": { 9 | "legacyDecorator": true, 10 | "decoratorMetadata": true 11 | }, 12 | "target": "es2018", 13 | "keepClassNames": true, 14 | "loose": true 15 | }, 16 | "module": { 17 | "type": "commonjs", 18 | "strict": true, 19 | "strictMode": true, 20 | "lazy": false, 21 | "noInterop": false 22 | }, 23 | "sourceMaps": "inline" 24 | } -------------------------------------------------------------------------------- /src/modules/user/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@travelhoop/user-module", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main":"./build/index.js", 6 | "typings":"./build/index.d.ts", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "build": "rimraf build && tsc", 10 | "build:watch":"swc src -d build", 11 | "lint": "eslint './**/*.ts'", 12 | "lint:fix": "eslint './**/*.ts' --fix" 13 | }, 14 | "author": "", 15 | "license": "ISC", 16 | "dependencies": { 17 | "@travelhoop/abstract-core": "1.0.0", 18 | "@travelhoop/infrastructure": "1.0.0", 19 | "express": "~4.17.1", 20 | "rimraf": "~3.0.2", 21 | "guid-typescript": "~1.0.9", 22 | "class-validator": "~0.13.1", 23 | "awilix": "~4.3.3", 24 | "awilix-express": "~4.0.0", 25 | "express-async-handler": "~1.1.4", 26 | "@mikro-orm/core": "~4.5.5", 27 | "@mikro-orm/postgresql": "~4.5.5", 28 | "bcryptjs": "~2.4.3", 29 | "jsonwebtoken": "~8.5.1", 30 | "date-fns": "~2.20.1", 31 | "http-status-codes": "~2.1.4" 32 | }, 33 | "devDependencies": { 34 | "eslint": "~7.23.0", 35 | "typescript": "~4.2.3", 36 | "@travelhoop/toolchain":"1.0.0", 37 | "@types/express": "~4.17.11", 38 | "@types/node": "~14.14.37", 39 | "@swc/core": "~1.2.51", 40 | "@swc/cli": "~0.1.36", 41 | "@types/bcryptjs": "~2.4.2", 42 | "@types/jsonwebtoken": "~8.5.1" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/modules/user/src/api/container.ts: -------------------------------------------------------------------------------- 1 | import { ContainerBuilder, StandardCreateContainerDependencies } from "@travelhoop/infrastructure"; 2 | import { asClass, asValue } from "awilix"; 3 | import { userModuleConfigFactory } from "../core/config"; 4 | import { ProfileRepository } from "../core/repositories/profile.repository"; 5 | import { UserRepository } from "../core/repositories/user.repository"; 6 | import { AuthService } from "../core/services/auth.service"; 7 | import { PasswordManager } from "../core/services/password-hasher"; 8 | import { UserService } from "../core/services/user.service"; 9 | import { UserCreatedSubscriber } from "../core/subscribers/user-created.subscriber"; 10 | import { createRouter } from "./routes/router"; 11 | 12 | export const createContainer = ({ dbConnection, redis }: StandardCreateContainerDependencies) => { 13 | const config = userModuleConfigFactory(process.env as any); 14 | return new ContainerBuilder() 15 | .addCommon() 16 | .register({ 17 | expiry: asValue(config.jwt.expiry), 18 | }) 19 | .addAuth({ secretKey: config.jwt.secretKey }) 20 | .addRedis(redis) 21 | .addRouting(createRouter) 22 | .addDbConnection(dbConnection) 23 | .addEventSubscribers({ 24 | messageBrokerQueueName: config.queues.messageBroker, 25 | eventSubscribers: [UserCreatedSubscriber], 26 | }) 27 | .register({ 28 | passwordManager: asClass(PasswordManager), 29 | authService: asClass(AuthService), 30 | userService: asClass(UserService), 31 | userRepository: asClass(UserRepository), 32 | profileRepository: asClass(ProfileRepository), 33 | }) 34 | .build(); 35 | }; 36 | -------------------------------------------------------------------------------- /src/modules/user/src/api/routes/router.ts: -------------------------------------------------------------------------------- 1 | import express, { Router } from "express"; 2 | import { MiddlewareType } from "@travelhoop/infrastructure"; 3 | import { userApi } from "./user.router"; 4 | 5 | export interface RoutingDependencies { 6 | userRouting: express.Router; 7 | } 8 | 9 | export const createRouter = ({ auth }: { auth: MiddlewareType }): Router => { 10 | const router = express.Router(); 11 | 12 | router.post("/register", userApi("register")); 13 | router.post("/login", userApi("login")); 14 | router.get("/:id", userApi("get")); 15 | router.put("/:id", auth, userApi("update")); 16 | 17 | return router; 18 | }; 19 | -------------------------------------------------------------------------------- /src/modules/user/src/api/routes/user.router.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "@travelhoop/infrastructure"; 2 | import { makeInvoker } from "awilix-express"; 3 | import { validateOrReject } from "class-validator"; 4 | import asyncHandler from "express-async-handler"; 5 | import { Guid } from "guid-typescript"; 6 | import { RegisterDto } from "../../core/dto/register.dto"; 7 | import { UserService } from "../../core/services/user.service"; 8 | import { LoginDto } from "../../core/dto/login.dto"; 9 | import { UpdateUserDto } from "../../core/dto/update-user.dto"; 10 | 11 | interface UserApiDependencies { 12 | userService: UserService; 13 | } 14 | 15 | const api = ({ userService }: UserApiDependencies) => ({ 16 | register: asyncHandler(async (req: Request, res: Response) => { 17 | const dto = new RegisterDto(req.body); 18 | await validateOrReject(dto); 19 | res.json(await userService.register(dto)); 20 | }), 21 | 22 | login: asyncHandler(async (req: Request, res: Response) => { 23 | const dto = new LoginDto(req.body); 24 | await validateOrReject(dto); 25 | res.json(await userService.login(dto)); 26 | }), 27 | 28 | get: asyncHandler(async (req: Request, res: Response) => { 29 | res.json(await userService.get(Guid.parse(req.params.id))); 30 | }), 31 | 32 | update: asyncHandler(async (req: Request, res: Response) => { 33 | const dto = new UpdateUserDto({ id: req.user?.id, ...req.body }); 34 | await validateOrReject(dto); 35 | res.json(await userService.update(dto)); 36 | }), 37 | }); 38 | 39 | export const userApi = makeInvoker(api); 40 | -------------------------------------------------------------------------------- /src/modules/user/src/api/user.module.ts: -------------------------------------------------------------------------------- 1 | import { standardAppModuleFactory } from "@travelhoop/infrastructure"; 2 | import { createContainer } from "./container"; 3 | 4 | export const userModule = standardAppModuleFactory({ basePath: "user", name: "user-module", createContainer }); 5 | -------------------------------------------------------------------------------- /src/modules/user/src/core/config.ts: -------------------------------------------------------------------------------- 1 | export interface EnvVariables extends NodeJS.Process { 2 | JWT_SECRET_KEY: string; 3 | USER_MODULE_JWT_EXPIRES_IN_MINUTES: string; 4 | ASYNC_MESSAGE_BROKER_QUEUE: string; 5 | SCHEDULER_SECURITY_TOKEN: string; 6 | } 7 | 8 | export const userModuleConfigFactory = (env: EnvVariables) => ({ 9 | jwt: { 10 | secretKey: env.JWT_SECRET_KEY, 11 | expiry: Number(env.USER_MODULE_JWT_EXPIRES_IN_MINUTES), 12 | }, 13 | securityTokens: { 14 | schedulerToken: env.SCHEDULER_SECURITY_TOKEN, 15 | }, 16 | queues: { 17 | messageBroker: env.ASYNC_MESSAGE_BROKER_QUEUE, 18 | }, 19 | }); 20 | 21 | export type UserModuleConfig = ReturnType; 22 | -------------------------------------------------------------------------------- /src/modules/user/src/core/dto/login.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail, IsString } from "class-validator"; 2 | 3 | export class LoginDto { 4 | @IsEmail() 5 | email: string; 6 | 7 | @IsString() 8 | password: string; 9 | 10 | constructor(props: LoginDto) { 11 | Object.assign(this, props); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/modules/user/src/core/dto/register.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail, IsString } from "class-validator"; 2 | 3 | export class RegisterDto { 4 | @IsEmail() 5 | email: string; 6 | 7 | @IsString() 8 | password: string; 9 | 10 | constructor(props: RegisterDto) { 11 | Object.assign(this, props); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/modules/user/src/core/dto/update-user.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsString } from "class-validator"; 2 | 3 | export class UpdateUserDto { 4 | @IsString() 5 | id: string; 6 | 7 | @IsString() 8 | firstName?: string; 9 | 10 | @IsString() 11 | lastName?: string; 12 | 13 | @IsString() 14 | location?: string; 15 | 16 | @IsString() 17 | aboutMe?: string; 18 | 19 | constructor(props: UpdateUserDto) { 20 | Object.assign(this, props); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/modules/user/src/core/dto/user.dto.ts: -------------------------------------------------------------------------------- 1 | export class UserDto { 2 | id: string; 3 | 4 | email: string; 5 | 6 | isActive: boolean; 7 | 8 | createdAt: Date; 9 | 10 | profile: { 11 | firstName?: string; 12 | 13 | lastName?: string; 14 | 15 | location?: string; 16 | 17 | aboutMe?: string; 18 | }; 19 | 20 | constructor(props: UserDto) { 21 | Object.assign(this, props); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/modules/user/src/core/entities/profile.ts: -------------------------------------------------------------------------------- 1 | import { Entity, PrimaryKey, Property } from "@mikro-orm/core"; 2 | import { GuidType } from "@travelhoop/infrastructure"; 3 | import { Guid } from "guid-typescript"; 4 | 5 | @Entity() 6 | export class Profile { 7 | @PrimaryKey({ type: GuidType }) 8 | id: Guid; 9 | 10 | @Property({ nullable: true }) 11 | firstName?: string; 12 | 13 | @Property({ nullable: true }) 14 | lastName?: string; 15 | 16 | @Property({ nullable: true }) 17 | location?: string; 18 | 19 | @Property({ nullable: true }) 20 | aboutMe?: string; 21 | 22 | static create(data: Partial) { 23 | const model = new Profile(); 24 | 25 | Object.assign(model, data); 26 | 27 | return model; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/modules/user/src/core/entities/user.ts: -------------------------------------------------------------------------------- 1 | import { Guid } from "guid-typescript"; 2 | import { Entity, Property, PrimaryKey, OneToOne } from "@mikro-orm/core"; 3 | import { GuidType } from "@travelhoop/infrastructure"; 4 | import { Profile } from "./profile"; 5 | 6 | @Entity() 7 | export class User { 8 | @PrimaryKey({ type: GuidType }) 9 | id: Guid; 10 | 11 | @OneToOne({ fieldName: "profileId" }) 12 | profile: Profile; 13 | 14 | @Property() 15 | email: string; 16 | 17 | @Property() 18 | password: string; 19 | 20 | @Property() 21 | isActive: boolean; 22 | 23 | @Property() 24 | createdAt: Date; 25 | 26 | static create(data: Partial) { 27 | const model = new User(); 28 | 29 | Object.assign(model, data); 30 | 31 | return model; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/modules/user/src/core/error/invalid-email-or-password.error.ts: -------------------------------------------------------------------------------- 1 | import { TravelhoopError } from "@travelhoop/abstract-core"; 2 | import StatusCodes from "http-status-codes"; 3 | 4 | export class InvalidEmailOrPasswordError extends TravelhoopError { 5 | constructor() { 6 | super("Invalid email or password", StatusCodes.BAD_REQUEST); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/modules/user/src/core/error/user-exists.error.ts: -------------------------------------------------------------------------------- 1 | import { TravelhoopError } from "@travelhoop/abstract-core"; 2 | import StatusCodes from "http-status-codes"; 3 | 4 | export class UserExistsError extends TravelhoopError { 5 | constructor() { 6 | super("User with provided email exists", StatusCodes.BAD_REQUEST); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/modules/user/src/core/error/user-not-found.error.ts: -------------------------------------------------------------------------------- 1 | import { TravelhoopError } from "@travelhoop/abstract-core"; 2 | import StatusCodes from "http-status-codes"; 3 | 4 | export class UserNotFoundError extends TravelhoopError { 5 | constructor() { 6 | super("User not found", StatusCodes.NOT_FOUND); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/modules/user/src/core/events/user-created.event.ts: -------------------------------------------------------------------------------- 1 | import { Event } from "@travelhoop/abstract-core"; 2 | 3 | interface UserCreatedPayload { 4 | id: string; 5 | } 6 | 7 | export class UserCreated implements Event { 8 | name = this.constructor.name; 9 | 10 | constructor(public payload: UserCreatedPayload) {} 11 | } 12 | -------------------------------------------------------------------------------- /src/modules/user/src/core/repositories/profile.repository.ts: -------------------------------------------------------------------------------- 1 | import { Guid } from "guid-typescript"; 2 | import { DbConnection } from "@travelhoop/infrastructure"; 3 | import { Profile } from "../entities/profile"; 4 | 5 | interface ProfileRepositoryDependencies { 6 | dbConnection: DbConnection; 7 | } 8 | export class ProfileRepository { 9 | constructor(private readonly deps: ProfileRepositoryDependencies) {} 10 | 11 | async find(id: Guid) { 12 | return this.deps.dbConnection.em.getRepository(Profile).findOne({ id }); 13 | } 14 | 15 | async add(profile: Profile): Promise { 16 | await this.deps.dbConnection.em.persistAndFlush(profile); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/modules/user/src/core/repositories/user.repository.ts: -------------------------------------------------------------------------------- 1 | import { DbConnection } from "@travelhoop/infrastructure"; 2 | import { Guid } from "guid-typescript"; 3 | import { User } from "../entities/user"; 4 | import { UserNotFoundError } from "../error/user-not-found.error"; 5 | 6 | interface UserRepositoryDependencies { 7 | dbConnection: DbConnection; 8 | } 9 | export class UserRepository { 10 | constructor(private readonly deps: UserRepositoryDependencies) {} 11 | 12 | async add(user: User): Promise { 13 | await this.deps.dbConnection.em.persistAndFlush(user); 14 | } 15 | 16 | async get(id: Guid) { 17 | const user = await this.deps.dbConnection.em.getRepository(User).findOne({ id }, { profile: true }); 18 | 19 | if (!user) { 20 | throw new UserNotFoundError(); 21 | } 22 | 23 | return user; 24 | } 25 | 26 | async getByEmail(email: string) { 27 | const user = await this.deps.dbConnection.em.getRepository(User).findOne({ email }, { profile: true }); 28 | 29 | if (!user) { 30 | throw new UserNotFoundError(); 31 | } 32 | 33 | return user; 34 | } 35 | 36 | async findByEmail(email: string) { 37 | return this.deps.dbConnection.em.getRepository(User).findOne({ email }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/modules/user/src/core/services/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { AuthenticatedUser } from "@travelhoop/abstract-core"; 2 | import { Guid } from "guid-typescript"; 3 | import { sign } from "jsonwebtoken"; 4 | import { addMinutes } from "date-fns"; 5 | 6 | interface AuthServiceDependencies { 7 | expiry: number; 8 | secretKey: string; 9 | } 10 | 11 | export class AuthService { 12 | constructor(private readonly deps: AuthServiceDependencies) {} 13 | 14 | createToken(userId: Guid) { 15 | const expiresIn = Math.floor(addMinutes(new Date(), this.deps.expiry).getTime() / 1000); 16 | const authUser: AuthenticatedUser = { id: userId, exp: expiresIn }; 17 | return sign(authUser, this.deps.secretKey); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/modules/user/src/core/services/password-hasher.ts: -------------------------------------------------------------------------------- 1 | import { hash, compare } from "bcryptjs"; 2 | 3 | export class PasswordManager { 4 | hashPassword(password: string) { 5 | return hash(password, 12); 6 | } 7 | 8 | compare(password: string, hashedPassword: string) { 9 | return compare(password, hashedPassword); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/modules/user/src/core/subscribers/user-created.subscriber.ts: -------------------------------------------------------------------------------- 1 | import { Guid } from "guid-typescript"; 2 | import { EventSubscriber, EventSubscribersMeta } from "@travelhoop/abstract-core"; 3 | import { Profile } from "../entities/profile"; 4 | import { UserCreated } from "../events/user-created.event"; 5 | import { ProfileRepository } from "../repositories/profile.repository"; 6 | 7 | interface UserCreatedSubscriberDependencies { 8 | profileRepository: ProfileRepository; 9 | } 10 | 11 | export class UserCreatedSubscriber implements EventSubscriber { 12 | constructor(private readonly deps: UserCreatedSubscriberDependencies) {} 13 | 14 | public getSubscribedEvents(): EventSubscribersMeta[] { 15 | return [{ name: UserCreated.name, method: "onUserCreated" }]; 16 | } 17 | 18 | async onUserCreated({ payload: { id } }: UserCreated) { 19 | const profileId = Guid.parse(id); 20 | if (await this.deps.profileRepository.find(profileId)) { 21 | return; 22 | } 23 | return this.deps.profileRepository.add(Profile.create({ id: profileId })); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/modules/user/src/index.ts: -------------------------------------------------------------------------------- 1 | import { userModule } from "./api/user.module"; 2 | 3 | export default userModule; 4 | -------------------------------------------------------------------------------- /src/modules/user/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/@travelhoop/toolchain/includes/tsconfig.web.json" 3 | } -------------------------------------------------------------------------------- /src/modules/user/user.rest: -------------------------------------------------------------------------------- 1 | @url = http://localhost:3010/user 2 | @email = user1@testing.com 3 | 4 | @password = password1234 5 | 6 | @userId = 3753ddba-acc3-c25c-3d48-762ac4d8cc28 7 | 8 | @accessToken=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOnsidmFsdWUiOiIzNzUzZGRiYS1hY2MzLWMyNWMtM2Q0OC03NjJhYzRkOGNjMjgifSwiZXhwIjoxNjE4ODUxMDEzMjM1LCJpYXQiOjE2MTg4NDkyMTN9.xn1v7Bdvep5QVvnhHFvySw3WgAWGFzqLKfKPUII4EO0 9 | ### 10 | POST {{url}}/register HTTP/1.1 11 | content-type: application/json 12 | 13 | { 14 | "email":"{{email}}", 15 | "password":"{{password}}" 16 | } 17 | 18 | ### 19 | GET {{url}}/{{userId}} HTTP/1.1 20 | content-type: application/json 21 | ### 22 | POST {{url}}/login HTTP/1.1 23 | content-type: application/json 24 | 25 | { 26 | "email":"{{email}}", 27 | "password":"{{password}}" 28 | } 29 | ### 30 | PUT {{url}}/{{userId}} HTTP/1.1 31 | content-type: application/json 32 | authorization: Bearer {{accessToken}} 33 | 34 | { 35 | "firstName":"John", 36 | "lastName":"Doe", 37 | "location":"London, UK", 38 | "aboutMe":"Lorem ipsum" 39 | } -------------------------------------------------------------------------------- /src/shared/abstract-core/.eslintignore: -------------------------------------------------------------------------------- 1 | **/*.d.ts 2 | *.json -------------------------------------------------------------------------------- /src/shared/abstract-core/.eslintrc.js: -------------------------------------------------------------------------------- 1 | require('@travelhoop/toolchain/patch/modern-module-resolution'); 2 | 3 | module.exports = { 4 | extends: [ "./node_modules/@travelhoop/toolchain/includes/.eslintrc" ], // <---- put your profile string here 5 | parserOptions: { 6 | project: "tsconfig.json", 7 | tsconfigRootDir: __dirname 8 | } 9 | }; -------------------------------------------------------------------------------- /src/shared/abstract-core/.swcrc: -------------------------------------------------------------------------------- 1 | { 2 | "jsc": { 3 | "parser": { 4 | "syntax": "typescript", 5 | "tsx": false, 6 | "decorators": true, 7 | "dynamicImport": true 8 | }, 9 | "target": "es2019" 10 | }, 11 | "module": { 12 | "type": "commonjs", 13 | "strict": true, 14 | "strictMode": true, 15 | "lazy": true, 16 | "noInterop": false 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/shared/abstract-core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@travelhoop/abstract-core", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main":"./build/index.js", 6 | "typings":"./build/index.d.ts", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "build": "rimraf build && tsc", 10 | "build:watch":"swc src -d build", 11 | "lint": "eslint './**/*.ts'", 12 | "lint:fix": "eslint './**/*.ts' --fix" 13 | }, 14 | "author": "", 15 | "license": "ISC", 16 | "dependencies": { 17 | "rimraf": "~3.0.2", 18 | "winston": "~3.3.3", 19 | "guid-typescript": "~1.0.9" 20 | }, 21 | "devDependencies": { 22 | "@travelhoop/toolchain":"1.0.0", 23 | "@types/node": "~14.14.37", 24 | "eslint": "~7.23.0", 25 | "typescript": "~4.2.3", 26 | "@swc/core": "~1.2.51", 27 | "@swc/cli": "~0.1.36" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/shared/abstract-core/src/auth/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./user"; 2 | -------------------------------------------------------------------------------- /src/shared/abstract-core/src/auth/user.ts: -------------------------------------------------------------------------------- 1 | import { Guid } from "guid-typescript"; 2 | 3 | export interface AuthenticatedUser { 4 | id: Guid; 5 | exp: number; 6 | } 7 | -------------------------------------------------------------------------------- /src/shared/abstract-core/src/command/command-dispatcher.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "."; 2 | 3 | export interface CommandDispatcher { 4 | execute(command: Command): Promise; 5 | } 6 | -------------------------------------------------------------------------------- /src/shared/abstract-core/src/command/command-handler.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "."; 2 | 3 | export interface CommandHandler = Command, TResult = void> { 4 | execute: (command: T) => Promise; 5 | } 6 | -------------------------------------------------------------------------------- /src/shared/abstract-core/src/command/command.ts: -------------------------------------------------------------------------------- 1 | export interface Command { 2 | payload: TPayload; 3 | } 4 | -------------------------------------------------------------------------------- /src/shared/abstract-core/src/command/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./command"; 2 | 3 | export * from "./command-dispatcher"; 4 | 5 | export * from "./command-handler"; 6 | -------------------------------------------------------------------------------- /src/shared/abstract-core/src/error/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./travelhoop.error"; 2 | -------------------------------------------------------------------------------- /src/shared/abstract-core/src/error/travelhoop.error.ts: -------------------------------------------------------------------------------- 1 | export abstract class TravelhoopError extends Error { 2 | public statusCode: number = 400; 3 | 4 | constructor(message: string, statusCode: number) { 5 | super(message); 6 | this.statusCode = statusCode; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/shared/abstract-core/src/event/event.dispatcher.ts: -------------------------------------------------------------------------------- 1 | import { Event } from "./event"; 2 | 3 | export interface EventDispatcher { 4 | dispatch(events: Event[]): Promise; 5 | dispatch(event: Event): Promise; 6 | } 7 | -------------------------------------------------------------------------------- /src/shared/abstract-core/src/event/event.subscriber.ts: -------------------------------------------------------------------------------- 1 | export interface EventSubscribersMeta { 2 | name: string; 3 | method: keyof T & string; 4 | } 5 | 6 | export interface EventSubscriber { 7 | getSubscribedEvents(): EventSubscribersMeta[]; 8 | } 9 | -------------------------------------------------------------------------------- /src/shared/abstract-core/src/event/event.ts: -------------------------------------------------------------------------------- 1 | export interface Event { 2 | name: string; 3 | payload: object; 4 | } 5 | -------------------------------------------------------------------------------- /src/shared/abstract-core/src/event/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./event"; 2 | 3 | export * from "./event.dispatcher"; 4 | 5 | export * from "./event.subscriber"; 6 | -------------------------------------------------------------------------------- /src/shared/abstract-core/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./logger"; 2 | 3 | export * from "./event"; 4 | 5 | export * from "./queue"; 6 | 7 | export * from "./messaging"; 8 | 9 | export * from "./auth"; 10 | 11 | export * from "./error"; 12 | 13 | export * from "./command"; 14 | -------------------------------------------------------------------------------- /src/shared/abstract-core/src/logger/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./logger"; 2 | -------------------------------------------------------------------------------- /src/shared/abstract-core/src/logger/logger.ts: -------------------------------------------------------------------------------- 1 | export type LogMethod = (level: string, msg: string) => Logger; 2 | 3 | export type LeveledLogMethod = (msg: string, error?: any) => Logger; 4 | 5 | export interface Logger { 6 | log: LogMethod; 7 | error: LeveledLogMethod; 8 | warn: LeveledLogMethod; 9 | info: LeveledLogMethod; 10 | verbose: LeveledLogMethod; 11 | debug: LeveledLogMethod; 12 | } 13 | -------------------------------------------------------------------------------- /src/shared/abstract-core/src/messaging/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./message-dispatcher"; 2 | 3 | export * from "./message"; 4 | -------------------------------------------------------------------------------- /src/shared/abstract-core/src/messaging/message-dispatcher.ts: -------------------------------------------------------------------------------- 1 | export interface MessageDispatcher { 2 | publish(message: string): Promise; 3 | } 4 | -------------------------------------------------------------------------------- /src/shared/abstract-core/src/messaging/message.ts: -------------------------------------------------------------------------------- 1 | export interface InternalAsyncMessage { 2 | name: string; 3 | payload: object; 4 | } 5 | -------------------------------------------------------------------------------- /src/shared/abstract-core/src/queue/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./queue"; 2 | -------------------------------------------------------------------------------- /src/shared/abstract-core/src/queue/queue.ts: -------------------------------------------------------------------------------- 1 | export interface QueueClient { 2 | createQueue(queueName: string): Promise; 3 | removeQueue(queueName: string): Promise; 4 | getMessage(queueName: string): Promise; 5 | sendMessage(queueName: string, message: string): Promise; 6 | } 7 | -------------------------------------------------------------------------------- /src/shared/abstract-core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/@travelhoop/toolchain/includes/tsconfig.web.json" 3 | } -------------------------------------------------------------------------------- /src/shared/infrastructure/.eslintignore: -------------------------------------------------------------------------------- 1 | **/*.d.ts 2 | *.json -------------------------------------------------------------------------------- /src/shared/infrastructure/.eslintrc.js: -------------------------------------------------------------------------------- 1 | require('@travelhoop/toolchain/patch/modern-module-resolution'); 2 | 3 | module.exports = { 4 | extends: [ "./node_modules/@travelhoop/toolchain/includes/.eslintrc" ], // <---- put your profile string here 5 | parserOptions: { 6 | project: "tsconfig.json", 7 | tsconfigRootDir: __dirname 8 | } 9 | }; -------------------------------------------------------------------------------- /src/shared/infrastructure/.swcrc: -------------------------------------------------------------------------------- 1 | { 2 | "jsc": { 3 | "parser": { 4 | "syntax": "typescript", 5 | "decorators": true, 6 | "dynamicImport": true 7 | }, 8 | "transform": { 9 | "legacyDecorator": true, 10 | "decoratorMetadata": true 11 | }, 12 | "target": "es2018", 13 | "keepClassNames": true, 14 | "loose": true 15 | }, 16 | "module": { 17 | "type": "commonjs", 18 | "strict": true, 19 | "strictMode": true, 20 | "lazy": false, 21 | "noInterop": false 22 | }, 23 | "sourceMaps": "inline" 24 | } -------------------------------------------------------------------------------- /src/shared/infrastructure/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@travelhoop/infrastructure", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main":"./build/index.js", 6 | "typings":"./build/index.d.ts", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "build": "rimraf build && tsc", 10 | "build:watch":"swc src -d build", 11 | "lint": "eslint './**/*.ts'", 12 | "lint:fix": "eslint './**/*.ts' --fix" 13 | }, 14 | "author": "", 15 | "license": "ISC", 16 | "dependencies": { 17 | "@travelhoop/abstract-core":"1.0.0", 18 | "@travelhoop/shared-kernel":"1.0.0", 19 | "express": "~4.17.1", 20 | "rimraf": "~3.0.2", 21 | "winston": "~3.3.3", 22 | "awilix": "~4.3.3", 23 | "@mikro-orm/core": "~4.5.5", 24 | "guid-typescript": "~1.0.9", 25 | "dotenv": "~8.2.0", 26 | "redis": "~3.1.1", 27 | "rsmq": "~0.12.3", 28 | "rsmq-worker": "~0.5.2", 29 | "jsonwebtoken": "~8.5.1" 30 | }, 31 | "devDependencies": { 32 | "@travelhoop/toolchain":"1.0.0", 33 | "@types/node": "~14.14.37", 34 | "@types/express": "~4.17.11", 35 | "@types/jsonwebtoken": "~8.5.1", 36 | "eslint": "~7.23.0", 37 | "typescript": "~4.2.3", 38 | "@swc/core": "~1.2.51", 39 | "@swc/cli": "~0.1.36", 40 | "@types/redis": "~2.8.28", 41 | "@types/rsmq-worker": "~0.3.29" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/shared/infrastructure/src/command/command-dispatcher.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandDispatcher, CommandHandler } from "@travelhoop/abstract-core"; 2 | 3 | interface CommandHandlers { 4 | [key: string]: CommandHandler; 5 | } 6 | 7 | export class InMemoryCommandDispatcher implements CommandDispatcher { 8 | private availableHandlers: CommandHandlers; 9 | 10 | constructor(commandHandlers: CommandHandler[]) { 11 | this.availableHandlers = commandHandlers.reduce((handlers: CommandHandlers, handler) => { 12 | handlers[this.getConstructorName(handler)] = handler; 13 | return handlers; 14 | }, {}); 15 | } 16 | 17 | execute(command: Command): Promise { 18 | if (!this.availableHandlers[this.getHandlerName(command)]) { 19 | return Promise.reject(new Error(`Command: ${this.getConstructorName(command)} is not supported.`)); 20 | } 21 | 22 | return this.availableHandlers[this.getHandlerName(command)].execute(command) as any; 23 | } 24 | 25 | private getConstructorName(object: object) { 26 | return object.constructor.name; 27 | } 28 | 29 | private getHandlerName(command: Command) { 30 | return `${this.getConstructorName(command)}Handler`; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/shared/infrastructure/src/command/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./command-dispatcher"; 2 | -------------------------------------------------------------------------------- /src/shared/infrastructure/src/config/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./load-env"; 2 | -------------------------------------------------------------------------------- /src/shared/infrastructure/src/config/load-env.ts: -------------------------------------------------------------------------------- 1 | const dotenv = require("dotenv"); 2 | 3 | export const loadEnvs = () => { 4 | dotenv.config(); 5 | }; 6 | -------------------------------------------------------------------------------- /src/shared/infrastructure/src/container/as-array.ts: -------------------------------------------------------------------------------- 1 | import { AwilixContainer, Resolver } from "awilix"; 2 | 3 | export function registerAsArray(resolvers: Resolver[]): Resolver { 4 | return { 5 | resolve: (container: AwilixContainer) => resolvers.map((r: Resolver) => container.build(r)), 6 | }; 7 | } 8 | -------------------------------------------------------------------------------- /src/shared/infrastructure/src/container/as-dictionary.ts: -------------------------------------------------------------------------------- 1 | import { AwilixContainer, asFunction } from "awilix"; 2 | 3 | export function asDictionary(dictionary: Record T>) { 4 | return { 5 | resolve: (container: AwilixContainer) => { 6 | const newDictionary: { [key: string]: any } = {}; 7 | Object.entries(dictionary).forEach(([key, value]) => { 8 | newDictionary[key] = container.build(asFunction(value)); 9 | }); 10 | return newDictionary; 11 | }, 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /src/shared/infrastructure/src/container/index.ts: -------------------------------------------------------------------------------- 1 | export { ContainerBuilder } from "./container-builder"; 2 | 3 | export { scopePerRequest } from "./middleware/scope-per-request"; 4 | 5 | export { registerAsArray } from "./as-array"; 6 | -------------------------------------------------------------------------------- /src/shared/infrastructure/src/container/middleware/scope-per-request.ts: -------------------------------------------------------------------------------- 1 | import { AwilixContainer } from "awilix"; 2 | import { Request, Response } from "express"; 3 | 4 | export const scopePerRequest = (basePath: string, container: AwilixContainer) => ( 5 | req: Request | any, 6 | _res: Response, 7 | next: Function, 8 | ) => { 9 | if (req.path.startsWith(basePath)) { 10 | // eslint-disable-next-line no-param-reassign 11 | req.container = container.createScope(); 12 | } 13 | next(); 14 | }; 15 | -------------------------------------------------------------------------------- /src/shared/infrastructure/src/event/event.dispatcher.ts: -------------------------------------------------------------------------------- 1 | import { Event, EventDispatcher, Logger, EventSubscriber } from "@travelhoop/abstract-core"; 2 | 3 | type Subscriber = (event: Event) => Promise; 4 | 5 | type Subscribers = { name: string; subscriber: Subscriber }; 6 | 7 | interface InMemoryEventDispatcherDependencies { 8 | logger: Logger; 9 | eventSubscribers: EventSubscriber[]; 10 | } 11 | 12 | export class InMemoryEventDispatcher implements EventDispatcher { 13 | private logger: Logger; 14 | 15 | private subscribers: Subscribers[] = []; 16 | 17 | constructor({ logger, eventSubscribers }: InMemoryEventDispatcherDependencies) { 18 | if (eventSubscribers) { 19 | this.addSubscribers(eventSubscribers); 20 | } 21 | 22 | this.logger = logger; 23 | } 24 | 25 | public async dispatch(event: Event | Event[]) { 26 | const events = event instanceof Array ? event : [event]; 27 | 28 | const eventNames = events.map(e => e.name); 29 | 30 | const promises = this.subscribers 31 | .filter(s => eventNames.includes(s.name)) 32 | .map(({ subscriber, name: eventName }) => { 33 | const subscribedEvent = events.find(e => e.name === eventName); 34 | 35 | if (!subscribedEvent) { 36 | throw new Error(`There is no subscriber for ${eventName} event`); 37 | } 38 | 39 | return subscriber(subscribedEvent).catch(e => 40 | this.logger.debug(`Subscriber failed to handle event ${eventName}`, e), 41 | ); 42 | }); 43 | 44 | if (promises.length) { 45 | this.logger.debug(`Dispatching events ${eventNames.join(", ")}@${JSON.stringify(events)}`); 46 | } 47 | 48 | await Promise.all(promises); 49 | } 50 | 51 | private addSubscribers(subscribers: EventSubscriber[]) { 52 | subscribers.forEach(subscriber => this.addSubscriber(subscriber)); 53 | } 54 | 55 | private addSubscriber(subscriber: EventSubscriber) { 56 | if (subscriber.getSubscribedEvents().length === 0) { 57 | return; 58 | } 59 | 60 | const subscribers = subscriber.getSubscribedEvents().map(({ name, method }) => ({ 61 | name, 62 | subscriber: (subscriber as any)[method].bind(subscriber), 63 | })); 64 | 65 | this.subscribers.push(...subscribers); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/shared/infrastructure/src/express/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./request"; 2 | 3 | export * from "./response"; 4 | 5 | export * from "./middleware"; 6 | -------------------------------------------------------------------------------- /src/shared/infrastructure/src/express/middleware/auth.ts: -------------------------------------------------------------------------------- 1 | import { Guid } from "guid-typescript"; 2 | import { Logger, AuthenticatedUser } from "@travelhoop/abstract-core"; 3 | import { Request, Response, NextFunction } from "express"; 4 | import { verify } from "jsonwebtoken"; 5 | 6 | interface AuthDependencies { 7 | logger: Logger; 8 | secretKey: string; 9 | } 10 | 11 | export const auth = 12 | ({ secretKey }: AuthDependencies) => 13 | (req: Request & { user?: AuthenticatedUser }, _res: Response, next: NextFunction) => { 14 | const token = req.headers.authorization?.split("Bearer ")[1]; 15 | 16 | if (!token) { 17 | throw new Error("Invalid token"); 18 | } 19 | 20 | try { 21 | const parsedToken = verify(token, secretKey) as any; 22 | const id = Guid.parse(parsedToken.id.value); 23 | 24 | // eslint-disable-next-line no-param-reassign 25 | req.user = { 26 | ...parsedToken, 27 | id, 28 | }; 29 | } catch (err) { 30 | throw new Error("Invalid token"); 31 | } 32 | 33 | next(); 34 | }; 35 | -------------------------------------------------------------------------------- /src/shared/infrastructure/src/express/middleware/error-handler.ts: -------------------------------------------------------------------------------- 1 | import { Logger, TravelhoopError } from "@travelhoop/abstract-core"; 2 | import { Request, Response, NextFunction } from "express"; 3 | 4 | interface ErrorHandlerDependencies { 5 | logger: Logger; 6 | } 7 | interface ValidationError { 8 | property: string; 9 | constraints: Record; 10 | } 11 | 12 | const handleClassValidator = (err: ValidationError[]) => { 13 | return err; 14 | }; 15 | 16 | export const errorHandler = 17 | ({ logger }: ErrorHandlerDependencies) => 18 | (err: Error, _req: Request, res: Response, _next: NextFunction) => { 19 | logger.error(err.toString()); 20 | 21 | if (Array.isArray(err)) { 22 | return res.status(400).json(handleClassValidator(err)); 23 | } 24 | 25 | if (err instanceof TravelhoopError) { 26 | return res.status(err.statusCode).json({ 27 | error: err.message, 28 | stack: err.stack, 29 | }); 30 | } 31 | 32 | return res.status(500).json({ 33 | error: err.message, 34 | stack: err.stack, 35 | }); 36 | }; 37 | -------------------------------------------------------------------------------- /src/shared/infrastructure/src/express/middleware/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./auth"; 2 | 3 | export * from "./scheduler-token"; 4 | 5 | export * from "./error-handler"; 6 | 7 | export * from "./middleware.type"; 8 | 9 | export * from "./request-context"; 10 | -------------------------------------------------------------------------------- /src/shared/infrastructure/src/express/middleware/middleware.type.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from "express"; 2 | 3 | export type MiddlewareType = (req: Request, res: Response, next: NextFunction) => Promise; 4 | -------------------------------------------------------------------------------- /src/shared/infrastructure/src/express/middleware/request-context.ts: -------------------------------------------------------------------------------- 1 | import { RequestContext } from "@mikro-orm/core"; 2 | import { NextFunction, Request, Response } from "express"; 3 | import { DbConnection } from "../../mikro-orm"; 4 | 5 | export const requestContext = 6 | ({ dbConnection }: { dbConnection: DbConnection }) => 7 | (_req: Request, _res: Response, next: NextFunction) => { 8 | RequestContext.create(dbConnection.em, next); 9 | }; 10 | -------------------------------------------------------------------------------- /src/shared/infrastructure/src/express/middleware/scheduler-token.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from "@travelhoop/abstract-core"; 2 | import { NextFunction, Request, Response } from "express"; 3 | 4 | interface SchedulerTokenDependencies { 5 | logger: Logger; 6 | schedulerToken: string; 7 | } 8 | 9 | export const checkSchedulerToken = 10 | ({ schedulerToken }: SchedulerTokenDependencies) => 11 | (req: Request, _res: Response, next: NextFunction) => { 12 | const token = req.headers["x-scheduler-token"]; 13 | 14 | if (!token || token !== schedulerToken) { 15 | throw new Error("Invalid token"); 16 | } 17 | 18 | next(); 19 | }; 20 | -------------------------------------------------------------------------------- /src/shared/infrastructure/src/express/request.ts: -------------------------------------------------------------------------------- 1 | import { AuthenticatedUser } from "@travelhoop/abstract-core"; 2 | import { Request as ExpressRequest } from "express"; 3 | 4 | export interface Request extends ExpressRequest { 5 | user?: AuthenticatedUser; 6 | } 7 | -------------------------------------------------------------------------------- /src/shared/infrastructure/src/express/response.ts: -------------------------------------------------------------------------------- 1 | import { Response as ExpressResponse } from "express"; 2 | 3 | export interface Response extends ExpressResponse {} 4 | -------------------------------------------------------------------------------- /src/shared/infrastructure/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./logger"; 2 | 3 | export * from "./container"; 4 | 5 | export * from "./mikro-orm"; 6 | 7 | export * from "./config"; 8 | 9 | export * from "./messaging"; 10 | 11 | export * from "./module"; 12 | 13 | export * from "./command"; 14 | 15 | export * from "./express"; 16 | -------------------------------------------------------------------------------- /src/shared/infrastructure/src/logger/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./logger"; 2 | export * from "./types"; 3 | -------------------------------------------------------------------------------- /src/shared/infrastructure/src/logger/logger.ts: -------------------------------------------------------------------------------- 1 | import * as winston from "winston"; 2 | 3 | export const obfuscateCircular = (obj: object, keysToHide: string[]) => { 4 | const seen = new WeakSet(); 5 | const obfuscatory = "******"; 6 | 7 | return JSON.stringify(obj, (k, v) => { 8 | if (keysToHide.includes(k)) { 9 | if (v !== null && typeof v === "string" && v.length >= 20) { 10 | return `${obfuscatory}${v.substring(v.length - 6)}`; 11 | } 12 | return obfuscatory; 13 | } 14 | if (v !== null && typeof v === "object") { 15 | if (seen.has(v)) { 16 | return undefined; 17 | } 18 | seen.add(v); 19 | } 20 | return v; 21 | }); 22 | }; 23 | 24 | const logFormat = ( 25 | env = process.env, 26 | keysToHide: string[] = env.LOGGER_KEYS_TO_HIDE ? env.LOGGER_KEYS_TO_HIDE.split(",") : [], 27 | ) => 28 | winston.format.printf(({ level, message, meta }) => { 29 | const stack = meta && meta.stack ? meta.stack : undefined; 30 | 31 | const logMessage = typeof message === "object" ? obfuscateCircular(message, keysToHide) : message; 32 | 33 | return JSON.stringify({ 34 | "@timestamp": new Date().toISOString(), 35 | "@version": 1, 36 | application: env.APP_NAME, 37 | environment: env.NODE_ENV, 38 | host: env.HOST, 39 | message: logMessage, 40 | meta, 41 | stack, 42 | severity: level, 43 | type: "stdin", 44 | }); 45 | }); 46 | 47 | export const createLogger = (env = process.env, keysToHide?: string[]) => 48 | winston.createLogger({ 49 | level: env.LOGGING_LEVEL || "debug", 50 | format: winston.format.combine(winston.format.splat(), logFormat(env, keysToHide)), 51 | transports: [new winston.transports.Console()], 52 | }); 53 | -------------------------------------------------------------------------------- /src/shared/infrastructure/src/logger/types.ts: -------------------------------------------------------------------------------- 1 | export type LogMethod = (level: string, msg: string) => Logger; 2 | 3 | export type LeveledLogMethod = (msg: string, error?: any) => Logger; 4 | 5 | export interface Logger { 6 | log: LogMethod; 7 | error: LeveledLogMethod; 8 | warn: LeveledLogMethod; 9 | info: LeveledLogMethod; 10 | verbose: LeveledLogMethod; 11 | debug: LeveledLogMethod; 12 | } 13 | -------------------------------------------------------------------------------- /src/shared/infrastructure/src/messaging/background.message-dispatcher.ts: -------------------------------------------------------------------------------- 1 | import { RedisClient as Redis } from "redis"; 2 | import RSMQWorker from "rsmq-worker"; 3 | import { Logger } from "@travelhoop/abstract-core"; 4 | import { AppModule } from ".."; 5 | 6 | interface BacgroundMessageDispatcher { 7 | redis: Redis; 8 | queueName: string; 9 | logger: Logger; 10 | modules: AppModule[]; 11 | } 12 | 13 | export const createBackgroundMessageDispatcher = ({ 14 | redis, 15 | queueName, 16 | logger, 17 | modules, 18 | }: BacgroundMessageDispatcher) => { 19 | const worker = new RSMQWorker(queueName, { redis }); 20 | 21 | worker.on("message", (msg, next, id) => { 22 | // process your message 23 | logger.info(`Processing message with id : ${id} and payload ${msg}`); 24 | 25 | const message = JSON.parse(msg); 26 | 27 | Promise.all(modules.map(appModule => appModule.dispatchEvent(message).catch(logger.error))).then(() => next()); 28 | }); 29 | 30 | // optional error listeners 31 | worker.on("error", (err, msg) => { 32 | logger.error("ERROR", { err, msg }); 33 | }); 34 | worker.on("exceeded", msg => { 35 | logger.error("EXCEEDED", msg.id); 36 | }); 37 | worker.on("timeout", msg => { 38 | logger.error("TIMEOUT", { msg }); 39 | }); 40 | 41 | worker.start(); 42 | }; 43 | -------------------------------------------------------------------------------- /src/shared/infrastructure/src/messaging/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./message-broker"; 2 | 3 | export * from "./redis.message-dispatcher"; 4 | 5 | export * from "./background.message-dispatcher"; 6 | -------------------------------------------------------------------------------- /src/shared/infrastructure/src/messaging/message-broker.ts: -------------------------------------------------------------------------------- 1 | import { MessageDispatcher } from "@travelhoop/abstract-core"; 2 | 3 | interface MessageBrokerDependencies { 4 | messageDispatcher: MessageDispatcher; 5 | } 6 | 7 | export class MessageBroker { 8 | constructor(private readonly deps: MessageBrokerDependencies) {} 9 | 10 | async publish(message: TMessage) { 11 | await this.deps.messageDispatcher.publish(JSON.stringify(message)); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/shared/infrastructure/src/messaging/redis.message-dispatcher.ts: -------------------------------------------------------------------------------- 1 | import { QueueClient, MessageDispatcher } from "@travelhoop/abstract-core"; 2 | 3 | interface RedisMessageBrokerDependencies { 4 | queueClient: QueueClient; 5 | messageBrokerQueueName: string; 6 | } 7 | 8 | export class RedisMessageDispatcher implements MessageDispatcher { 9 | constructor(private readonly deps: RedisMessageBrokerDependencies) {} 10 | 11 | async publish(message: string): Promise { 12 | await this.deps.queueClient.sendMessage(this.deps.messageBrokerQueueName, message); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/shared/infrastructure/src/mikro-orm/db-connection.ts: -------------------------------------------------------------------------------- 1 | import { MikroORM, IDatabaseDriver, Connection } from "@mikro-orm/core"; 2 | 3 | export type DbConnection = MikroORM>; 4 | -------------------------------------------------------------------------------- /src/shared/infrastructure/src/mikro-orm/decorators/transactional-command-dispatcher.decorator.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandDispatcher } from "@travelhoop/abstract-core"; 2 | 3 | interface TransactionalCommandDispatcherDecoratorDependencies { 4 | commandDispatcher: CommandDispatcher; 5 | } 6 | 7 | export class TransactionalCommandDispatcherDecorator implements CommandDispatcher { 8 | constructor(private readonly deps: TransactionalCommandDispatcherDecoratorDependencies) {} 9 | 10 | async execute(command: Command): Promise { 11 | console.log("before"); 12 | 13 | await this.deps.commandDispatcher.execute(command); 14 | 15 | console.log("after"); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/shared/infrastructure/src/mikro-orm/entity-schema/aggregate-root.ts: -------------------------------------------------------------------------------- 1 | import { EntitySchema } from "@mikro-orm/core"; 2 | import { AggregateId, AggregateRootProps } from "@travelhoop/shared-kernel"; 3 | import { AggregateIdType } from "../types/aggregate-id"; 4 | 5 | export const aggregateRootEntitySchema = new EntitySchema>({ 6 | name: "AggregateRoot", 7 | abstract: true, 8 | properties: { 9 | id: { type: AggregateIdType, primary: true }, 10 | version: { type: "number", default: 1, version: true }, 11 | domainEvents: { type: "array", persist: false }, 12 | versionIncremented: { type: "boolean", persist: false }, 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /src/shared/infrastructure/src/mikro-orm/entity-schema/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./aggregate-root"; 2 | -------------------------------------------------------------------------------- /src/shared/infrastructure/src/mikro-orm/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./types"; 2 | 3 | export * from "./entity-schema"; 4 | 5 | export * from "./repository"; 6 | 7 | export * from "./db-connection"; 8 | -------------------------------------------------------------------------------- /src/shared/infrastructure/src/mikro-orm/repository.ts: -------------------------------------------------------------------------------- 1 | import { Connection, EntityManager, EntityRepository, EntitySchema, IDatabaseDriver, MikroORM } from "@mikro-orm/core"; 2 | import { AggregateRoot } from "@travelhoop/shared-kernel"; 3 | 4 | export interface MikroOrmRepositoryDependencies { 5 | dbConnection: MikroORM>; 6 | entitySchema: EntitySchema; 7 | } 8 | 9 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 10 | export abstract class MikroOrmRepository { 11 | protected readonly repo: EntityRepository; 12 | 13 | protected readonly em: EntityManager>; 14 | 15 | constructor({ dbConnection, entitySchema: entity }: MikroOrmRepositoryDependencies) { 16 | this.em = dbConnection.em; 17 | this.repo = (dbConnection.em.getRepository(entity) as unknown) as EntityRepository; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/shared/infrastructure/src/mikro-orm/types/aggregate-id.ts: -------------------------------------------------------------------------------- 1 | import { Type } from "@mikro-orm/core"; 2 | import { AggregateId } from "@travelhoop/shared-kernel"; 3 | 4 | export class AggregateIdType extends Type { 5 | convertToDatabaseValue(value: AggregateId | undefined): string | undefined { 6 | if (!value) { 7 | return value; 8 | } 9 | 10 | return value.toString(); 11 | } 12 | 13 | convertToJSValue(value: string | undefined): AggregateId | undefined { 14 | if (!value) { 15 | return undefined; 16 | } 17 | 18 | return AggregateId.parse(value); 19 | } 20 | 21 | getColumnType(): string { 22 | return "uuid"; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/shared/infrastructure/src/mikro-orm/types/guid-type.ts: -------------------------------------------------------------------------------- 1 | import { Type } from "@mikro-orm/core"; 2 | import { Guid } from "guid-typescript"; 3 | 4 | export class GuidType extends Type { 5 | convertToDatabaseValue(value: Guid | undefined): string | undefined { 6 | if (!value) { 7 | return value; 8 | } 9 | 10 | return value.toString(); 11 | } 12 | 13 | convertToJSValue(value: string | undefined): Guid | undefined { 14 | if (!value) { 15 | return undefined; 16 | } 17 | 18 | return Guid.parse(value); 19 | } 20 | 21 | getColumnType(): string { 22 | return "uuid"; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/shared/infrastructure/src/mikro-orm/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./guid-type"; 2 | 3 | export * from "./aggregate-id"; 4 | -------------------------------------------------------------------------------- /src/shared/infrastructure/src/module/app-module.factory.ts: -------------------------------------------------------------------------------- 1 | import { Event, EventDispatcher } from "@travelhoop/abstract-core"; 2 | import { AwilixContainer } from "awilix"; 3 | import { Application, Router } from "express"; 4 | import { AppModule, UseDependencies } from ".."; 5 | import { scopePerRequest } from "../container"; 6 | 7 | export interface StandardCreateContainerDependencies extends UseDependencies {} 8 | 9 | export interface StandardAppModuleFactoryDependencies { 10 | basePath: string; 11 | name: string; 12 | createContainer: (deps: StandardCreateContainerDependencies) => AwilixContainer; 13 | } 14 | 15 | const basePathFactory = (basePath: string) => `/${basePath.replace("/", "")}`; 16 | 17 | export const standardAppModuleFactory = ({ 18 | basePath, 19 | name, 20 | createContainer, 21 | }: StandardAppModuleFactoryDependencies): AppModule => { 22 | const path = basePathFactory(basePath); 23 | let container: AwilixContainer; 24 | return { 25 | basePath: path, 26 | path, 27 | name, 28 | use: (app: Application, { dbConnection, redis }: UseDependencies) => { 29 | container = createContainer({ dbConnection, redis }); 30 | app.use(scopePerRequest(path, container)); 31 | app.use(path, container.resolve("router")); 32 | }, 33 | dispatchEvent: async (event: Event): Promise => { 34 | await container.resolve("eventDispatcher").dispatch(event); 35 | }, 36 | }; 37 | }; 38 | -------------------------------------------------------------------------------- /src/shared/infrastructure/src/module/app-module.ts: -------------------------------------------------------------------------------- 1 | import { Application } from "express"; 2 | import { RedisClient as Redis } from "redis"; 3 | import { DbConnection } from ".."; 4 | import { Event } from "@travelhoop/abstract-core"; 5 | 6 | export interface UseDependencies { 7 | dbConnection: DbConnection; 8 | redis: Redis; 9 | } 10 | export interface AppModule { 11 | basePath: string; 12 | name: string; 13 | path: string; 14 | use: (app: Application, deps: UseDependencies) => void; 15 | dispatchEvent(event: Event): Promise; 16 | } 17 | -------------------------------------------------------------------------------- /src/shared/infrastructure/src/module/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./app-module.factory"; 2 | 3 | export * from "./app-module"; 4 | -------------------------------------------------------------------------------- /src/shared/infrastructure/src/redis/redis.queue.ts: -------------------------------------------------------------------------------- 1 | import { QueueClient } from "@travelhoop/abstract-core"; 2 | import { RedisClient as Redis } from "redis"; 3 | import RedisSMQ, { QueueMessage } from "rsmq"; 4 | 5 | interface RedisClientDependencies { 6 | redis: Redis; 7 | } 8 | 9 | export class RedisClient implements QueueClient { 10 | private readonly client: Redis; 11 | 12 | private readonly queue: RedisSMQ; 13 | 14 | constructor({ redis }: RedisClientDependencies) { 15 | this.client = redis; 16 | this.queue = new RedisSMQ({ client: this.client }); 17 | } 18 | 19 | async createQueue(queueName: string): Promise { 20 | await this.queue.createQueueAsync({ qname: queueName }); 21 | } 22 | 23 | async removeQueue(queueName: string): Promise { 24 | await this.queue.deleteQueueAsync({ qname: queueName }); 25 | } 26 | 27 | async getMessage(queueName: string): Promise { 28 | const message = await this.queue.popMessageAsync({ qname: queueName }); 29 | return JSON.parse((message as QueueMessage).message); 30 | } 31 | 32 | async sendMessage(queueName: string, message: string): Promise { 33 | await this.queue.sendMessageAsync({ qname: queueName, message }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/shared/infrastructure/src/typings/global.d.ts: -------------------------------------------------------------------------------- 1 | import { AuthenticatedUser } from "@travelhoop/abstract-core"; 2 | import { AwilixContainer } from "awilix"; 3 | import { DbConnection } from ".."; 4 | 5 | declare type Omit = Pick>; 6 | 7 | declare global { 8 | namespace NodeJS { 9 | interface Global { 10 | container: AwilixContainer; 11 | dbConnection: DbConnection; 12 | } 13 | } 14 | namespace express { 15 | interface Request { 16 | user: AuthenticatedUser; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/shared/infrastructure/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/@travelhoop/toolchain/includes/tsconfig.web.json" 3 | } -------------------------------------------------------------------------------- /src/shared/kernel/.eslintignore: -------------------------------------------------------------------------------- 1 | **/*.d.ts 2 | *.json -------------------------------------------------------------------------------- /src/shared/kernel/.eslintrc.js: -------------------------------------------------------------------------------- 1 | require('@travelhoop/toolchain/patch/modern-module-resolution'); 2 | 3 | module.exports = { 4 | extends: [ "./node_modules/@travelhoop/toolchain/includes/.eslintrc" ], // <---- put your profile string here 5 | parserOptions: { 6 | project: "tsconfig.json", 7 | tsconfigRootDir: __dirname 8 | } 9 | }; -------------------------------------------------------------------------------- /src/shared/kernel/.swcrc: -------------------------------------------------------------------------------- 1 | { 2 | "jsc": { 3 | "parser": { 4 | "syntax": "typescript", 5 | "decorators": true, 6 | "dynamicImport": true 7 | }, 8 | "transform": { 9 | "legacyDecorator": true, 10 | "decoratorMetadata": true 11 | }, 12 | "target": "es2018", 13 | "keepClassNames": true, 14 | "loose": true 15 | }, 16 | "module": { 17 | "type": "commonjs", 18 | "strict": true, 19 | "strictMode": true, 20 | "lazy": false, 21 | "noInterop": false 22 | }, 23 | "sourceMaps": "inline" 24 | } -------------------------------------------------------------------------------- /src/shared/kernel/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@travelhoop/shared-kernel", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main":"./build/index.js", 6 | "typings":"./build/index.d.ts", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "build": "rimraf build && tsc", 10 | "build:watch":"swc src -d build", 11 | "lint": "eslint './**/*.ts'", 12 | "lint:fix": "eslint './**/*.ts' --fix" 13 | }, 14 | "author": "", 15 | "license": "ISC", 16 | "dependencies": { 17 | "@mikro-orm/core": "~4.5.5", 18 | "guid-typescript": "~1.0.9", 19 | "rimraf": "~3.0.2" 20 | }, 21 | "devDependencies": { 22 | "@travelhoop/toolchain":"1.0.0", 23 | "@types/node": "~14.14.37", 24 | "eslint": "~7.23.0", 25 | "typescript": "~4.2.3", 26 | "@swc/core": "~1.2.51", 27 | "@swc/cli": "~0.1.36" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/shared/kernel/src/aggregate/aggregate-id.ts: -------------------------------------------------------------------------------- 1 | import { Guid } from "guid-typescript"; 2 | 3 | export class AggregateId { 4 | private value: string; 5 | 6 | static create(guid?: Guid) { 7 | const id = new AggregateId(); 8 | id.value = guid?.toString() || Guid.create().toString(); 9 | return id; 10 | } 11 | 12 | static parse(guid: string) { 13 | const id = new AggregateId(); 14 | id.value = Guid.parse(guid).toString(); 15 | return id; 16 | } 17 | 18 | static isAggregateId(aggregateId: AggregateId) { 19 | return Guid.isGuid(aggregateId.toString()); 20 | } 21 | 22 | toString() { 23 | return this.value; 24 | } 25 | 26 | toGuid() { 27 | return Guid.parse(this.value); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/shared/kernel/src/aggregate/aggregate-root.ts: -------------------------------------------------------------------------------- 1 | import { AggregateId } from "./aggregate-id"; 2 | import { DomainEvent } from "../domain-event"; 3 | 4 | export interface AggregateRootProps { 5 | id: T; 6 | version: number; 7 | domainEvents: DomainEvent[]; 8 | versionIncremented: boolean; 9 | } 10 | 11 | export class AggregateRoot { 12 | public id: T; 13 | 14 | public version: number = 1; 15 | 16 | private domainEvents: DomainEvent[] = []; 17 | 18 | private versionIncremented: boolean; 19 | 20 | addEvent(event: DomainEvent) { 21 | if (!this.domainEvents) this.domainEvents = []; 22 | 23 | if (!this.domainEvents && !this.versionIncremented) { 24 | this.incrementVersion(); 25 | } 26 | 27 | this.domainEvents.push(event); 28 | } 29 | 30 | clearEvents() { 31 | this.domainEvents = []; 32 | } 33 | 34 | get events(): DomainEvent[] { 35 | return this.domainEvents; 36 | } 37 | 38 | private incrementVersion() { 39 | if (!this.versionIncremented) { 40 | this.version += 1; 41 | this.versionIncremented = true; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/shared/kernel/src/aggregate/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./aggregate-id"; 2 | 3 | export * from "./aggregate-root"; 4 | -------------------------------------------------------------------------------- /src/shared/kernel/src/domain-event/domain-event.dispatcher.ts: -------------------------------------------------------------------------------- 1 | import { DomainEvent } from "./domain.event"; 2 | 3 | export interface DomainEventDispatcher { 4 | dispatch(events: DomainEvent[]): Promise; 5 | dispatch(event: DomainEvent): Promise; 6 | } 7 | -------------------------------------------------------------------------------- /src/shared/kernel/src/domain-event/domain-event.subscriber.ts: -------------------------------------------------------------------------------- 1 | export interface DomainEventSubscribersMeta { 2 | name: string; 3 | method: keyof T & string; 4 | } 5 | 6 | export interface DomainEventSubscriber { 7 | getSubscribedEvents(): DomainEventSubscribersMeta[]; 8 | } 9 | -------------------------------------------------------------------------------- /src/shared/kernel/src/domain-event/domain.event.ts: -------------------------------------------------------------------------------- 1 | export interface DomainEvent { 2 | payload: TPayload; 3 | } 4 | -------------------------------------------------------------------------------- /src/shared/kernel/src/domain-event/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./domain.event"; 2 | 3 | export * from "./domain-event.dispatcher"; 4 | 5 | export * from "./domain-event.subscriber"; 6 | -------------------------------------------------------------------------------- /src/shared/kernel/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./aggregate"; 2 | 3 | export * from "./domain-event"; 4 | -------------------------------------------------------------------------------- /src/shared/kernel/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/@travelhoop/toolchain/includes/tsconfig.web.json" 3 | } -------------------------------------------------------------------------------- /tools/toolchain/.eslintignore: -------------------------------------------------------------------------------- 1 | includes/.eslintrc.js -------------------------------------------------------------------------------- /tools/toolchain/.swcrc: -------------------------------------------------------------------------------- 1 | { 2 | "jsc": { 3 | "parser": { 4 | "syntax": "typescript", 5 | "tsx": false, 6 | "decorators": true, 7 | "dynamicImport": true 8 | }, 9 | "target": "es2019" 10 | }, 11 | "module": { 12 | "type": "commonjs", 13 | "strict": true, 14 | "strictMode": true, 15 | "lazy": true, 16 | "noInterop": false 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tools/toolchain/includes/.eslintrc.js: -------------------------------------------------------------------------------- 1 | require("@rushstack/eslint-patch/modern-module-resolution"); 2 | 3 | module.exports = { 4 | "extends": [ 5 | "airbnb-typescript/base", 6 | "plugin:prettier/recommended", 7 | "prettier/@typescript-eslint" 8 | ], 9 | "env": { 10 | "mocha": true 11 | }, 12 | "plugins": ["prettier", "unicorn", "no-only-tests"], 13 | "parser": "@typescript-eslint/parser", 14 | "parserOptions":{ 15 | "project": "tools/toolchain/includes/tsconfig.web.json" 16 | }, 17 | "rules": { 18 | "no-throw-literal": "off", 19 | "no-console": "error", 20 | "import/prefer-default-export": "off", 21 | "no-empty-pattern": "off", 22 | "import/no-cycle": "off", 23 | "import/no-duplicates": "off", 24 | "no-empty-function": "off", 25 | "no-underscore-dangle": "off", 26 | "no-useless-constructor": "off", 27 | "class-methods-use-this": "off", 28 | "@typescript-eslint/quotes": ["error", "double"], 29 | "no-empty-interface": "off", 30 | "ordered-imports": "off", 31 | "object-literal-sort-keys": "off", 32 | "no-param-reassign": ["error", { "props": true, "ignorePropertyModificationsFor": ["propertyDesciptor", "handlers"]}], 33 | "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }], 34 | "no-only-tests/no-only-tests": "error", 35 | "unicorn/filename-case": [ 36 | "error", 37 | { 38 | "case": "kebabCase" 39 | } 40 | ], 41 | "curly": "error" 42 | }, 43 | "overrides": [ 44 | { 45 | "files": "*.spec.ts", 46 | "rules": { 47 | "no-unused-expressions": "off", 48 | "@typescript-eslint/no-unused-expressions": "off" 49 | } 50 | } 51 | ], 52 | } -------------------------------------------------------------------------------- /tools/toolchain/includes/tsconfig.web.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2019", 4 | "types": ["node"], 5 | "module": "commonjs", 6 | "declaration": true, 7 | "strict": true, 8 | "allowSyntheticDefaultImports": true, 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "strictPropertyInitialization": false, 12 | "sourceMap": true, 13 | "inlineSourceMap": false, 14 | "outDir": "../../../../build", 15 | "typeRoots": ["../../../@types", "../../../@travelhoop/infrastructure/src/typings"], 16 | "resolveJsonModule": true, 17 | "allowJs": true, 18 | "esModuleInterop": true, 19 | }, 20 | "include": [ 21 | "../../../../src/**/*.ts", 22 | ], 23 | "exclude": ["../../../../node_modules", "../../../../build/**", "../../../../*.rest"] 24 | } -------------------------------------------------------------------------------- /tools/toolchain/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@travelhoop/toolchain", 3 | "version": "1.0.0", 4 | "description": "Toolchain used to build projects in this repo", 5 | "license": "MIT", 6 | "scripts": { 7 | "build": "", 8 | "build:watch":"" 9 | }, 10 | "dependencies": { 11 | "colors": "^1.3.2" 12 | }, 13 | "devDependencies": { 14 | "@types/node": "^10.9.4", 15 | "@rushstack/eslint-patch": "~1.0.6", 16 | "@typescript-eslint/eslint-plugin": "~4.20.0", 17 | "@typescript-eslint/parser": "~4.20.0", 18 | "rimraf": "^2.6.2", 19 | "typescript": "~4.2.3", 20 | "eslint": "7.23.0", 21 | "eslint-config-airbnb-typescript": "~12.3.1", 22 | "eslint-config-prettier": "8.1.0", 23 | "eslint-plugin-import": "2.22.1", 24 | "eslint-plugin-no-only-tests": "~2.4.0", 25 | "eslint-plugin-prettier": "~3.3.1", 26 | "eslint-plugin-unicorn": "29.0.0", 27 | "ts-node-dev": "1.1.6", 28 | "prettier": "~2.2.1", 29 | "unicorn": "~0.0.1" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tools/toolchain/patch/modern-module-resolution.js: -------------------------------------------------------------------------------- 1 | require('@rushstack/eslint-patch/modern-module-resolution'); 2 | --------------------------------------------------------------------------------