├── .commitlintrc.mjs ├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── ci.yml │ └── quality.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .lintstagedrc.mjs ├── .npmrc ├── .prettierrc.json ├── LICENSE ├── README.md ├── docs └── images │ └── logoasl.png ├── examples └── banking │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── docker-compose.recreate.yaml │ ├── docker-compose.yaml │ ├── nest-cli.json │ ├── package.json │ ├── src │ ├── account │ │ ├── application │ │ │ ├── command │ │ │ │ ├── create-account.command.ts │ │ │ │ ├── create-account.handler.ts │ │ │ │ ├── create-deposit.command.ts │ │ │ │ ├── create-deposit.handler.ts │ │ │ │ ├── create-widthdrawal.command.ts │ │ │ │ ├── create-widthdrawal.handler.ts │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── query │ │ │ │ ├── get-account.handler.ts │ │ │ │ ├── get-account.query.ts │ │ │ │ ├── get-accounts.handler.ts │ │ │ │ ├── get-accounts.query.ts │ │ │ │ └── index.ts │ │ │ └── services │ │ │ │ ├── account-finder.interface.ts │ │ │ │ └── index.ts │ │ ├── domain │ │ │ ├── event │ │ │ │ ├── account-was-created.event.ts │ │ │ │ ├── deposit-was-done.event.ts │ │ │ │ ├── index.ts │ │ │ │ └── withdrawal-was-done.event.ts │ │ │ ├── exception │ │ │ │ ├── index.ts │ │ │ │ └── invalid-title.ts │ │ │ ├── index.ts │ │ │ └── model │ │ │ │ ├── account-id.ts │ │ │ │ ├── account.ts │ │ │ │ ├── amount.ts │ │ │ │ ├── index.ts │ │ │ │ ├── title.ts │ │ │ │ └── transaction.ts │ │ ├── dto │ │ │ ├── index.ts │ │ │ ├── request │ │ │ │ ├── create-account.dto.ts │ │ │ │ ├── create-transaction.dto.ts │ │ │ │ └── index.ts │ │ │ └── response │ │ │ │ ├── account.dto.ts │ │ │ │ └── index.ts │ │ ├── index.ts │ │ └── infraestructure │ │ │ ├── account.module.ts │ │ │ ├── account.providers.ts │ │ │ ├── controller │ │ │ ├── account.controller.ts │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── read-model │ │ │ ├── accounts │ │ │ │ ├── account-was-created.projection.ts │ │ │ │ ├── account.schema.ts │ │ │ │ ├── deposit-was-done.projection.ts │ │ │ │ ├── index.ts │ │ │ │ └── withdrawal-was-done.projection.ts │ │ │ └── index.ts │ │ │ └── services │ │ │ ├── account-finder.service.ts │ │ │ ├── account.service.ts │ │ │ └── index.ts │ ├── app.module.ts │ ├── console.ts │ ├── main.ts │ ├── user │ │ ├── application │ │ │ ├── command │ │ │ │ ├── create-user.handler.ts │ │ │ │ ├── create-user.query.ts │ │ │ │ ├── delete-user.handler.ts │ │ │ │ ├── delete-user.query.ts │ │ │ │ ├── index.ts │ │ │ │ ├── update-user.handler.ts │ │ │ │ └── update-user.query.ts │ │ │ ├── index.ts │ │ │ ├── query │ │ │ │ ├── get-user.handler.ts │ │ │ │ ├── get-user.query.ts │ │ │ │ ├── get-users.handler.ts │ │ │ │ ├── get-users.query.ts │ │ │ │ └── index.ts │ │ │ └── services │ │ │ │ ├── index.ts │ │ │ │ └── user-finder.interface.ts │ │ ├── domain │ │ │ ├── event │ │ │ │ ├── index.ts │ │ │ │ ├── password-was-updated.event.ts │ │ │ │ ├── user-was-created.event.ts │ │ │ │ └── user-was-deleted.event.ts │ │ │ ├── index.ts │ │ │ └── model │ │ │ │ ├── index.ts │ │ │ │ ├── password.ts │ │ │ │ ├── user-id.ts │ │ │ │ ├── user.ts │ │ │ │ └── username.ts │ │ ├── dto │ │ │ ├── index.ts │ │ │ ├── request │ │ │ │ ├── create-user.dto.ts │ │ │ │ ├── index.ts │ │ │ │ └── update-user.dto.ts │ │ │ └── response │ │ │ │ ├── index.ts │ │ │ │ └── user.dto.ts │ │ ├── index.ts │ │ └── infraestructure │ │ │ ├── controller │ │ │ ├── index.ts │ │ │ └── user.controller.ts │ │ │ ├── index.ts │ │ │ ├── read-model │ │ │ ├── index.ts │ │ │ └── users │ │ │ │ ├── index.ts │ │ │ │ ├── password-was-updated.projection.ts │ │ │ │ ├── user-was-created.projection.ts │ │ │ │ ├── user-was-deleted.projection.ts │ │ │ │ └── user.schema.ts │ │ │ ├── services │ │ │ ├── index.ts │ │ │ ├── user-finder.service.ts │ │ │ └── user.service.ts │ │ │ ├── user.module.ts │ │ │ └── user.providers.ts │ └── utils │ │ ├── catch.error.ts │ │ └── index.ts │ ├── tsconfig.build.json │ └── tsconfig.json ├── package.json ├── packages └── nestjs-eventstore │ ├── .dockerignore │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── src │ ├── aggregate.repository.ts │ ├── crypto │ │ ├── aes │ │ │ ├── create-aes-key.ts │ │ │ ├── decrypt-with-aes-key.ts │ │ │ ├── encrypt-with-aes-key.ts │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── hash │ │ │ ├── generate-hash.ts │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── key.dto.ts │ │ └── key.schema.ts │ ├── decorators │ │ ├── index.ts │ │ └── inject-repository.decorator.ts │ ├── domain │ │ ├── exceptions │ │ │ ├── domain-error.ts │ │ │ ├── id-already-registered.error.ts │ │ │ ├── id-not-found.error.ts │ │ │ ├── index.ts │ │ │ ├── invalid-event-error.ts │ │ │ └── invalid-id-error.ts │ │ ├── index.ts │ │ └── models │ │ │ ├── aggregate-root.ts │ │ │ ├── event.ts │ │ │ ├── id.spec.ts │ │ │ ├── id.ts │ │ │ ├── index.ts │ │ │ ├── value-object.spec.ts │ │ │ └── value-object.ts │ ├── errors │ │ ├── index.ts │ │ ├── key-not-found.error.ts │ │ └── transformer-not-found.error.ts │ ├── eventstore-core.module.ts │ ├── eventstore.cli.ts │ ├── eventstore.config.ts │ ├── eventstore.constants.ts │ ├── eventstore.mapper.ts │ ├── eventstore.module.ts │ ├── eventstore.ts │ ├── index.ts │ ├── interfaces │ │ ├── eventstore-module.interface.ts │ │ ├── index.ts │ │ └── transformer.type.ts │ ├── services │ │ ├── index.ts │ │ ├── key.service.spec.ts │ │ ├── key.service.ts │ │ ├── projections.service.ts │ │ └── transformer.service.ts │ └── utils │ │ ├── index.ts │ │ └── repository.ts │ ├── tsconfig.json │ └── tsup.config.ts ├── pnpm-lock.yaml └── pnpm-workspace.yaml /.commitlintrc.mjs: -------------------------------------------------------------------------------- 1 | export default { extends: ['@commitlint/config-conventional'] } 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "plugin:unicorn/recommended", 4 | "plugin:@typescript-eslint/recommended", 5 | "plugin:prettier/recommended" 6 | ], 7 | "parser": "@typescript-eslint/parser", 8 | "plugins": [ 9 | "@typescript-eslint", 10 | "import", 11 | "simple-import-sort", 12 | "sort", 13 | "unused-imports" 14 | ], 15 | "root": true, 16 | "ignorePatterns": "**/dist/*", 17 | "rules": { 18 | "@typescript-eslint/no-shadow": "error", 19 | "@typescript-eslint/no-unused-vars": [ 20 | "error", 21 | { "ignoreRestSiblings": true } 22 | ], 23 | "default-case-last": "error", 24 | "default-param-last": "error", 25 | "dot-notation": "error", 26 | "eqeqeq": "error", 27 | "import/first": "error", 28 | "import/newline-after-import": "error", 29 | "import/no-duplicates": "error", 30 | "no-shadow": "off", 31 | "simple-import-sort/exports": "error", 32 | "simple-import-sort/imports": "error", 33 | "sort/destructuring-properties": "error", 34 | "sort/object-properties": "error", 35 | "sort/type-properties": "error", 36 | "unicorn/no-null": "off", 37 | "unicorn/no-array-reduce": "off", 38 | "unicorn/no-process-exit": "off", 39 | "unicorn/prevent-abbreviations": "off", 40 | "unused-imports/no-unused-imports": "error" 41 | }, 42 | "overrides": [ 43 | { 44 | "files": "examples/**/*.ts", 45 | "rules": { 46 | "unicorn/prefer-top-level-await": "off" 47 | } 48 | } 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Version [e.g. 22] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | test: 11 | name: 'Test' 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | node-version: [18.x, 20.x] 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v3 19 | 20 | - name: Setup pnpm 21 | uses: pnpm/action-setup@v4.0.0 22 | with: 23 | version: 9.6.0 24 | 25 | - name: Use Node.js ${{ matrix.node-version }} 26 | uses: actions/setup-node@v2 27 | with: 28 | node-version: ${{ matrix.node-version }} 29 | cache: 'pnpm' 30 | 31 | - name: Install dependencies 32 | run: pnpm install --frozen-lockfile --prefer-offline 33 | 34 | - name: Run tests 35 | run: pnpm test -------------------------------------------------------------------------------- /.github/workflows/quality.yml: -------------------------------------------------------------------------------- 1 | name: Quality 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | prettier: 11 | name: 'Prettier' 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v3 16 | 17 | - name: Setup pnpm 18 | uses: pnpm/action-setup@v4.0.0 19 | with: 20 | version: 9.6.0 21 | 22 | - name: Use Node.js 18 23 | uses: actions/setup-node@v3 24 | with: 25 | node-version: '18' 26 | cache: 'pnpm' 27 | 28 | - name: Install dependencies 29 | run: pnpm install --frozen-lockfile --prefer-offline 30 | 31 | - name: Run Prettier check 32 | run: pnpm run prettier-check 33 | 34 | eslint: 35 | name: 'ESLint' 36 | runs-on: ubuntu-latest 37 | steps: 38 | - name: Checkout 39 | uses: actions/checkout@v3 40 | 41 | - name: Setup pnpm 42 | uses: pnpm/action-setup@v4.0.0 43 | with: 44 | version: 9.6.0 45 | 46 | - name: Use Node.js 18 47 | uses: actions/setup-node@v3 48 | with: 49 | node-version: '18' 50 | cache: 'pnpm' 51 | 52 | - name: Install dependencies 53 | run: pnpm install --frozen-lockfile --prefer-offline 54 | 55 | - name: Run ESLint check 56 | run: pnpm run lint 57 | 58 | types: 59 | name: 'TypeScript' 60 | runs-on: ubuntu-latest 61 | steps: 62 | - name: Checkout 63 | uses: actions/checkout@v3 64 | 65 | - name: Setup pnpm 66 | uses: pnpm/action-setup@v4.0.0 67 | with: 68 | version: 9.6.0 69 | 70 | - name: Use Node.js 18 71 | uses: actions/setup-node@v3 72 | with: 73 | node-version: '18' 74 | cache: 'pnpm' 75 | 76 | - name: Install dependencies 77 | run: pnpm install --frozen-lockfile --prefer-offline 78 | 79 | - name: Run TypeScript type check 80 | run: pnpm run type-check -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | npx --no -- commitlint --edit 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx lint-staged 2 | -------------------------------------------------------------------------------- /.lintstagedrc.mjs: -------------------------------------------------------------------------------- 1 | import { relative } from 'path' 2 | 3 | const buildEslintCommand = (filenames) => 4 | `pnpm eslint --fix ${filenames 5 | .map((f) => relative(process.cwd(), f)) 6 | .join(' ')}` 7 | 8 | export default { 9 | '*.{js,jsx,ts,tsx}': [buildEslintCommand], 10 | } 11 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | auto-install-peers = true 2 | strict-peer-dependencies = false 3 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "trailingComma": "all" 5 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | EUROPEAN UNION PUBLIC LICENCE v. 1.2 2 | EUPL © the European Union 2007, 2016 3 | 4 | This European Union Public Licence (the ‘EUPL’) applies to the Work (as defined 5 | below) which is provided under the terms of this Licence. Any use of the Work, 6 | other than as authorised under this Licence is prohibited (to the extent such 7 | use is covered by a right of the copyright holder of the Work). 8 | 9 | The Work is provided under the terms of this Licence when the Licensor (as 10 | defined below) has placed the following notice immediately following the 11 | copyright notice for the Work: 12 | 13 | Licensed under the EUPL 14 | 15 | or has expressed by any other means his willingness to license under the EUPL. 16 | 17 | 1. Definitions 18 | 19 | In this Licence, the following terms have the following meaning: 20 | 21 | - ‘The Licence’: this Licence. 22 | 23 | - ‘The Original Work’: the work or software distributed or communicated by the 24 | Licensor under this Licence, available as Source Code and also as Executable 25 | Code as the case may be. 26 | 27 | - ‘Derivative Works’: the works or software that could be created by the 28 | Licensee, based upon the Original Work or modifications thereof. This Licence 29 | does not define the extent of modification or dependence on the Original Work 30 | required in order to classify a work as a Derivative Work; this extent is 31 | determined by copyright law applicable in the country mentioned in Article 15. 32 | 33 | - ‘The Work’: the Original Work or its Derivative Works. 34 | 35 | - ‘The Source Code’: the human-readable form of the Work which is the most 36 | convenient for people to study and modify. 37 | 38 | - ‘The Executable Code’: any code which has generally been compiled and which is 39 | meant to be interpreted by a computer as a program. 40 | 41 | - ‘The Licensor’: the natural or legal person that distributes or communicates 42 | the Work under the Licence. 43 | 44 | - ‘Contributor(s)’: any natural or legal person who modifies the Work under the 45 | Licence, or otherwise contributes to the creation of a Derivative Work. 46 | 47 | - ‘The Licensee’ or ‘You’: any natural or legal person who makes any usage of 48 | the Work under the terms of the Licence. 49 | 50 | - ‘Distribution’ or ‘Communication’: any act of selling, giving, lending, 51 | renting, distributing, communicating, transmitting, or otherwise making 52 | available, online or offline, copies of the Work or providing access to its 53 | essential functionalities at the disposal of any other natural or legal 54 | person. 55 | 56 | 2. Scope of the rights granted by the Licence 57 | 58 | The Licensor hereby grants You a worldwide, royalty-free, non-exclusive, 59 | sublicensable licence to do the following, for the duration of copyright vested 60 | in the Original Work: 61 | 62 | - use the Work in any circumstance and for all usage, 63 | - reproduce the Work, 64 | - modify the Work, and make Derivative Works based upon the Work, 65 | - communicate to the public, including the right to make available or display 66 | the Work or copies thereof to the public and perform publicly, as the case may 67 | be, the Work, 68 | - distribute the Work or copies thereof, 69 | - lend and rent the Work or copies thereof, 70 | - sublicense rights in the Work or copies thereof. 71 | 72 | Those rights can be exercised on any media, supports and formats, whether now 73 | known or later invented, as far as the applicable law permits so. 74 | 75 | In the countries where moral rights apply, the Licensor waives his right to 76 | exercise his moral right to the extent allowed by law in order to make effective 77 | the licence of the economic rights here above listed. 78 | 79 | The Licensor grants to the Licensee royalty-free, non-exclusive usage rights to 80 | any patents held by the Licensor, to the extent necessary to make use of the 81 | rights granted on the Work under this Licence. 82 | 83 | 3. Communication of the Source Code 84 | 85 | The Licensor may provide the Work either in its Source Code form, or as 86 | Executable Code. If the Work is provided as Executable Code, the Licensor 87 | provides in addition a machine-readable copy of the Source Code of the Work 88 | along with each copy of the Work that the Licensor distributes or indicates, in 89 | a notice following the copyright notice attached to the Work, a repository where 90 | the Source Code is easily and freely accessible for as long as the Licensor 91 | continues to distribute or communicate the Work. 92 | 93 | 4. Limitations on copyright 94 | 95 | Nothing in this Licence is intended to deprive the Licensee of the benefits from 96 | any exception or limitation to the exclusive rights of the rights owners in the 97 | Work, of the exhaustion of those rights or of other applicable limitations 98 | thereto. 99 | 100 | 5. Obligations of the Licensee 101 | 102 | The grant of the rights mentioned above is subject to some restrictions and 103 | obligations imposed on the Licensee. Those obligations are the following: 104 | 105 | Attribution right: The Licensee shall keep intact all copyright, patent or 106 | trademarks notices and all notices that refer to the Licence and to the 107 | disclaimer of warranties. The Licensee must include a copy of such notices and a 108 | copy of the Licence with every copy of the Work he/she distributes or 109 | communicates. The Licensee must cause any Derivative Work to carry prominent 110 | notices stating that the Work has been modified and the date of modification. 111 | 112 | Copyleft clause: If the Licensee distributes or communicates copies of the 113 | Original Works or Derivative Works, this Distribution or Communication will be 114 | done under the terms of this Licence or of a later version of this Licence 115 | unless the Original Work is expressly distributed only under this version of the 116 | Licence — for example by communicating ‘EUPL v. 1.2 only’. The Licensee 117 | (becoming Licensor) cannot offer or impose any additional terms or conditions on 118 | the Work or Derivative Work that alter or restrict the terms of the Licence. 119 | 120 | Compatibility clause: If the Licensee Distributes or Communicates Derivative 121 | Works or copies thereof based upon both the Work and another work licensed under 122 | a Compatible Licence, this Distribution or Communication can be done under the 123 | terms of this Compatible Licence. For the sake of this clause, ‘Compatible 124 | Licence’ refers to the licences listed in the appendix attached to this Licence. 125 | Should the Licensee's obligations under the Compatible Licence conflict with 126 | his/her obligations under this Licence, the obligations of the Compatible 127 | Licence shall prevail. 128 | 129 | Provision of Source Code: When distributing or communicating copies of the Work, 130 | the Licensee will provide a machine-readable copy of the Source Code or indicate 131 | a repository where this Source will be easily and freely available for as long 132 | as the Licensee continues to distribute or communicate the Work. 133 | 134 | Legal Protection: This Licence does not grant permission to use the trade names, 135 | trademarks, service marks, or names of the Licensor, except as required for 136 | reasonable and customary use in describing the origin of the Work and 137 | reproducing the content of the copyright notice. 138 | 139 | 6. Chain of Authorship 140 | 141 | The original Licensor warrants that the copyright in the Original Work granted 142 | hereunder is owned by him/her or licensed to him/her and that he/she has the 143 | power and authority to grant the Licence. 144 | 145 | Each Contributor warrants that the copyright in the modifications he/she brings 146 | to the Work are owned by him/her or licensed to him/her and that he/she has the 147 | power and authority to grant the Licence. 148 | 149 | Each time You accept the Licence, the original Licensor and subsequent 150 | Contributors grant You a licence to their contributions to the Work, under the 151 | terms of this Licence. 152 | 153 | 7. Disclaimer of Warranty 154 | 155 | The Work is a work in progress, which is continuously improved by numerous 156 | Contributors. It is not a finished work and may therefore contain defects or 157 | ‘bugs’ inherent to this type of development. 158 | 159 | For the above reason, the Work is provided under the Licence on an ‘as is’ basis 160 | and without warranties of any kind concerning the Work, including without 161 | limitation merchantability, fitness for a particular purpose, absence of defects 162 | or errors, accuracy, non-infringement of intellectual property rights other than 163 | copyright as stated in Article 6 of this Licence. 164 | 165 | This disclaimer of warranty is an essential part of the Licence and a condition 166 | for the grant of any rights to the Work. 167 | 168 | 8. Disclaimer of Liability 169 | 170 | Except in the cases of wilful misconduct or damages directly caused to natural 171 | persons, the Licensor will in no event be liable for any direct or indirect, 172 | material or moral, damages of any kind, arising out of the Licence or of the use 173 | of the Work, including without limitation, damages for loss of goodwill, work 174 | stoppage, computer failure or malfunction, loss of data or any commercial 175 | damage, even if the Licensor has been advised of the possibility of such damage. 176 | However, the Licensor will be liable under statutory product liability laws as 177 | far such laws apply to the Work. 178 | 179 | 9. Additional agreements 180 | 181 | While distributing the Work, You may choose to conclude an additional agreement, 182 | defining obligations or services consistent with this Licence. However, if 183 | accepting obligations, You may act only on your own behalf and on your sole 184 | responsibility, not on behalf of the original Licensor or any other Contributor, 185 | and only if You agree to indemnify, defend, and hold each Contributor harmless 186 | for any liability incurred by, or claims asserted against such Contributor by 187 | the fact You have accepted any warranty or additional liability. 188 | 189 | 10. Acceptance of the Licence 190 | 191 | The provisions of this Licence can be accepted by clicking on an icon ‘I agree’ 192 | placed under the bottom of a window displaying the text of this Licence or by 193 | affirming consent in any other similar way, in accordance with the rules of 194 | applicable law. Clicking on that icon indicates your clear and irrevocable 195 | acceptance of this Licence and all of its terms and conditions. 196 | 197 | Similarly, you irrevocably accept this Licence and all of its terms and 198 | conditions by exercising any rights granted to You by Article 2 of this Licence, 199 | such as the use of the Work, the creation by You of a Derivative Work or the 200 | Distribution or Communication by You of the Work or copies thereof. 201 | 202 | 11. Information to the public 203 | 204 | In case of any Distribution or Communication of the Work by means of electronic 205 | communication by You (for example, by offering to download the Work from a 206 | remote location) the distribution channel or media (for example, a website) must 207 | at least provide to the public the information requested by the applicable law 208 | regarding the Licensor, the Licence and the way it may be accessible, concluded, 209 | stored and reproduced by the Licensee. 210 | 211 | 12. Termination of the Licence 212 | 213 | The Licence and the rights granted hereunder will terminate automatically upon 214 | any breach by the Licensee of the terms of the Licence. 215 | 216 | Such a termination will not terminate the licences of any person who has 217 | received the Work from the Licensee under the Licence, provided such persons 218 | remain in full compliance with the Licence. 219 | 220 | 13. Miscellaneous 221 | 222 | Without prejudice of Article 9 above, the Licence represents the complete 223 | agreement between the Parties as to the Work. 224 | 225 | If any provision of the Licence is invalid or unenforceable under applicable 226 | law, this will not affect the validity or enforceability of the Licence as a 227 | whole. Such provision will be construed or reformed so as necessary to make it 228 | valid and enforceable. 229 | 230 | The European Commission may publish other linguistic versions or new versions of 231 | this Licence or updated versions of the Appendix, so far this is required and 232 | reasonable, without reducing the scope of the rights granted by the Licence. New 233 | versions of the Licence will be published with a unique version number. 234 | 235 | All linguistic versions of this Licence, approved by the European Commission, 236 | have identical value. Parties can take advantage of the linguistic version of 237 | their choice. 238 | 239 | 14. Jurisdiction 240 | 241 | Without prejudice to specific agreement between parties, 242 | 243 | - any litigation resulting from the interpretation of this License, arising 244 | between the European Union institutions, bodies, offices or agencies, as a 245 | Licensor, and any Licensee, will be subject to the jurisdiction of the Court 246 | of Justice of the European Union, as laid down in article 272 of the Treaty on 247 | the Functioning of the European Union, 248 | 249 | - any litigation arising between other parties and resulting from the 250 | interpretation of this License, will be subject to the exclusive jurisdiction 251 | of the competent court where the Licensor resides or conducts its primary 252 | business. 253 | 254 | 15. Applicable Law 255 | 256 | Without prejudice to specific agreement between parties, 257 | 258 | - this Licence shall be governed by the law of the European Union Member State 259 | where the Licensor has his seat, resides or has his registered office, 260 | 261 | - this licence shall be governed by Belgian law if the Licensor has no seat, 262 | residence or registered office inside a European Union Member State. 263 | 264 | Appendix 265 | 266 | ‘Compatible Licences’ according to Article 5 EUPL are: 267 | 268 | - GNU General Public License (GPL) v. 2, v. 3 269 | - GNU Affero General Public License (AGPL) v. 3 270 | - Open Software License (OSL) v. 2.1, v. 3.0 271 | - Eclipse Public License (EPL) v. 1.0 272 | - CeCILL v. 2.0, v. 2.1 273 | - Mozilla Public Licence (MPL) v. 2 274 | - GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3 275 | - Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for 276 | works other than software 277 | - European Union Public Licence (EUPL) v. 1.1, v. 1.2 278 | - Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong 279 | Reciprocity (LiLiQ-R+). 280 | 281 | The European Commission may update this Appendix to later versions of the above 282 | licences without producing a new version of the EUPL, as long as they provide 283 | the rights granted in Article 2 of this Licence and protect the covered Source 284 | Code from exclusive appropriation. 285 | 286 | All other changes or additions to this Appendix require the production of a new 287 | EUPL version. 288 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EventSource module for NestJS 2 | 3 | [![Contributors][contributors-shield]][contributors-url] 4 | [![Forks][forks-shield]][forks-url] 5 | [![Stargazers][stars-shield]][stars-url] 6 | [![Issues][issues-shield]][issues-url] 7 | [![MIT License][license-shield]][license-url] 8 | 9 | 10 |
11 |

12 | 13 | Aula Software Libre de la UCO 14 | 15 | 16 |

EventSource module for NestJS

17 | 18 |

19 | NestJS module for eventsourcing development with eventstore database 20 |

21 |

22 | 23 | 24 | 25 | ## About The Project 26 | 27 | This module allows you to connect to a [EventstoreDB](https://www.eventstore.com/) to do event sourcing with nestjs. 28 | 29 | **This is a Work In Progress**, not ready to use it in producction. 30 | 31 | ## Getting Started 32 | 33 | WIP 34 | 35 | See [example](./example) 36 | 37 | ### Prerequisites 38 | 39 | You require to have a nestjs project with this modules already installed and loaded: 40 | 41 | - [@nestjs/cqrs](https://www.npmjs.com/package/@nestjs/cqrs) 42 | - [nestjs-console](https://www.npmjs.com/package/nestjs-console) 43 | 44 | ### Installation 45 | 46 | - npm 47 | 48 | npm install @aulasoftwarelibre/nestjs-eventstore 49 | 50 | - pnpm 51 | 52 | npm add @aulasoftwarelibre/nestjs-eventstore 53 | 54 | - yarn 55 | 56 | yarn add @aulasoftwarelibre/nestjs-eventstore 57 | 58 | ## Usage 59 | 60 | ### Loading the module 61 | 62 | ## Contributing 63 | 64 | ## License 65 | 66 | Distributed under the EUPL-1.2 License. See `LICENSE` for more information. 67 | 68 | ## Running the example 69 | 70 | To run the example you will build the library first: 71 | 72 | ```shell 73 | pnpm run build 74 | ``` 75 | 76 | Then go to the example folder an run the containers: 77 | 78 | ```shell 79 | cd examples/banking 80 | docker compose up -d 81 | ``` 82 | 83 | And finally run the project: 84 | 85 | ```shell 86 | pnpm run start:dev 87 | ``` 88 | 89 | You will access to the example application in the next urls: 90 | 91 | - [Swagger API](http://localhost:3000/api/) 92 | - [EventStore Database (Write model)](http://localhost:2113) 93 | - [Mongo Database (Read model)](http://admin:pass@localhost:8081/) 94 | 95 | ## Acknowledgements 96 | 97 | This module was created following next articles: 98 | 99 | - https://medium.com/@qasimsoomro/building-microservices-using-node-js-with-ddd-cqrs-and-event-sourcing-part-1-of-2-52e0dc3d81df 100 | - https://medium.com/@qasimsoomro/building-microservices-using-node-js-with-ddd-cqrs-and-event-sourcing-part-2-of-2-9a5f6708e0f 101 | - https://nordfjord.io/blog/event-sourcing-in-nestjs 102 | 103 | 104 | 105 | 106 | [contributors-shield]: https://img.shields.io/github/contributors/aulasoftwarelibre/nestjs-eventstore.svg?style=for-the-badge 107 | [contributors-url]: https://github.com/aulasoftwarelibre/nestjs-eventstore/graphs/contributors 108 | [forks-shield]: https://img.shields.io/github/forks/aulasoftwarelibre/nestjs-eventstore.svg?style=for-the-badge 109 | [forks-url]: https://github.com/aulasoftwarelibre/nestjs-eventstore/network/members 110 | [stars-shield]: https://img.shields.io/github/stars/aulasoftwarelibre/nestjs-eventstore.svg?style=for-the-badge 111 | [stars-url]: https://github.com/aulasoftwarelibre/nestjs-eventstore/stargazers 112 | [issues-shield]: https://img.shields.io/github/issues/aulasoftwarelibre/nestjs-eventstore.svg?style=for-the-badge 113 | [issues-url]: https://github.com/aulasoftwarelibre/nestjs-eventstore/issues 114 | [license-shield]: https://img.shields.io/github/license/aulasoftwarelibre/nestjs-eventstore.svg?style=for-the-badge 115 | [license-url]: https://github.com/aulasoftwarelibre/nestjs-eventstore/blob/master/LICENSE 116 | -------------------------------------------------------------------------------- /docs/images/logoasl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aulasoftwarelibre/nestjs-eventstore/d46795cc79a9c4ab1eee6adb356ac8c802808f8b/docs/images/logoasl.png -------------------------------------------------------------------------------- /examples/banking/.gitignore: -------------------------------------------------------------------------------- 1 | src/nestjs-eventstore 2 | -------------------------------------------------------------------------------- /examples/banking/LICENSE: -------------------------------------------------------------------------------- 1 | EUROPEAN UNION PUBLIC LICENCE v. 1.2 2 | EUPL © the European Union 2007, 2016 3 | 4 | This European Union Public Licence (the ‘EUPL’) applies to the Work (as defined 5 | below) which is provided under the terms of this Licence. Any use of the Work, 6 | other than as authorised under this Licence is prohibited (to the extent such 7 | use is covered by a right of the copyright holder of the Work). 8 | 9 | The Work is provided under the terms of this Licence when the Licensor (as 10 | defined below) has placed the following notice immediately following the 11 | copyright notice for the Work: 12 | 13 | Licensed under the EUPL 14 | 15 | or has expressed by any other means his willingness to license under the EUPL. 16 | 17 | 1. Definitions 18 | 19 | In this Licence, the following terms have the following meaning: 20 | 21 | - ‘The Licence’: this Licence. 22 | 23 | - ‘The Original Work’: the work or software distributed or communicated by the 24 | Licensor under this Licence, available as Source Code and also as Executable 25 | Code as the case may be. 26 | 27 | - ‘Derivative Works’: the works or software that could be created by the 28 | Licensee, based upon the Original Work or modifications thereof. This Licence 29 | does not define the extent of modification or dependence on the Original Work 30 | required in order to classify a work as a Derivative Work; this extent is 31 | determined by copyright law applicable in the country mentioned in Article 15. 32 | 33 | - ‘The Work’: the Original Work or its Derivative Works. 34 | 35 | - ‘The Source Code’: the human-readable form of the Work which is the most 36 | convenient for people to study and modify. 37 | 38 | - ‘The Executable Code’: any code which has generally been compiled and which is 39 | meant to be interpreted by a computer as a program. 40 | 41 | - ‘The Licensor’: the natural or legal person that distributes or communicates 42 | the Work under the Licence. 43 | 44 | - ‘Contributor(s)’: any natural or legal person who modifies the Work under the 45 | Licence, or otherwise contributes to the creation of a Derivative Work. 46 | 47 | - ‘The Licensee’ or ‘You’: any natural or legal person who makes any usage of 48 | the Work under the terms of the Licence. 49 | 50 | - ‘Distribution’ or ‘Communication’: any act of selling, giving, lending, 51 | renting, distributing, communicating, transmitting, or otherwise making 52 | available, online or offline, copies of the Work or providing access to its 53 | essential functionalities at the disposal of any other natural or legal 54 | person. 55 | 56 | 2. Scope of the rights granted by the Licence 57 | 58 | The Licensor hereby grants You a worldwide, royalty-free, non-exclusive, 59 | sublicensable licence to do the following, for the duration of copyright vested 60 | in the Original Work: 61 | 62 | - use the Work in any circumstance and for all usage, 63 | - reproduce the Work, 64 | - modify the Work, and make Derivative Works based upon the Work, 65 | - communicate to the public, including the right to make available or display 66 | the Work or copies thereof to the public and perform publicly, as the case may 67 | be, the Work, 68 | - distribute the Work or copies thereof, 69 | - lend and rent the Work or copies thereof, 70 | - sublicense rights in the Work or copies thereof. 71 | 72 | Those rights can be exercised on any media, supports and formats, whether now 73 | known or later invented, as far as the applicable law permits so. 74 | 75 | In the countries where moral rights apply, the Licensor waives his right to 76 | exercise his moral right to the extent allowed by law in order to make effective 77 | the licence of the economic rights here above listed. 78 | 79 | The Licensor grants to the Licensee royalty-free, non-exclusive usage rights to 80 | any patents held by the Licensor, to the extent necessary to make use of the 81 | rights granted on the Work under this Licence. 82 | 83 | 3. Communication of the Source Code 84 | 85 | The Licensor may provide the Work either in its Source Code form, or as 86 | Executable Code. If the Work is provided as Executable Code, the Licensor 87 | provides in addition a machine-readable copy of the Source Code of the Work 88 | along with each copy of the Work that the Licensor distributes or indicates, in 89 | a notice following the copyright notice attached to the Work, a repository where 90 | the Source Code is easily and freely accessible for as long as the Licensor 91 | continues to distribute or communicate the Work. 92 | 93 | 4. Limitations on copyright 94 | 95 | Nothing in this Licence is intended to deprive the Licensee of the benefits from 96 | any exception or limitation to the exclusive rights of the rights owners in the 97 | Work, of the exhaustion of those rights or of other applicable limitations 98 | thereto. 99 | 100 | 5. Obligations of the Licensee 101 | 102 | The grant of the rights mentioned above is subject to some restrictions and 103 | obligations imposed on the Licensee. Those obligations are the following: 104 | 105 | Attribution right: The Licensee shall keep intact all copyright, patent or 106 | trademarks notices and all notices that refer to the Licence and to the 107 | disclaimer of warranties. The Licensee must include a copy of such notices and a 108 | copy of the Licence with every copy of the Work he/she distributes or 109 | communicates. The Licensee must cause any Derivative Work to carry prominent 110 | notices stating that the Work has been modified and the date of modification. 111 | 112 | Copyleft clause: If the Licensee distributes or communicates copies of the 113 | Original Works or Derivative Works, this Distribution or Communication will be 114 | done under the terms of this Licence or of a later version of this Licence 115 | unless the Original Work is expressly distributed only under this version of the 116 | Licence — for example by communicating ‘EUPL v. 1.2 only’. The Licensee 117 | (becoming Licensor) cannot offer or impose any additional terms or conditions on 118 | the Work or Derivative Work that alter or restrict the terms of the Licence. 119 | 120 | Compatibility clause: If the Licensee Distributes or Communicates Derivative 121 | Works or copies thereof based upon both the Work and another work licensed under 122 | a Compatible Licence, this Distribution or Communication can be done under the 123 | terms of this Compatible Licence. For the sake of this clause, ‘Compatible 124 | Licence’ refers to the licences listed in the appendix attached to this Licence. 125 | Should the Licensee's obligations under the Compatible Licence conflict with 126 | his/her obligations under this Licence, the obligations of the Compatible 127 | Licence shall prevail. 128 | 129 | Provision of Source Code: When distributing or communicating copies of the Work, 130 | the Licensee will provide a machine-readable copy of the Source Code or indicate 131 | a repository where this Source will be easily and freely available for as long 132 | as the Licensee continues to distribute or communicate the Work. 133 | 134 | Legal Protection: This Licence does not grant permission to use the trade names, 135 | trademarks, service marks, or names of the Licensor, except as required for 136 | reasonable and customary use in describing the origin of the Work and 137 | reproducing the content of the copyright notice. 138 | 139 | 6. Chain of Authorship 140 | 141 | The original Licensor warrants that the copyright in the Original Work granted 142 | hereunder is owned by him/her or licensed to him/her and that he/she has the 143 | power and authority to grant the Licence. 144 | 145 | Each Contributor warrants that the copyright in the modifications he/she brings 146 | to the Work are owned by him/her or licensed to him/her and that he/she has the 147 | power and authority to grant the Licence. 148 | 149 | Each time You accept the Licence, the original Licensor and subsequent 150 | Contributors grant You a licence to their contributions to the Work, under the 151 | terms of this Licence. 152 | 153 | 7. Disclaimer of Warranty 154 | 155 | The Work is a work in progress, which is continuously improved by numerous 156 | Contributors. It is not a finished work and may therefore contain defects or 157 | ‘bugs’ inherent to this type of development. 158 | 159 | For the above reason, the Work is provided under the Licence on an ‘as is’ basis 160 | and without warranties of any kind concerning the Work, including without 161 | limitation merchantability, fitness for a particular purpose, absence of defects 162 | or errors, accuracy, non-infringement of intellectual property rights other than 163 | copyright as stated in Article 6 of this Licence. 164 | 165 | This disclaimer of warranty is an essential part of the Licence and a condition 166 | for the grant of any rights to the Work. 167 | 168 | 8. Disclaimer of Liability 169 | 170 | Except in the cases of wilful misconduct or damages directly caused to natural 171 | persons, the Licensor will in no event be liable for any direct or indirect, 172 | material or moral, damages of any kind, arising out of the Licence or of the use 173 | of the Work, including without limitation, damages for loss of goodwill, work 174 | stoppage, computer failure or malfunction, loss of data or any commercial 175 | damage, even if the Licensor has been advised of the possibility of such damage. 176 | However, the Licensor will be liable under statutory product liability laws as 177 | far such laws apply to the Work. 178 | 179 | 9. Additional agreements 180 | 181 | While distributing the Work, You may choose to conclude an additional agreement, 182 | defining obligations or services consistent with this Licence. However, if 183 | accepting obligations, You may act only on your own behalf and on your sole 184 | responsibility, not on behalf of the original Licensor or any other Contributor, 185 | and only if You agree to indemnify, defend, and hold each Contributor harmless 186 | for any liability incurred by, or claims asserted against such Contributor by 187 | the fact You have accepted any warranty or additional liability. 188 | 189 | 10. Acceptance of the Licence 190 | 191 | The provisions of this Licence can be accepted by clicking on an icon ‘I agree’ 192 | placed under the bottom of a window displaying the text of this Licence or by 193 | affirming consent in any other similar way, in accordance with the rules of 194 | applicable law. Clicking on that icon indicates your clear and irrevocable 195 | acceptance of this Licence and all of its terms and conditions. 196 | 197 | Similarly, you irrevocably accept this Licence and all of its terms and 198 | conditions by exercising any rights granted to You by Article 2 of this Licence, 199 | such as the use of the Work, the creation by You of a Derivative Work or the 200 | Distribution or Communication by You of the Work or copies thereof. 201 | 202 | 11. Information to the public 203 | 204 | In case of any Distribution or Communication of the Work by means of electronic 205 | communication by You (for example, by offering to download the Work from a 206 | remote location) the distribution channel or media (for example, a website) must 207 | at least provide to the public the information requested by the applicable law 208 | regarding the Licensor, the Licence and the way it may be accessible, concluded, 209 | stored and reproduced by the Licensee. 210 | 211 | 12. Termination of the Licence 212 | 213 | The Licence and the rights granted hereunder will terminate automatically upon 214 | any breach by the Licensee of the terms of the Licence. 215 | 216 | Such a termination will not terminate the licences of any person who has 217 | received the Work from the Licensee under the Licence, provided such persons 218 | remain in full compliance with the Licence. 219 | 220 | 13. Miscellaneous 221 | 222 | Without prejudice of Article 9 above, the Licence represents the complete 223 | agreement between the Parties as to the Work. 224 | 225 | If any provision of the Licence is invalid or unenforceable under applicable 226 | law, this will not affect the validity or enforceability of the Licence as a 227 | whole. Such provision will be construed or reformed so as necessary to make it 228 | valid and enforceable. 229 | 230 | The European Commission may publish other linguistic versions or new versions of 231 | this Licence or updated versions of the Appendix, so far this is required and 232 | reasonable, without reducing the scope of the rights granted by the Licence. New 233 | versions of the Licence will be published with a unique version number. 234 | 235 | All linguistic versions of this Licence, approved by the European Commission, 236 | have identical value. Parties can take advantage of the linguistic version of 237 | their choice. 238 | 239 | 14. Jurisdiction 240 | 241 | Without prejudice to specific agreement between parties, 242 | 243 | - any litigation resulting from the interpretation of this License, arising 244 | between the European Union institutions, bodies, offices or agencies, as a 245 | Licensor, and any Licensee, will be subject to the jurisdiction of the Court 246 | of Justice of the European Union, as laid down in article 272 of the Treaty on 247 | the Functioning of the European Union, 248 | 249 | - any litigation arising between other parties and resulting from the 250 | interpretation of this License, will be subject to the exclusive jurisdiction 251 | of the competent court where the Licensor resides or conducts its primary 252 | business. 253 | 254 | 15. Applicable Law 255 | 256 | Without prejudice to specific agreement between parties, 257 | 258 | - this Licence shall be governed by the law of the European Union Member State 259 | where the Licensor has his seat, resides or has his registered office, 260 | 261 | - this licence shall be governed by Belgian law if the Licensor has no seat, 262 | residence or registered office inside a European Union Member State. 263 | 264 | Appendix 265 | 266 | ‘Compatible Licences’ according to Article 5 EUPL are: 267 | 268 | - GNU General Public License (GPL) v. 2, v. 3 269 | - GNU Affero General Public License (AGPL) v. 3 270 | - Open Software License (OSL) v. 2.1, v. 3.0 271 | - Eclipse Public License (EPL) v. 1.0 272 | - CeCILL v. 2.0, v. 2.1 273 | - Mozilla Public Licence (MPL) v. 2 274 | - GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3 275 | - Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for 276 | works other than software 277 | - European Union Public Licence (EUPL) v. 1.1, v. 1.2 278 | - Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong 279 | Reciprocity (LiLiQ-R+). 280 | 281 | The European Commission may update this Appendix to later versions of the above 282 | licences without producing a new version of the EUPL, as long as they provide 283 | the rights granted in Article 2 of this Licence and protect the covered Source 284 | Code from exclusive appropriation. 285 | 286 | All other changes or additions to this Appendix require the production of a new 287 | EUPL version. 288 | -------------------------------------------------------------------------------- /examples/banking/README.md: -------------------------------------------------------------------------------- 1 |

2 | Nest Logo 3 |

4 | 5 | [circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 6 | [circleci-url]: https://circleci.com/gh/nestjs/nest 7 | 8 |

A progressive Node.js framework for building efficient and scalable server-side applications.

9 |

10 | NPM Version 11 | Package License 12 | NPM Downloads 13 | CircleCI 14 | Coverage 15 | Discord 16 | Backers on Open Collective 17 | Sponsors on Open Collective 18 | 19 | Support us 20 | 21 |

22 | 24 | 25 | ## Description 26 | 27 | [Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. 28 | 29 | ## Installation 30 | 31 | ```bash 32 | $ npm install 33 | ``` 34 | 35 | ## Running the app 36 | 37 | ```bash 38 | # development 39 | $ npm run start 40 | 41 | # watch mode 42 | $ npm run start:dev 43 | 44 | # production mode 45 | $ npm run start:prod 46 | ``` 47 | 48 | ## Test 49 | 50 | ```bash 51 | # unit tests 52 | $ npm run test 53 | 54 | # e2e tests 55 | $ npm run test:e2e 56 | 57 | # test coverage 58 | $ npm run test:cov 59 | ``` 60 | 61 | ## Support 62 | 63 | Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). 64 | 65 | ## Stay in touch 66 | 67 | - Author - [Kamil Myśliwiec](https://kamilmysliwiec.com) 68 | - Website - [https://nestjs.com](https://nestjs.com/) 69 | - Twitter - [@nestframework](https://twitter.com/nestframework) 70 | 71 | ## License 72 | 73 | Nest is [MIT licensed](LICENSE). 74 | -------------------------------------------------------------------------------- /examples/banking/docker-compose.recreate.yaml: -------------------------------------------------------------------------------- 1 | version: '3.6' 2 | 3 | services: 4 | app: 5 | command: ['npm', 'run', 'console:dev', 'eventstore:readmodel:restore'] 6 | -------------------------------------------------------------------------------- /examples/banking/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | eventstore.db: 3 | platform: linux/amd64 4 | image: eventstore/eventstore:${EVENSTORE_IMAGE:-lts} 5 | environment: 6 | - EVENTSTORE_CLUSTER_SIZE=1 7 | - EVENTSTORE_RUN_PROJECTIONS=All 8 | - EVENTSTORE_START_STANDARD_PROJECTIONS=true 9 | - EVENTSTORE_INSECURE=true 10 | - EVENTSTORE_ENABLE_EXTERNAL_TCP=true 11 | - EVENTSTORE_ENABLE_ATOM_PUB_OVER_HTTP=true 12 | - DOTNET_EnableWriteXorExecute=0 13 | ports: 14 | - '1113:1113' 15 | - '2113:2113' 16 | volumes: 17 | - type: volume 18 | source: eventstore-volume-data 19 | target: /var/lib/eventstore 20 | - type: volume 21 | source: eventstore-volume-logs 22 | target: /var/log/eventstore 23 | 24 | mongo: 25 | image: mongo:latest 26 | ports: 27 | - 27017:27017 28 | volumes: 29 | - type: volume 30 | source: mongo-volume-data 31 | target: /data/db 32 | 33 | mongo-express: 34 | depends_on: 35 | - mongo 36 | image: mongo-express:latest 37 | ports: 38 | - 8081:8081 39 | 40 | volumes: 41 | eventstore-volume-data: 42 | eventstore-volume-logs: 43 | mongo-volume-data: 44 | mongoclient-volume-data: 45 | -------------------------------------------------------------------------------- /examples/banking/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src", 4 | "compilerOptions": { 5 | "builder": "swc", 6 | "typeCheck": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/banking/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "banking", 3 | "version": "0.0.0", 4 | "author": "Sergio Gómez ", 5 | "private": true, 6 | "license": "EUPL-1.2", 7 | "scripts": { 8 | "prebuild": "rimraf dist", 9 | "build": "nest build", 10 | "prettier-check": "prettier --check \"./**/*.ts*\"", 11 | "start": "nest start", 12 | "start:dev": "nest start --watch", 13 | "start:debug": "nest start --debug --watch", 14 | "start:prod": "node dist/main", 15 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 16 | "console:dev": "nest start --entryFile console.js", 17 | "console": "node dist/console.js" 18 | }, 19 | "dependencies": { 20 | "@aulasoftwarelibre/nestjs-eventstore": "workspace:*", 21 | "@eventstore/db-client": "^6.2.1", 22 | "@nestjs/common": "^10.0.4", 23 | "@nestjs/config": "^3.0.0", 24 | "@nestjs/core": "^10.0.4", 25 | "@nestjs/cqrs": "^10.0.1", 26 | "@nestjs/mongoose": "^10.0.10", 27 | "@nestjs/platform-express": "^10.0.4", 28 | "@nestjs/swagger": "^7.0.12", 29 | "class-transformer": "^0.5.1", 30 | "class-validator": "^0.14.0", 31 | "clone": "^2.1.2", 32 | "mongoose": "^8.5.2", 33 | "nest-commander": "^3.11.0", 34 | "reflect-metadata": "^0.1.13", 35 | "rimraf": "^5.0.1", 36 | "rxjs": "^7.8.1", 37 | "shallow-equal-object": "^1.1.1", 38 | "swagger-ui-express": "^4.6.3", 39 | "uuid": "^9.0.0" 40 | }, 41 | "devDependencies": { 42 | "@nestjs/cli": "^10.1.0", 43 | "@nestjs/schematics": "^10.0.1", 44 | "@nestjs/testing": "^10.0.4", 45 | "@types/express": "^4.17.17", 46 | "@types/jest": "^29.5.2", 47 | "@types/node": "^20.3.3", 48 | "@types/supertest": "^2.0.12", 49 | "@typescript-eslint/eslint-plugin": "^5.60.1", 50 | "@typescript-eslint/parser": "^5.60.1", 51 | "supertest": "^6.3.3", 52 | "ts-loader": "^9.4.4", 53 | "ts-node": "^10.9.1", 54 | "tsconfig-paths": "^4.2.0", 55 | "typescript": "^5.1.6" 56 | }, 57 | "peerDependencies": { 58 | "@nestjs/common": "^10.0.0", 59 | "@nestjs/config": "^3.0.0", 60 | "@nestjs/core": "^10.0.0", 61 | "@nestjs/cqrs": "^10.0.0", 62 | "@nestjs/mongoose": "^10.0.0", 63 | "mongoose": "^7.3.0", 64 | "nest-commander": "^3.11.0", 65 | "reflect-metadata": "^0.1.13", 66 | "rxjs": "^7.0.0" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /examples/banking/src/account/application/command/create-account.command.ts: -------------------------------------------------------------------------------- 1 | import { ICommand } from '@nestjs/cqrs' 2 | 3 | export class CreateAccountCommand implements ICommand { 4 | constructor( 5 | public readonly id: string, 6 | public readonly title: string, 7 | ) {} 8 | } 9 | -------------------------------------------------------------------------------- /examples/banking/src/account/application/command/create-account.handler.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AggregateRepository, 3 | IdAlreadyRegisteredError, 4 | InjectAggregateRepository, 5 | } from '@aulasoftwarelibre/nestjs-eventstore' 6 | import { CommandHandler, ICommandHandler } from '@nestjs/cqrs' 7 | 8 | import { Account, AccountId } from '../../domain' 9 | import { Title } from '../../domain/model/title' 10 | import { CreateAccountCommand } from './create-account.command' 11 | 12 | @CommandHandler(CreateAccountCommand) 13 | export class CreateAccountHandler 14 | implements ICommandHandler 15 | { 16 | constructor( 17 | @InjectAggregateRepository(Account) 18 | private readonly accounts: AggregateRepository, 19 | ) {} 20 | 21 | async execute(command: CreateAccountCommand) { 22 | const accountId = AccountId.with(command.id) 23 | const title = Title.with(command.title) 24 | 25 | if ((await this.accounts.find(accountId)) instanceof Account) { 26 | throw IdAlreadyRegisteredError.withId(accountId) 27 | } 28 | 29 | const account = Account.add(accountId, title) 30 | 31 | this.accounts.save(account) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /examples/banking/src/account/application/command/create-deposit.command.ts: -------------------------------------------------------------------------------- 1 | export class CreateDepositCommand { 2 | constructor( 3 | public readonly accountId: string, 4 | public readonly value: number, 5 | public readonly date: Date, 6 | ) {} 7 | } 8 | -------------------------------------------------------------------------------- /examples/banking/src/account/application/command/create-deposit.handler.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AggregateRepository, 3 | InjectAggregateRepository, 4 | } from '@aulasoftwarelibre/nestjs-eventstore' 5 | import { CommandHandler, ICommandHandler } from '@nestjs/cqrs' 6 | 7 | import { Account, AccountId, Amount } from '../../domain' 8 | import { CreateDepositCommand } from './create-deposit.command' 9 | 10 | @CommandHandler(CreateDepositCommand) 11 | export class CreateDepositHandler 12 | implements ICommandHandler 13 | { 14 | constructor( 15 | @InjectAggregateRepository(Account) 16 | private readonly accounts: AggregateRepository, 17 | ) {} 18 | 19 | async execute(command: CreateDepositCommand) { 20 | const accountId = AccountId.with(command.accountId) 21 | const value = Amount.with(command.value) 22 | const date = command.date 23 | 24 | const account = await this.accounts.find(accountId) 25 | console.debug('account', account) 26 | 27 | if (false === account instanceof Account) { 28 | throw new TypeError(`Account ${command.accountId} not found`) 29 | } 30 | 31 | account.deposit(value, date) 32 | 33 | this.accounts.save(account) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /examples/banking/src/account/application/command/create-widthdrawal.command.ts: -------------------------------------------------------------------------------- 1 | export class CreateWidthdrawalCommand { 2 | constructor( 3 | public readonly accountId: string, 4 | public readonly value: number, 5 | public readonly date: Date, 6 | ) {} 7 | } 8 | -------------------------------------------------------------------------------- /examples/banking/src/account/application/command/create-widthdrawal.handler.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AggregateRepository, 3 | InjectAggregateRepository, 4 | } from '@aulasoftwarelibre/nestjs-eventstore' 5 | import { CommandHandler, ICommandHandler } from '@nestjs/cqrs' 6 | 7 | import { Account, AccountId, Amount } from '../../domain' 8 | import { CreateWidthdrawalCommand } from './create-widthdrawal.command' 9 | 10 | @CommandHandler(CreateWidthdrawalCommand) 11 | export class CreateWidthdrawalHandler 12 | implements ICommandHandler 13 | { 14 | constructor( 15 | @InjectAggregateRepository(Account) 16 | private readonly accounts: AggregateRepository, 17 | ) {} 18 | 19 | async execute(command: CreateWidthdrawalCommand) { 20 | const accountId = AccountId.with(command.accountId) 21 | const value = Amount.with(command.value) 22 | const date = command.date 23 | 24 | const account = await this.accounts.find(accountId) 25 | 26 | if (false === account instanceof Account) { 27 | throw new TypeError(`Account ${command.accountId} not found`) 28 | } 29 | 30 | account.widthdrawal(value, date) 31 | 32 | this.accounts.save(account) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/banking/src/account/application/command/index.ts: -------------------------------------------------------------------------------- 1 | import { CreateAccountHandler } from './create-account.handler' 2 | import { CreateDepositHandler } from './create-deposit.handler' 3 | import { CreateWidthdrawalHandler } from './create-widthdrawal.handler' 4 | 5 | export * from './create-account.command' 6 | export * from './create-deposit.command' 7 | export * from './create-widthdrawal.command' 8 | 9 | export const commandHandlers = [ 10 | CreateAccountHandler, 11 | CreateDepositHandler, 12 | CreateWidthdrawalHandler, 13 | ] 14 | -------------------------------------------------------------------------------- /examples/banking/src/account/application/index.ts: -------------------------------------------------------------------------------- 1 | export * from './command' 2 | export * from './query' 3 | export * from './services' 4 | -------------------------------------------------------------------------------- /examples/banking/src/account/application/query/get-account.handler.ts: -------------------------------------------------------------------------------- 1 | import { IdNotFoundError } from '@aulasoftwarelibre/nestjs-eventstore' 2 | import { Inject } from '@nestjs/common' 3 | import { IQueryHandler, QueryHandler } from '@nestjs/cqrs' 4 | 5 | import { AccountId } from '../../domain' 6 | import { AccountDto } from '../../dto' 7 | import { ACCOUNT_FINDER, IAccountFinder } from '../services' 8 | import { GetAccountQuery } from './get-account.query' 9 | 10 | @QueryHandler(GetAccountQuery) 11 | export class GetAccountHandler implements IQueryHandler { 12 | constructor( 13 | @Inject(ACCOUNT_FINDER) private readonly finder: IAccountFinder, 14 | ) {} 15 | 16 | async execute(query: GetAccountQuery): Promise { 17 | const accountId = AccountId.with(query.id) 18 | 19 | const account = await this.finder.find(accountId) 20 | 21 | if (!account) { 22 | throw IdNotFoundError.withId(accountId) 23 | } 24 | 25 | return account 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /examples/banking/src/account/application/query/get-account.query.ts: -------------------------------------------------------------------------------- 1 | export class GetAccountQuery { 2 | constructor(public readonly id: string) {} 3 | } 4 | -------------------------------------------------------------------------------- /examples/banking/src/account/application/query/get-accounts.handler.ts: -------------------------------------------------------------------------------- 1 | import { Inject } from '@nestjs/common' 2 | import { IQueryHandler, QueryHandler } from '@nestjs/cqrs' 3 | 4 | import { AccountDto } from '../../dto' 5 | import { ACCOUNT_FINDER, IAccountFinder } from '../services' 6 | import { GetAccountsQuery } from './get-accounts.query' 7 | 8 | @QueryHandler(GetAccountsQuery) 9 | export class GetAccountsHandler implements IQueryHandler { 10 | constructor( 11 | @Inject(ACCOUNT_FINDER) private readonly finder: IAccountFinder, 12 | ) {} 13 | 14 | async execute(): Promise { 15 | return this.finder.findAll() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/banking/src/account/application/query/get-accounts.query.ts: -------------------------------------------------------------------------------- 1 | export class GetAccountsQuery {} 2 | -------------------------------------------------------------------------------- /examples/banking/src/account/application/query/index.ts: -------------------------------------------------------------------------------- 1 | import { GetAccountHandler } from './get-account.handler' 2 | import { GetAccountsHandler } from './get-accounts.handler' 3 | 4 | export * from './get-account.query' 5 | export * from './get-accounts.query' 6 | 7 | export const queryHandlers = [GetAccountsHandler, GetAccountHandler] 8 | -------------------------------------------------------------------------------- /examples/banking/src/account/application/services/account-finder.interface.ts: -------------------------------------------------------------------------------- 1 | import { AccountId } from '../../domain' 2 | import { AccountDto } from '../../dto' 3 | 4 | export const ACCOUNT_FINDER = 'ACCOUNT_FINDER' 5 | 6 | export interface IAccountFinder { 7 | find(id: AccountId): Promise 8 | findAll(): Promise 9 | } 10 | -------------------------------------------------------------------------------- /examples/banking/src/account/application/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './account-finder.interface' 2 | -------------------------------------------------------------------------------- /examples/banking/src/account/domain/event/account-was-created.event.ts: -------------------------------------------------------------------------------- 1 | import { Event } from '@aulasoftwarelibre/nestjs-eventstore' 2 | 3 | import { CreateAccountDto } from '../../dto/request' 4 | 5 | export class AccountWasCreated extends Event { 6 | constructor( 7 | public readonly id: string, 8 | public readonly title: string, 9 | ) { 10 | super(id, { _id: id, title }) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/banking/src/account/domain/event/deposit-was-done.event.ts: -------------------------------------------------------------------------------- 1 | import { Event } from '@aulasoftwarelibre/nestjs-eventstore' 2 | 3 | import { CreateTransactionDto } from '../../dto/request' 4 | 5 | export class DepositWasDone extends Event { 6 | constructor( 7 | public readonly id: string, 8 | public readonly value: number, 9 | public readonly date: Date, 10 | ) { 11 | super(id, { _id: id, date, value }) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/banking/src/account/domain/event/index.ts: -------------------------------------------------------------------------------- 1 | export * from './account-was-created.event' 2 | export * from './deposit-was-done.event' 3 | export * from './withdrawal-was-done.event' 4 | -------------------------------------------------------------------------------- /examples/banking/src/account/domain/event/withdrawal-was-done.event.ts: -------------------------------------------------------------------------------- 1 | import { Event } from '@aulasoftwarelibre/nestjs-eventstore' 2 | 3 | import { CreateTransactionDto } from '../../dto/request' 4 | 5 | export class WithdrawalWasDone extends Event { 6 | constructor( 7 | public readonly id: string, 8 | public readonly value: number, 9 | public readonly date: Date, 10 | ) { 11 | super(id, { _id: id, date, value }) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/banking/src/account/domain/exception/index.ts: -------------------------------------------------------------------------------- 1 | export * from './invalid-title' 2 | -------------------------------------------------------------------------------- /examples/banking/src/account/domain/exception/invalid-title.ts: -------------------------------------------------------------------------------- 1 | import { DomainError } from '@aulasoftwarelibre/nestjs-eventstore' 2 | 3 | export class InvalidTitleError extends DomainError { 4 | private constructor(stack?: string) { 5 | super(stack) 6 | } 7 | 8 | public static becauseEmpty(): InvalidTitleError { 9 | return new InvalidTitleError(`Title cannot be empty`) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/banking/src/account/domain/index.ts: -------------------------------------------------------------------------------- 1 | export * from './event' 2 | export * from './exception' 3 | export * from './model' 4 | -------------------------------------------------------------------------------- /examples/banking/src/account/domain/model/account-id.ts: -------------------------------------------------------------------------------- 1 | import { Id } from '@aulasoftwarelibre/nestjs-eventstore' 2 | 3 | export class AccountId extends Id { 4 | public static with(id: string): AccountId { 5 | return new AccountId(id) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /examples/banking/src/account/domain/model/account.ts: -------------------------------------------------------------------------------- 1 | import { AggregateRoot } from '@aulasoftwarelibre/nestjs-eventstore' 2 | 3 | import { AccountWasCreated, DepositWasDone, WithdrawalWasDone } from '../event' 4 | import { AccountId } from './account-id' 5 | import { Amount } from './amount' 6 | import { Title } from './title' 7 | import { Transaction } from './transaction' 8 | 9 | export class Account extends AggregateRoot { 10 | private _accountId: AccountId 11 | private _title: Title 12 | private _transactions: Transaction[] 13 | 14 | public static add(accountId: AccountId, title: Title): Account { 15 | const account = new Account() 16 | 17 | account.apply(new AccountWasCreated(accountId.value, title.value)) 18 | 19 | return account 20 | } 21 | 22 | aggregateId(): string { 23 | return this.id.value 24 | } 25 | 26 | get id(): AccountId { 27 | return this._accountId 28 | } 29 | 30 | get title(): Title { 31 | return this._title 32 | } 33 | 34 | public deposit(amount: Amount, date: Date) { 35 | this.apply(new DepositWasDone(this._accountId.value, amount.value, date)) 36 | } 37 | 38 | public widthdrawal(amount: Amount, date: Date) { 39 | this.apply(new WithdrawalWasDone(this._accountId.value, amount.value, date)) 40 | } 41 | 42 | private onAccountWasCreated(event: AccountWasCreated) { 43 | this._accountId = AccountId.with(event.id) 44 | this._title = Title.with(event.title) 45 | this._transactions = [] 46 | } 47 | 48 | private onDepositWasDone(event: DepositWasDone) { 49 | this._transactions.push( 50 | Transaction.with(Amount.with(event.value), event.date), 51 | ) 52 | } 53 | 54 | private onWithdrawalWasDone(event: WithdrawalWasDone) { 55 | this._transactions.push( 56 | Transaction.with(Amount.with(event.value).negative(), event.date), 57 | ) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /examples/banking/src/account/domain/model/amount.ts: -------------------------------------------------------------------------------- 1 | import { ValueObject } from '@aulasoftwarelibre/nestjs-eventstore' 2 | 3 | export class Amount extends ValueObject<{ 4 | value: number 5 | }> { 6 | public static with(value: number): Amount { 7 | return new Amount({ value }) 8 | } 9 | 10 | get value(): number { 11 | return this.props.value 12 | } 13 | 14 | public negative(): Amount { 15 | return Amount.with(-this.value) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/banking/src/account/domain/model/index.ts: -------------------------------------------------------------------------------- 1 | export * from './account' 2 | export * from './account-id' 3 | export * from './amount' 4 | export * from './transaction' 5 | -------------------------------------------------------------------------------- /examples/banking/src/account/domain/model/title.ts: -------------------------------------------------------------------------------- 1 | import { ValueObject } from '@aulasoftwarelibre/nestjs-eventstore' 2 | 3 | import { InvalidTitleError } from '../exception' 4 | 5 | export class Title extends ValueObject<{ 6 | value: string 7 | }> { 8 | public static with(value: string) { 9 | if (value.length === 0) { 10 | throw InvalidTitleError.becauseEmpty() 11 | } 12 | 13 | return new Title({ value }) 14 | } 15 | 16 | get value(): string { 17 | return this.props.value 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/banking/src/account/domain/model/transaction.ts: -------------------------------------------------------------------------------- 1 | import { ValueObject } from '@aulasoftwarelibre/nestjs-eventstore' 2 | 3 | import { Amount } from './amount' 4 | 5 | export class Transaction extends ValueObject<{ 6 | date: Date 7 | value: Amount 8 | }> { 9 | public static with(value: Amount, date: Date) { 10 | return new Transaction({ date, value }) 11 | } 12 | 13 | get value(): Amount { 14 | return this.props.value 15 | } 16 | 17 | get date(): Date { 18 | return this.props.date 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/banking/src/account/dto/index.ts: -------------------------------------------------------------------------------- 1 | export * from './request' 2 | export * from './response' 3 | -------------------------------------------------------------------------------- /examples/banking/src/account/dto/request/create-account.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger' 2 | import { IsNotEmpty, IsUUID } from 'class-validator' 3 | 4 | export class CreateAccountDto { 5 | @ApiProperty() 6 | @IsNotEmpty() 7 | @IsUUID(4) 8 | _id: string 9 | 10 | @ApiProperty() 11 | @IsNotEmpty() 12 | title: string 13 | 14 | constructor(_id: string, title: string) { 15 | this._id = _id 16 | this.title = title 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/banking/src/account/dto/request/create-transaction.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger' 2 | import { IsNotEmpty, Min } from 'class-validator' 3 | 4 | export class CreateTransactionDto { 5 | @ApiProperty({ required: false }) 6 | _id: string 7 | 8 | @ApiProperty({ 9 | minimum: 1, 10 | }) 11 | @IsNotEmpty() 12 | @Min(1) 13 | value: number 14 | 15 | @ApiProperty() 16 | @IsNotEmpty() 17 | date: Date 18 | 19 | constructor(_id: string, value: number, date: Date) { 20 | this._id = _id 21 | this.value = value 22 | this.date = date 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/banking/src/account/dto/request/index.ts: -------------------------------------------------------------------------------- 1 | export * from './create-account.dto' 2 | export * from './create-transaction.dto' 3 | -------------------------------------------------------------------------------- /examples/banking/src/account/dto/response/account.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger' 2 | 3 | export class AccountDto { 4 | @ApiProperty() 5 | _id: string 6 | 7 | @ApiProperty() 8 | title: string 9 | 10 | @ApiProperty() 11 | balance: number 12 | 13 | constructor(_id: string, title: string, balance: number) { 14 | this._id = _id 15 | this.title = title 16 | this.balance = balance 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/banking/src/account/dto/response/index.ts: -------------------------------------------------------------------------------- 1 | export * from './account.dto' 2 | -------------------------------------------------------------------------------- /examples/banking/src/account/index.ts: -------------------------------------------------------------------------------- 1 | export * from './infraestructure' 2 | -------------------------------------------------------------------------------- /examples/banking/src/account/infraestructure/account.module.ts: -------------------------------------------------------------------------------- 1 | import { Event, EventStoreModule } from '@aulasoftwarelibre/nestjs-eventstore' 2 | import { Module } from '@nestjs/common' 3 | import { CqrsModule } from '@nestjs/cqrs' 4 | import { MongooseModule } from '@nestjs/mongoose' 5 | 6 | import { commandHandlers, queryHandlers } from '../application' 7 | import { 8 | Account, 9 | AccountWasCreated, 10 | DepositWasDone, 11 | WithdrawalWasDone, 12 | } from '../domain' 13 | import { CreateAccountDto, CreateTransactionDto } from '../dto/request' 14 | import { accountProviders } from './account.providers' 15 | import { AccountController } from './controller' 16 | import { 17 | ACCOUNTS_PROJECTION, 18 | AccountSchema, 19 | projectionHandlers, 20 | } from './read-model' 21 | import { AccountService } from './services/account.service' 22 | 23 | @Module({ 24 | controllers: [AccountController], 25 | imports: [ 26 | CqrsModule, 27 | EventStoreModule.forFeature([Account], { 28 | AccountWasCreated: (event: Event) => 29 | new AccountWasCreated(event.payload._id, event.payload.title), 30 | DepositWasDone: (event: Event) => 31 | new DepositWasDone( 32 | event.payload._id, 33 | event.payload.value, 34 | event.payload.date, 35 | ), 36 | WithdrawalWasDone: (event: Event) => 37 | new WithdrawalWasDone( 38 | event.payload._id, 39 | event.payload.value, 40 | event.payload.date, 41 | ), 42 | }), 43 | MongooseModule.forFeature([ 44 | { 45 | name: ACCOUNTS_PROJECTION, 46 | schema: AccountSchema, 47 | }, 48 | ]), 49 | ], 50 | providers: [ 51 | ...commandHandlers, 52 | ...queryHandlers, 53 | ...projectionHandlers, 54 | ...accountProviders, 55 | AccountService, 56 | ], 57 | }) 58 | export class AccountModule {} 59 | -------------------------------------------------------------------------------- /examples/banking/src/account/infraestructure/account.providers.ts: -------------------------------------------------------------------------------- 1 | import { ACCOUNT_FINDER } from '../application' 2 | import { AccountFinder } from './services' 3 | 4 | export const accountProviders = [ 5 | { 6 | provide: ACCOUNT_FINDER, 7 | useClass: AccountFinder, 8 | }, 9 | ] 10 | -------------------------------------------------------------------------------- /examples/banking/src/account/infraestructure/controller/account.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IdAlreadyRegisteredError, 3 | IdNotFoundError, 4 | } from '@aulasoftwarelibre/nestjs-eventstore' 5 | import { 6 | Body, 7 | ConflictException, 8 | Controller, 9 | Get, 10 | NotFoundException, 11 | Param, 12 | Post, 13 | } from '@nestjs/common' 14 | import { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger' 15 | 16 | import { catchError } from '../../../utils' 17 | import { AccountDto, CreateAccountDto, CreateTransactionDto } from '../../dto' 18 | import { AccountService } from '../services' 19 | 20 | @ApiTags('Accounts') 21 | @Controller('accounts') 22 | export class AccountController { 23 | constructor(private readonly accountService: AccountService) {} 24 | 25 | @ApiOperation({ summary: 'Get accounts' }) 26 | @ApiOkResponse() 27 | @Get() 28 | async getAccounts(): Promise { 29 | return await this.accountService.getAccounts() 30 | } 31 | 32 | @ApiOperation({ summary: 'Create account' }) 33 | @ApiOkResponse({ type: AccountDto }) 34 | @Post() 35 | async createAccount( 36 | @Body() accountDto: CreateAccountDto, 37 | ): Promise { 38 | try { 39 | return await this.accountService.createAccount(accountDto) 40 | } catch (error) { 41 | const error_ = 42 | error instanceof IdAlreadyRegisteredError 43 | ? new ConflictException(error.message) 44 | : catchError(error) 45 | throw error_ 46 | } 47 | } 48 | 49 | @ApiOperation({ summary: 'Get account' }) 50 | @ApiOkResponse({ type: AccountDto }) 51 | @Get(':id') 52 | async getAccount(@Param('id') id: string): Promise { 53 | try { 54 | return await this.accountService.getAccount(id) 55 | } catch (error) { 56 | const error_ = 57 | error instanceof IdNotFoundError 58 | ? new NotFoundException('Account not found') 59 | : catchError(error) 60 | throw error_ 61 | } 62 | } 63 | 64 | @ApiOperation({ summary: 'Create deposit' }) 65 | @Post(':id/deposit') 66 | async createDeposit( 67 | @Body() transactionDto: CreateTransactionDto, 68 | @Param('id') id: string, 69 | ) { 70 | try { 71 | return await this.accountService.createDeposit(id, transactionDto) 72 | } catch (error) { 73 | const error_ = 74 | error instanceof IdNotFoundError 75 | ? new NotFoundException('Scope not found') 76 | : catchError(error) 77 | throw error_ 78 | } 79 | } 80 | 81 | @ApiOperation({ summary: 'Create withdrawal' }) 82 | @Post(':id/withdrawal') 83 | async createWithdrawal( 84 | @Body() transactionDto: CreateTransactionDto, 85 | @Param('id') id: string, 86 | ) { 87 | try { 88 | return await this.accountService.createWithdrawal(id, transactionDto) 89 | } catch (error) { 90 | const error_ = 91 | error instanceof IdNotFoundError 92 | ? new NotFoundException('Account not found') 93 | : catchError(error) 94 | throw error_ 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /examples/banking/src/account/infraestructure/controller/index.ts: -------------------------------------------------------------------------------- 1 | export * from './account.controller' 2 | -------------------------------------------------------------------------------- /examples/banking/src/account/infraestructure/index.ts: -------------------------------------------------------------------------------- 1 | export * from './account.module' 2 | -------------------------------------------------------------------------------- /examples/banking/src/account/infraestructure/read-model/accounts/account-was-created.projection.ts: -------------------------------------------------------------------------------- 1 | import { EventsHandler, IEventHandler } from '@nestjs/cqrs' 2 | import { InjectModel } from '@nestjs/mongoose' 3 | import { Model } from 'mongoose' 4 | 5 | import { AccountWasCreated } from '../../../domain' 6 | import { AccountDocument, ACCOUNTS_PROJECTION } from './account.schema' 7 | 8 | @EventsHandler(AccountWasCreated) 9 | export class AccountWasCreatedProjection 10 | implements IEventHandler 11 | { 12 | constructor( 13 | @InjectModel(ACCOUNTS_PROJECTION) 14 | private readonly accounts: Model, 15 | ) {} 16 | 17 | async handle(event: AccountWasCreated) { 18 | const account = new this.accounts({ ...event.payload, balance: 0 }) 19 | 20 | await account.save() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/banking/src/account/infraestructure/read-model/accounts/account.schema.ts: -------------------------------------------------------------------------------- 1 | import { Document, Schema } from 'mongoose' 2 | 3 | import { AccountDto } from '../../../dto' 4 | 5 | export const ACCOUNTS_PROJECTION = 'accounts' 6 | 7 | export type AccountDocument = AccountDto & Document 8 | 9 | export const AccountSchema = new Schema( 10 | { 11 | _id: String, 12 | balance: Number, 13 | title: String, 14 | }, 15 | { 16 | versionKey: false, 17 | }, 18 | ) 19 | -------------------------------------------------------------------------------- /examples/banking/src/account/infraestructure/read-model/accounts/deposit-was-done.projection.ts: -------------------------------------------------------------------------------- 1 | import { EventsHandler, IEventHandler } from '@nestjs/cqrs' 2 | import { InjectModel } from '@nestjs/mongoose' 3 | import { Model } from 'mongoose' 4 | 5 | import { DepositWasDone } from '../../../domain' 6 | import { AccountDocument, ACCOUNTS_PROJECTION } from './account.schema' 7 | 8 | @EventsHandler(DepositWasDone) 9 | export class DepositWasDoneProjection implements IEventHandler { 10 | constructor( 11 | @InjectModel(ACCOUNTS_PROJECTION) 12 | private readonly accounts: Model, 13 | ) {} 14 | 15 | async handle(event: DepositWasDone) { 16 | await this.accounts 17 | .updateOne({ _id: event.id }, { $inc: { balance: event.value } }) 18 | .exec() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/banking/src/account/infraestructure/read-model/accounts/index.ts: -------------------------------------------------------------------------------- 1 | import { AccountWasCreatedProjection } from './account-was-created.projection' 2 | import { DepositWasDoneProjection } from './deposit-was-done.projection' 3 | import { WithdrawalWasDoneProjection } from './withdrawal-was-done.projection' 4 | 5 | export * from './account.schema' 6 | 7 | export const projectionHandlers = [ 8 | AccountWasCreatedProjection, 9 | DepositWasDoneProjection, 10 | WithdrawalWasDoneProjection, 11 | ] 12 | -------------------------------------------------------------------------------- /examples/banking/src/account/infraestructure/read-model/accounts/withdrawal-was-done.projection.ts: -------------------------------------------------------------------------------- 1 | import { EventsHandler, IEventHandler } from '@nestjs/cqrs' 2 | import { InjectModel } from '@nestjs/mongoose' 3 | import { Model } from 'mongoose' 4 | 5 | import { WithdrawalWasDone } from '../../../domain' 6 | import { AccountDocument, ACCOUNTS_PROJECTION } from './account.schema' 7 | 8 | @EventsHandler(WithdrawalWasDone) 9 | export class WithdrawalWasDoneProjection 10 | implements IEventHandler 11 | { 12 | constructor( 13 | @InjectModel(ACCOUNTS_PROJECTION) 14 | private readonly accounts: Model, 15 | ) {} 16 | 17 | async handle(event: WithdrawalWasDone) { 18 | await this.accounts 19 | .updateOne({ _id: event.id }, { $inc: { balance: -event.value } }) 20 | .exec() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/banking/src/account/infraestructure/read-model/index.ts: -------------------------------------------------------------------------------- 1 | export * from './accounts' 2 | -------------------------------------------------------------------------------- /examples/banking/src/account/infraestructure/services/account-finder.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common' 2 | import { InjectModel } from '@nestjs/mongoose' 3 | import { Model } from 'mongoose' 4 | 5 | import { IAccountFinder } from '../../application' 6 | import { AccountId } from '../../domain' 7 | import { AccountDto } from '../../dto' 8 | import { AccountDocument, ACCOUNTS_PROJECTION } from '../read-model' 9 | 10 | @Injectable() 11 | export class AccountFinder implements IAccountFinder { 12 | constructor( 13 | @InjectModel(ACCOUNTS_PROJECTION) private accounts: Model, 14 | ) {} 15 | async findAll(): Promise { 16 | return this.accounts.find().lean() 17 | } 18 | 19 | async find(id: AccountId): Promise { 20 | return this.accounts.findById(id.value).lean() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/banking/src/account/infraestructure/services/account.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common' 2 | import { CommandBus, QueryBus } from '@nestjs/cqrs' 3 | 4 | import { 5 | CreateAccountCommand, 6 | CreateDepositCommand, 7 | CreateWidthdrawalCommand, 8 | GetAccountQuery, 9 | GetAccountsQuery, 10 | } from '../../application' 11 | import { AccountDto, CreateAccountDto, CreateTransactionDto } from '../../dto' 12 | 13 | @Injectable() 14 | export class AccountService { 15 | constructor( 16 | private readonly commandBus: CommandBus, 17 | private readonly queryBus: QueryBus, 18 | ) {} 19 | 20 | async getAccounts(): Promise { 21 | return this.queryBus.execute(new GetAccountsQuery()) 22 | } 23 | 24 | async getAccount(id: string): Promise { 25 | return this.queryBus.execute(new GetAccountQuery(id)) 26 | } 27 | 28 | async createAccount(accountDto: CreateAccountDto): Promise { 29 | await this.commandBus.execute( 30 | new CreateAccountCommand(accountDto._id, accountDto.title), 31 | ) 32 | 33 | return new AccountDto(accountDto._id, accountDto.title, 0) 34 | } 35 | 36 | async createDeposit(id: string, transactionDto: CreateTransactionDto) { 37 | await this.commandBus.execute( 38 | new CreateDepositCommand(id, transactionDto.value, transactionDto.date), 39 | ) 40 | } 41 | 42 | async createWithdrawal(id: string, transactionDto: CreateTransactionDto) { 43 | await this.commandBus.execute( 44 | new CreateWidthdrawalCommand( 45 | id, 46 | transactionDto.value, 47 | transactionDto.date, 48 | ), 49 | ) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /examples/banking/src/account/infraestructure/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './account.service' 2 | export * from './account-finder.service' 3 | -------------------------------------------------------------------------------- /examples/banking/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { 2 | EVENTSTORE_KEYSTORE_CONNECTION, 3 | EventStoreModule, 4 | } from '@aulasoftwarelibre/nestjs-eventstore' 5 | import { Module } from '@nestjs/common' 6 | import { ConfigModule } from '@nestjs/config' 7 | import { CqrsModule } from '@nestjs/cqrs' 8 | import { MongooseModule } from '@nestjs/mongoose' 9 | 10 | import { AccountModule } from './account' 11 | import { UserModule } from './user' 12 | 13 | @Module({ 14 | imports: [ 15 | ConfigModule.forRoot({ 16 | envFilePath: [ 17 | `.env.${process.env.NODE_ENV}.local`, 18 | `.env.${process.env.NODE_ENV}`, 19 | '.env.local', 20 | '.env', 21 | ], 22 | isGlobal: true, 23 | }), 24 | CqrsModule, 25 | EventStoreModule.forRoot({ 26 | connection: process.env.EVENTSTORE_URI, 27 | }), 28 | MongooseModule.forRoot(process.env.MONGO_URI, {}), 29 | MongooseModule.forRoot(process.env.KEYSTORE_URI, { 30 | connectionName: EVENTSTORE_KEYSTORE_CONNECTION, 31 | }), 32 | AccountModule, 33 | UserModule, 34 | ], 35 | }) 36 | export class AppModule {} 37 | -------------------------------------------------------------------------------- /examples/banking/src/console.ts: -------------------------------------------------------------------------------- 1 | import { CommandFactory } from 'nest-commander' 2 | 3 | import { AppModule } from './app.module' 4 | 5 | async function bootstrap() { 6 | await CommandFactory.run(AppModule) 7 | } 8 | 9 | bootstrap() 10 | -------------------------------------------------------------------------------- /examples/banking/src/main.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ClassSerializerInterceptor, 3 | Logger, 4 | ValidationPipe, 5 | } from '@nestjs/common' 6 | import { NestFactory, Reflector } from '@nestjs/core' 7 | import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger' 8 | 9 | import { AppModule } from './app.module' 10 | 11 | async function bootstrap() { 12 | const app = await NestFactory.create(AppModule) 13 | 14 | const config = new DocumentBuilder() 15 | .setTitle('nestjs-eventstore example') 16 | .setDescription('An application demo using EventstoreDB') 17 | .build() 18 | 19 | const document = SwaggerModule.createDocument(app, config) 20 | SwaggerModule.setup('api', app, document) 21 | 22 | const port = process.env.PORT || 3000 23 | app.useGlobalPipes(new ValidationPipe()) 24 | app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector))) 25 | await app.listen(port, () => { 26 | Logger.log(`Listening at http://localhost:${port}/api`) 27 | }) 28 | } 29 | bootstrap() 30 | -------------------------------------------------------------------------------- /examples/banking/src/user/application/command/create-user.handler.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AggregateRepository, 3 | IdAlreadyRegisteredError, 4 | InjectAggregateRepository, 5 | } from '@aulasoftwarelibre/nestjs-eventstore' 6 | import { CommandHandler, ICommandHandler } from '@nestjs/cqrs' 7 | 8 | import { Password, User, UserId, Username } from '../../domain' 9 | import { CreateUserCommand } from './create-user.query' 10 | 11 | @CommandHandler(CreateUserCommand) 12 | export class CreateUserHandler implements ICommandHandler { 13 | constructor( 14 | @InjectAggregateRepository(User) 15 | private readonly users: AggregateRepository, 16 | ) {} 17 | 18 | async execute(command: CreateUserCommand) { 19 | const userId = UserId.with(command.id) 20 | const username = Username.with(command.username) 21 | const password = Password.with(command.password) 22 | 23 | if ((await this.users.find(userId)) instanceof User) { 24 | throw IdAlreadyRegisteredError.withId(userId) 25 | } 26 | 27 | const user = User.add(userId, username, password) 28 | 29 | this.users.save(user) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/banking/src/user/application/command/create-user.query.ts: -------------------------------------------------------------------------------- 1 | import { ICommand } from '@nestjs/cqrs' 2 | 3 | export class CreateUserCommand implements ICommand { 4 | constructor( 5 | public readonly id: string, 6 | public readonly username: string, 7 | public readonly password: string, 8 | ) {} 9 | } 10 | -------------------------------------------------------------------------------- /examples/banking/src/user/application/command/delete-user.handler.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AggregateRepository, 3 | IdNotFoundError, 4 | InjectAggregateRepository, 5 | } from '@aulasoftwarelibre/nestjs-eventstore' 6 | import { CommandHandler, ICommandHandler } from '@nestjs/cqrs' 7 | 8 | import { User, UserId } from '../../domain' 9 | import { DeleteUserCommand } from './delete-user.query' 10 | 11 | @CommandHandler(DeleteUserCommand) 12 | export class DeleteUserHandler implements ICommandHandler { 13 | constructor( 14 | @InjectAggregateRepository(User) 15 | private readonly users: AggregateRepository, 16 | ) {} 17 | 18 | async execute(command: DeleteUserCommand): Promise { 19 | const userId = UserId.with(command.id) 20 | 21 | const user = await this.users.find(userId) 22 | 23 | if (!user || user.deleted) { 24 | throw IdNotFoundError.withId(userId) 25 | } 26 | 27 | user.delete() 28 | 29 | this.users.delete(user) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/banking/src/user/application/command/delete-user.query.ts: -------------------------------------------------------------------------------- 1 | import { ICommand } from '@nestjs/cqrs' 2 | 3 | export class DeleteUserCommand implements ICommand { 4 | constructor(public readonly id: string) {} 5 | } 6 | -------------------------------------------------------------------------------- /examples/banking/src/user/application/command/index.ts: -------------------------------------------------------------------------------- 1 | import { CreateUserHandler } from './create-user.handler' 2 | import { DeleteUserHandler } from './delete-user.handler' 3 | import { UpdateUserHandler } from './update-user.handler' 4 | 5 | export const commandHandlers = [ 6 | CreateUserHandler, 7 | UpdateUserHandler, 8 | DeleteUserHandler, 9 | ] 10 | 11 | export * from './create-user.query' 12 | export * from './delete-user.query' 13 | export * from './update-user.query' 14 | -------------------------------------------------------------------------------- /examples/banking/src/user/application/command/update-user.handler.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AggregateRepository, 3 | IdNotFoundError, 4 | InjectAggregateRepository, 5 | } from '@aulasoftwarelibre/nestjs-eventstore' 6 | import { CommandHandler, ICommandHandler } from '@nestjs/cqrs' 7 | 8 | import { Password, User, UserId } from '../../domain' 9 | import { UpdateUserCommand } from './update-user.query' 10 | 11 | @CommandHandler(UpdateUserCommand) 12 | export class UpdateUserHandler implements ICommandHandler { 13 | constructor( 14 | @InjectAggregateRepository(User) 15 | private readonly users: AggregateRepository, 16 | ) {} 17 | 18 | async execute(command: UpdateUserCommand): Promise { 19 | const userId = UserId.with(command.id) 20 | 21 | const user = await this.users.find(userId) 22 | 23 | if (!user || user.deleted) { 24 | throw IdNotFoundError.withId(userId) 25 | } 26 | 27 | if (command.password) { 28 | const password = Password.with(command.password) 29 | user.updatePassword(password) 30 | } 31 | 32 | this.users.save(user) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/banking/src/user/application/command/update-user.query.ts: -------------------------------------------------------------------------------- 1 | import { ICommand } from '@nestjs/cqrs' 2 | 3 | export class UpdateUserCommand implements ICommand { 4 | constructor( 5 | public readonly id: string, 6 | public readonly username?: string, 7 | public readonly password?: string, 8 | ) {} 9 | } 10 | -------------------------------------------------------------------------------- /examples/banking/src/user/application/index.ts: -------------------------------------------------------------------------------- 1 | export * from './command' 2 | export * from './query' 3 | export * from './services' 4 | -------------------------------------------------------------------------------- /examples/banking/src/user/application/query/get-user.handler.ts: -------------------------------------------------------------------------------- 1 | import { IdNotFoundError } from '@aulasoftwarelibre/nestjs-eventstore' 2 | import { Inject } from '@nestjs/common' 3 | import { IQueryHandler, QueryHandler } from '@nestjs/cqrs' 4 | 5 | import { UserId } from '../../domain' 6 | import { UserDto } from '../../dto' 7 | import { IUserFinder, USER_FINDER } from '../services' 8 | import { GetUserQuery } from './get-user.query' 9 | 10 | @QueryHandler(GetUserQuery) 11 | export class GetUserHandler implements IQueryHandler { 12 | constructor( 13 | @Inject(USER_FINDER) 14 | private readonly finder: IUserFinder, 15 | ) {} 16 | 17 | async execute(query: GetUserQuery): Promise { 18 | const userId = UserId.with(query.id) 19 | 20 | const user = await this.finder.find(userId) 21 | 22 | if (!user) { 23 | throw IdNotFoundError.withId(userId) 24 | } 25 | 26 | return user 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/banking/src/user/application/query/get-user.query.ts: -------------------------------------------------------------------------------- 1 | export class GetUserQuery { 2 | constructor(public readonly id: string) {} 3 | } 4 | -------------------------------------------------------------------------------- /examples/banking/src/user/application/query/get-users.handler.ts: -------------------------------------------------------------------------------- 1 | import { Inject } from '@nestjs/common' 2 | import { IQueryHandler, QueryHandler } from '@nestjs/cqrs' 3 | 4 | import { UserDto } from '../../dto' 5 | import { IUserFinder, USER_FINDER } from '../services' 6 | import { GetUsersQuery } from './get-users.query' 7 | 8 | @QueryHandler(GetUsersQuery) 9 | export class GetUsersHandler implements IQueryHandler { 10 | constructor(@Inject(USER_FINDER) private readonly finder: IUserFinder) {} 11 | 12 | async execute(): Promise { 13 | return this.finder.findAll() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/banking/src/user/application/query/get-users.query.ts: -------------------------------------------------------------------------------- 1 | export class GetUsersQuery {} 2 | -------------------------------------------------------------------------------- /examples/banking/src/user/application/query/index.ts: -------------------------------------------------------------------------------- 1 | import { GetUserHandler } from './get-user.handler' 2 | import { GetUsersHandler } from './get-users.handler' 3 | 4 | export * from './get-user.query' 5 | export * from './get-users.query' 6 | 7 | export const queryHandlers = [GetUsersHandler, GetUserHandler] 8 | -------------------------------------------------------------------------------- /examples/banking/src/user/application/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './user-finder.interface' 2 | -------------------------------------------------------------------------------- /examples/banking/src/user/application/services/user-finder.interface.ts: -------------------------------------------------------------------------------- 1 | import { UserId } from '../../domain' 2 | import { UserDto } from '../../dto' 3 | 4 | export const USER_FINDER = 'USER_FINDER' 5 | 6 | export interface IUserFinder { 7 | find(id: UserId): Promise 8 | findAll(): Promise 9 | } 10 | -------------------------------------------------------------------------------- /examples/banking/src/user/domain/event/index.ts: -------------------------------------------------------------------------------- 1 | export * from './password-was-updated.event' 2 | export * from './user-was-created.event' 3 | export * from './user-was-deleted.event' 4 | -------------------------------------------------------------------------------- /examples/banking/src/user/domain/event/password-was-updated.event.ts: -------------------------------------------------------------------------------- 1 | import { Event } from '@aulasoftwarelibre/nestjs-eventstore' 2 | 3 | import { UpdateUserDto } from '../../dto' 4 | 5 | export type PasswordWasUpdatedProps = Pick 6 | 7 | export class PasswordWasUpdated extends Event { 8 | constructor( 9 | public readonly id: string, 10 | public readonly password: string, 11 | ) { 12 | super(id, { password }) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/banking/src/user/domain/event/user-was-created.event.ts: -------------------------------------------------------------------------------- 1 | import { Event } from '@aulasoftwarelibre/nestjs-eventstore' 2 | 3 | import { CreateUserDto } from '../../dto' 4 | 5 | export class UserWasCreated extends Event { 6 | constructor( 7 | public readonly id: string, 8 | public readonly username: string, 9 | public readonly password: string, 10 | ) { 11 | super(id, { _id: id, password, username }) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/banking/src/user/domain/event/user-was-deleted.event.ts: -------------------------------------------------------------------------------- 1 | import { Event } from '@aulasoftwarelibre/nestjs-eventstore' 2 | 3 | export class UserWasDeleted extends Event { 4 | constructor(public readonly id: string) { 5 | super(id) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /examples/banking/src/user/domain/index.ts: -------------------------------------------------------------------------------- 1 | export * from './event' 2 | export * from './model' 3 | -------------------------------------------------------------------------------- /examples/banking/src/user/domain/model/index.ts: -------------------------------------------------------------------------------- 1 | export * from './password' 2 | export * from './user' 3 | export * from './user-id' 4 | export * from './username' 5 | -------------------------------------------------------------------------------- /examples/banking/src/user/domain/model/password.ts: -------------------------------------------------------------------------------- 1 | import { DomainError, ValueObject } from '@aulasoftwarelibre/nestjs-eventstore' 2 | 3 | export class Password extends ValueObject<{ 4 | value: string 5 | }> { 6 | public static with(value: string): Password { 7 | if (value.length < 12) { 8 | throw DomainError.because('Password is too short (min. 12 characters)') 9 | } 10 | 11 | return new Password({ value }) 12 | } 13 | 14 | get value(): string { 15 | return this.props.value 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/banking/src/user/domain/model/user-id.ts: -------------------------------------------------------------------------------- 1 | import { Id } from '@aulasoftwarelibre/nestjs-eventstore' 2 | 3 | export class UserId extends Id { 4 | public static with(id: string): UserId { 5 | return new UserId(id) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /examples/banking/src/user/domain/model/user.ts: -------------------------------------------------------------------------------- 1 | import { EncryptedAggregateRoot } from '@aulasoftwarelibre/nestjs-eventstore' 2 | 3 | import { UserWasCreated } from '../event' 4 | import { PasswordWasUpdated } from '../event/password-was-updated.event' 5 | import { UserWasDeleted } from '../event/user-was-deleted.event' 6 | import { Password } from './password' 7 | import { UserId } from './user-id' 8 | import { Username } from './username' 9 | 10 | export class User extends EncryptedAggregateRoot { 11 | private _userId: UserId 12 | private _username: Username 13 | private _password: Password 14 | private _deleted: boolean 15 | 16 | public static add( 17 | userId: UserId, 18 | username: Username, 19 | password: Password, 20 | ): User { 21 | const user = new User() 22 | 23 | user.apply(new UserWasCreated(userId.value, username.value, password.value)) 24 | 25 | return user 26 | } 27 | 28 | public aggregateId(): string { 29 | return this.id.value 30 | } 31 | 32 | get id(): UserId { 33 | return this._userId 34 | } 35 | 36 | get username(): Username { 37 | return this._username 38 | } 39 | 40 | get password(): Password { 41 | return this._password 42 | } 43 | 44 | get deleted(): boolean { 45 | return this._deleted 46 | } 47 | 48 | updatePassword(newPassword: Password): void { 49 | if (this._password.equals(newPassword)) { 50 | return 51 | } 52 | 53 | this.apply(new PasswordWasUpdated(this.id.value, newPassword.value)) 54 | } 55 | 56 | delete() { 57 | if (this._deleted) { 58 | return 59 | } 60 | 61 | this.apply(new UserWasDeleted(this.id.value)) 62 | } 63 | 64 | private onUserWasCreated(event: UserWasCreated) { 65 | this._userId = UserId.with(event.id) 66 | this._username = Username.with(event.username) 67 | this._password = Password.with(event.password) 68 | this._deleted = false 69 | } 70 | 71 | private onPasswordWasUpdated(event: PasswordWasUpdated) { 72 | this._password = Password.with(event.password) 73 | } 74 | 75 | private onUserWasDeleted() { 76 | this._deleted = true 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /examples/banking/src/user/domain/model/username.ts: -------------------------------------------------------------------------------- 1 | import { DomainError, ValueObject } from '@aulasoftwarelibre/nestjs-eventstore' 2 | 3 | export class Username extends ValueObject<{ 4 | value: string 5 | }> { 6 | public static with(value: string): Username { 7 | if (value.length === 0) { 8 | throw DomainError.because('Username cannot be empty') 9 | } 10 | 11 | return new Username({ value }) 12 | } 13 | 14 | get value(): string { 15 | return this.props.value 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/banking/src/user/dto/index.ts: -------------------------------------------------------------------------------- 1 | export * from './request' 2 | export * from './response' 3 | -------------------------------------------------------------------------------- /examples/banking/src/user/dto/request/create-user.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger' 2 | import { IsNotEmpty, IsUUID } from 'class-validator' 3 | 4 | export class CreateUserDto { 5 | @ApiProperty() 6 | @IsNotEmpty() 7 | @IsUUID(4) 8 | public readonly _id: string 9 | 10 | @ApiProperty() 11 | @IsNotEmpty() 12 | public readonly username: string 13 | 14 | @ApiProperty() 15 | @IsNotEmpty() 16 | public readonly password: string 17 | } 18 | -------------------------------------------------------------------------------- /examples/banking/src/user/dto/request/index.ts: -------------------------------------------------------------------------------- 1 | export * from './create-user.dto' 2 | export * from './update-user.dto' 3 | -------------------------------------------------------------------------------- /examples/banking/src/user/dto/request/update-user.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger' 2 | 3 | export class UpdateUserDto { 4 | @ApiProperty({ required: false }) 5 | public readonly username: string 6 | 7 | @ApiProperty({ required: false }) 8 | public readonly password: string 9 | } 10 | -------------------------------------------------------------------------------- /examples/banking/src/user/dto/response/index.ts: -------------------------------------------------------------------------------- 1 | export * from './user.dto' 2 | -------------------------------------------------------------------------------- /examples/banking/src/user/dto/response/user.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger' 2 | import { Exclude } from 'class-transformer' 3 | 4 | interface Props { 5 | _id: string 6 | password: string 7 | username: string 8 | } 9 | 10 | export class UserDto { 11 | @ApiProperty() 12 | public readonly _id: string 13 | 14 | @ApiProperty() 15 | public readonly username: string 16 | 17 | @Exclude() 18 | public readonly password: string 19 | 20 | constructor(props: Props) { 21 | this._id = props._id 22 | this.username = props.username 23 | this.password = props.password 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/banking/src/user/index.ts: -------------------------------------------------------------------------------- 1 | export * from './infraestructure' 2 | -------------------------------------------------------------------------------- /examples/banking/src/user/infraestructure/controller/index.ts: -------------------------------------------------------------------------------- 1 | export * from './user.controller' 2 | -------------------------------------------------------------------------------- /examples/banking/src/user/infraestructure/controller/user.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IdAlreadyRegisteredError, 3 | IdNotFoundError, 4 | } from '@aulasoftwarelibre/nestjs-eventstore' 5 | import { 6 | Body, 7 | ConflictException, 8 | Controller, 9 | Delete, 10 | Get, 11 | NotFoundException, 12 | Param, 13 | Post, 14 | Put, 15 | } from '@nestjs/common' 16 | import { 17 | ApiOkResponse, 18 | ApiOperation, 19 | ApiResponse, 20 | ApiTags, 21 | } from '@nestjs/swagger' 22 | 23 | import { catchError } from '../../../utils' 24 | import { CreateUserDto, UpdateUserDto, UserDto } from '../../dto' 25 | import { UserService } from '../services' 26 | 27 | @ApiTags('Users') 28 | @Controller('users') 29 | export class UserController { 30 | constructor(private readonly userService: UserService) {} 31 | 32 | @ApiOperation({ summary: 'Get users' }) 33 | @Get() 34 | async findAll(): Promise { 35 | return this.userService.findAll() 36 | } 37 | 38 | @ApiOperation({ summary: 'Create user' }) 39 | @ApiOkResponse({ type: UserDto }) 40 | @Post() 41 | async create(@Body() userDto: CreateUserDto): Promise { 42 | try { 43 | return await this.userService.create(userDto) 44 | } catch (error) { 45 | const error_ = 46 | error instanceof IdAlreadyRegisteredError 47 | ? new ConflictException(error.message) 48 | : catchError(error) 49 | throw error_ 50 | } 51 | } 52 | 53 | @ApiOperation({ summary: 'Get user' }) 54 | @ApiOkResponse({ type: UserDto }) 55 | @Get(':id') 56 | async findOne(@Param('id') id: string): Promise { 57 | try { 58 | return await this.userService.findOne(id) 59 | } catch (error) { 60 | const error_ = 61 | error instanceof IdNotFoundError 62 | ? new NotFoundException('User not found') 63 | : catchError(error) 64 | throw error_ 65 | } 66 | } 67 | 68 | @ApiOperation({ summary: 'Edit user' }) 69 | @ApiOkResponse({ type: UserDto }) 70 | @Put(':id') 71 | async update( 72 | @Param('id') id: string, 73 | @Body() editUserDto: UpdateUserDto, 74 | ): Promise { 75 | try { 76 | return await this.userService.update(id, editUserDto) 77 | } catch (error) { 78 | const error_ = 79 | error instanceof IdNotFoundError 80 | ? new NotFoundException('User not found') 81 | : catchError(error) 82 | throw error_ 83 | } 84 | } 85 | 86 | @ApiOperation({ summary: 'Delete user' }) 87 | @ApiResponse({ description: 'User deleted', status: 204 }) 88 | @ApiResponse({ description: 'User not found', status: 404 }) 89 | @Delete(':id') 90 | async delete(@Param('id') id: string): Promise { 91 | try { 92 | return this.userService.delete(id) 93 | } catch (error) { 94 | const error_ = 95 | error instanceof IdNotFoundError 96 | ? new NotFoundException('User not found') 97 | : catchError(error) 98 | throw error_ 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /examples/banking/src/user/infraestructure/index.ts: -------------------------------------------------------------------------------- 1 | export * from './user.module' 2 | -------------------------------------------------------------------------------- /examples/banking/src/user/infraestructure/read-model/index.ts: -------------------------------------------------------------------------------- 1 | export * from './users' 2 | -------------------------------------------------------------------------------- /examples/banking/src/user/infraestructure/read-model/users/index.ts: -------------------------------------------------------------------------------- 1 | import { PasswordWasUpdatedProjection } from './password-was-updated.projection' 2 | import { UserWasCreatedProjection } from './user-was-created.projection' 3 | import { UserWasDeletedProjection } from './user-was-deleted.projection' 4 | 5 | export * from './user.schema' 6 | 7 | export const projectionHandlers = [ 8 | UserWasCreatedProjection, 9 | PasswordWasUpdatedProjection, 10 | UserWasDeletedProjection, 11 | ] 12 | -------------------------------------------------------------------------------- /examples/banking/src/user/infraestructure/read-model/users/password-was-updated.projection.ts: -------------------------------------------------------------------------------- 1 | import { EventsHandler, IEventHandler } from '@nestjs/cqrs' 2 | import { InjectModel } from '@nestjs/mongoose' 3 | import { Model } from 'mongoose' 4 | 5 | import { PasswordWasUpdated } from '../../../domain' 6 | import { UserDocument, USERS_PROJECTION } from './user.schema' 7 | 8 | @EventsHandler(PasswordWasUpdated) 9 | export class PasswordWasUpdatedProjection 10 | implements IEventHandler 11 | { 12 | constructor( 13 | @InjectModel(USERS_PROJECTION) 14 | private readonly users: Model, 15 | ) {} 16 | 17 | async handle(event: PasswordWasUpdated) { 18 | this.users 19 | .findByIdAndUpdate(event.aggregateId, { 20 | password: event.password, 21 | }) 22 | .exec() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/banking/src/user/infraestructure/read-model/users/user-was-created.projection.ts: -------------------------------------------------------------------------------- 1 | import { EventsHandler, IEventHandler } from '@nestjs/cqrs' 2 | import { InjectModel } from '@nestjs/mongoose' 3 | import { Model } from 'mongoose' 4 | 5 | import { UserWasCreated } from '../../../domain' 6 | import { UserDocument, USERS_PROJECTION } from './user.schema' 7 | 8 | @EventsHandler(UserWasCreated) 9 | export class UserWasCreatedProjection implements IEventHandler { 10 | constructor( 11 | @InjectModel(USERS_PROJECTION) 12 | private readonly users: Model, 13 | ) {} 14 | 15 | async handle(event: UserWasCreated) { 16 | const user = new this.users({ 17 | ...event.payload, 18 | }) 19 | 20 | await user.save() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/banking/src/user/infraestructure/read-model/users/user-was-deleted.projection.ts: -------------------------------------------------------------------------------- 1 | import { EventsHandler, IEventHandler } from '@nestjs/cqrs' 2 | import { InjectModel } from '@nestjs/mongoose' 3 | import { Model } from 'mongoose' 4 | 5 | import { UserWasDeleted } from '../../../domain' 6 | import { UserDocument, USERS_PROJECTION } from './user.schema' 7 | 8 | @EventsHandler(UserWasDeleted) 9 | export class UserWasDeletedProjection implements IEventHandler { 10 | constructor( 11 | @InjectModel(USERS_PROJECTION) 12 | private readonly users: Model, 13 | ) {} 14 | 15 | async handle(event: UserWasDeleted) { 16 | this.users.findByIdAndDelete(event.aggregateId).exec() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/banking/src/user/infraestructure/read-model/users/user.schema.ts: -------------------------------------------------------------------------------- 1 | import { Document, Schema } from 'mongoose' 2 | 3 | import { UserDto } from '../../../dto' 4 | 5 | export const USERS_PROJECTION = 'users' 6 | 7 | export type UserDocument = UserDto & Document 8 | 9 | export const UserSchema = new Schema( 10 | { 11 | _id: String, 12 | password: String, 13 | username: { index: { unique: true }, type: String }, 14 | }, 15 | { 16 | versionKey: false, 17 | }, 18 | ) 19 | -------------------------------------------------------------------------------- /examples/banking/src/user/infraestructure/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './user.service' 2 | export * from './user-finder.service' 3 | -------------------------------------------------------------------------------- /examples/banking/src/user/infraestructure/services/user-finder.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common' 2 | import { InjectModel } from '@nestjs/mongoose' 3 | import { Model } from 'mongoose' 4 | 5 | import { IUserFinder } from '../../application' 6 | import { UserId } from '../../domain' 7 | import { UserDto } from '../../dto' 8 | import { UserDocument, USERS_PROJECTION } from '../read-model' 9 | 10 | @Injectable() 11 | export class UserFinder implements IUserFinder { 12 | constructor( 13 | @InjectModel(USERS_PROJECTION) private readonly users: Model, 14 | ) {} 15 | 16 | async findAll(): Promise { 17 | const users = await this.users.find().lean() 18 | 19 | return users.map((user) => new UserDto(user)) 20 | } 21 | 22 | async find(id: UserId): Promise { 23 | const user = await this.users.findById(id.value).lean() 24 | 25 | return new UserDto(user) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /examples/banking/src/user/infraestructure/services/user.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, NotFoundException } from '@nestjs/common' 2 | import { CommandBus, QueryBus } from '@nestjs/cqrs' 3 | 4 | import { 5 | CreateUserCommand, 6 | DeleteUserCommand, 7 | GetUserQuery, 8 | GetUsersQuery, 9 | UpdateUserCommand, 10 | } from '../../application' 11 | import { CreateUserDto, UpdateUserDto, UserDto } from '../../dto' 12 | 13 | @Injectable() 14 | export class UserService { 15 | constructor( 16 | private readonly commandBus: CommandBus, 17 | private readonly queryBus: QueryBus, 18 | ) {} 19 | 20 | async findOne(id: string): Promise { 21 | return this.queryBus.execute(new GetUserQuery(id)) 22 | } 23 | 24 | async findAll(): Promise { 25 | return this.queryBus.execute(new GetUsersQuery()) 26 | } 27 | 28 | async create(userDto: CreateUserDto): Promise { 29 | await this.commandBus.execute( 30 | new CreateUserCommand(userDto._id, userDto.username, userDto.password), 31 | ) 32 | 33 | return new UserDto({ ...userDto }) 34 | } 35 | 36 | async update(id: string, editUserDto: UpdateUserDto): Promise { 37 | await this.commandBus.execute( 38 | new UpdateUserCommand(id, editUserDto.username, editUserDto.password), 39 | ) 40 | 41 | const user = await this.queryBus.execute(new GetUserQuery(id)) 42 | 43 | if (!user) { 44 | throw new NotFoundException('User not found.') 45 | } 46 | 47 | return new UserDto({ ...user }) 48 | } 49 | 50 | async delete(id: string) { 51 | await this.commandBus.execute(new DeleteUserCommand(id)) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /examples/banking/src/user/infraestructure/user.module.ts: -------------------------------------------------------------------------------- 1 | import { Event, EventStoreModule } from '@aulasoftwarelibre/nestjs-eventstore' 2 | import { Module } from '@nestjs/common' 3 | import { CqrsModule } from '@nestjs/cqrs' 4 | import { MongooseModule } from '@nestjs/mongoose' 5 | 6 | import { commandHandlers, queryHandlers } from '../application' 7 | import { 8 | PasswordWasUpdated, 9 | PasswordWasUpdatedProps, 10 | User, 11 | UserWasCreated, 12 | UserWasDeleted, 13 | } from '../domain' 14 | import { CreateUserDto } from '../dto' 15 | import { UserController } from './controller' 16 | import { projectionHandlers, USERS_PROJECTION, UserSchema } from './read-model' 17 | import { UserService } from './services' 18 | import { userProviders } from './user.providers' 19 | 20 | @Module({ 21 | controllers: [UserController], 22 | imports: [ 23 | CqrsModule, 24 | EventStoreModule.forFeature([User], { 25 | PasswordWasUpdated: (event: Event) => 26 | new PasswordWasUpdated(event.aggregateId, event.payload.password), 27 | UserWasCreated: (event: Event) => 28 | new UserWasCreated( 29 | event.aggregateId, 30 | event.payload.username, 31 | event.payload.password, 32 | ), 33 | UserWasDeleted: (event: Event) => new UserWasDeleted(event.aggregateId), 34 | }), 35 | MongooseModule.forFeature([ 36 | { 37 | name: USERS_PROJECTION, 38 | schema: UserSchema, 39 | }, 40 | ]), 41 | ], 42 | providers: [ 43 | ...commandHandlers, 44 | ...queryHandlers, 45 | ...projectionHandlers, 46 | ...userProviders, 47 | UserService, 48 | ], 49 | }) 50 | export class UserModule {} 51 | -------------------------------------------------------------------------------- /examples/banking/src/user/infraestructure/user.providers.ts: -------------------------------------------------------------------------------- 1 | import { USER_FINDER } from '../application' 2 | import { UserFinder } from './services' 3 | 4 | export const userProviders = [ 5 | { 6 | provide: USER_FINDER, 7 | useClass: UserFinder, 8 | }, 9 | ] 10 | -------------------------------------------------------------------------------- /examples/banking/src/utils/catch.error.ts: -------------------------------------------------------------------------------- 1 | import { DomainError } from '@aulasoftwarelibre/nestjs-eventstore' 2 | import { BadRequestException } from '@nestjs/common' 3 | 4 | export const catchError = (error: Error): Error => { 5 | if (error instanceof DomainError) { 6 | return new BadRequestException(error.message) 7 | } else if (error instanceof Error) { 8 | return new BadRequestException(`Unexpected error: ${error.message}`) 9 | } else { 10 | return new BadRequestException('Server error') 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/banking/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './catch.error' 2 | -------------------------------------------------------------------------------- /examples/banking/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /examples/banking/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "ES2021", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "strictNullChecks": false, 16 | "noImplicitAny": false, 17 | "strictBindCallApply": false, 18 | "forceConsistentCasingInFileNames": false, 19 | "noFallthroughCasesInSwitch": false 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Sergio Gómez Bachiller ", 3 | "license": "EUPL-1.2", 4 | "private": true, 5 | "scripts": { 6 | "build": "pnpm --filter './packages/nestjs-eventstore' run build", 7 | "changeset": "changeset", 8 | "clean": "pnpm --filter './packages/nestjs-eventstore' run clean", 9 | "dev": "pnpm --filter './packages/nestjs-eventstore' run dev", 10 | "prepare": "husky", 11 | "lint": "pnpm --filter './packages/nestjs-eventstore' run lint", 12 | "type-check": "pnpm --filter './packages/nestjs-eventstore' run type-check", 13 | "prettier-check": "pnpm --filter './packages/nestjs-eventstore' run prettier-check", 14 | "test": "pnpm --filter './packages/nestjs-eventstore' run test", 15 | "ci:release": "pnpm clean && pnpm build && changeset publish", 16 | "ci:version": "changeset version && pnpm install --no-frozen-lockfile" 17 | }, 18 | "devDependencies": { 19 | "@changesets/cli": "^2.27.1", 20 | "@commitlint/cli": "^19.3.0", 21 | "@commitlint/config-conventional": "^19.2.2", 22 | "@nestjs/cli": "^10.1.0", 23 | "@swc/core": "^1.7.10", 24 | "@typescript-eslint/eslint-plugin": "^7.9.0", 25 | "@typescript-eslint/parser": "^7.9.0", 26 | "@vitest/coverage-v8": "^2.0.5", 27 | "@vitest/ui": "^2.0.5", 28 | "eslint": "^8.57.0", 29 | "eslint-config-prettier": "^9.1.0", 30 | "eslint-plugin-import": "^2.29.1", 31 | "eslint-plugin-prettier": "^5.1.3", 32 | "eslint-plugin-simple-import-sort": "^12.1.0", 33 | "eslint-plugin-sort": "^3.0.2", 34 | "eslint-plugin-unicorn": "^52.0.0", 35 | "eslint-plugin-unused-imports": "^3.2.0", 36 | "husky": "^9.0.11", 37 | "lint-staged": "^15.2.2", 38 | "prettier": "^3.2.5", 39 | "unplugin-swc": "^1.5.1", 40 | "vite-tsconfig-paths": "^4.3.2", 41 | "vitest": "^2.0.5" 42 | }, 43 | "homepage": "https://github.com/aulasoftwarelibre/nestjs-eventstore", 44 | "repository": { 45 | "type": "git", 46 | "url": "git+https://github.com/aulasoftwarelibre/nestjs-eventstore.git" 47 | }, 48 | "bugs": { 49 | "url": "https://github.com/aulasoftwarelibre/nestjs-eventstore/issues" 50 | }, 51 | "keywords": [ 52 | "ai" 53 | ], 54 | "packageManager": "pnpm@9.6.0" 55 | } 56 | -------------------------------------------------------------------------------- /packages/nestjs-eventstore/.dockerignore: -------------------------------------------------------------------------------- 1 | dist 2 | example/dist 3 | example/src/nestjs-eventstore 4 | 5 | # Common 6 | README.md 7 | CHANGELOG.md 8 | docker-compose.yml 9 | Dockerfile 10 | .env* 11 | 12 | # git 13 | .git 14 | .gitattributes 15 | .gitignore 16 | 17 | # Node 18 | ## Logs 19 | logs 20 | *.log 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | ## Dependency directories 26 | node_modules/ 27 | -------------------------------------------------------------------------------- /packages/nestjs-eventstore/LICENSE: -------------------------------------------------------------------------------- 1 | EUROPEAN UNION PUBLIC LICENCE v. 1.2 2 | EUPL © the European Union 2007, 2016 3 | 4 | This European Union Public Licence (the ‘EUPL’) applies to the Work (as defined 5 | below) which is provided under the terms of this Licence. Any use of the Work, 6 | other than as authorised under this Licence is prohibited (to the extent such 7 | use is covered by a right of the copyright holder of the Work). 8 | 9 | The Work is provided under the terms of this Licence when the Licensor (as 10 | defined below) has placed the following notice immediately following the 11 | copyright notice for the Work: 12 | 13 | Licensed under the EUPL 14 | 15 | or has expressed by any other means his willingness to license under the EUPL. 16 | 17 | 1. Definitions 18 | 19 | In this Licence, the following terms have the following meaning: 20 | 21 | - ‘The Licence’: this Licence. 22 | 23 | - ‘The Original Work’: the work or software distributed or communicated by the 24 | Licensor under this Licence, available as Source Code and also as Executable 25 | Code as the case may be. 26 | 27 | - ‘Derivative Works’: the works or software that could be created by the 28 | Licensee, based upon the Original Work or modifications thereof. This Licence 29 | does not define the extent of modification or dependence on the Original Work 30 | required in order to classify a work as a Derivative Work; this extent is 31 | determined by copyright law applicable in the country mentioned in Article 15. 32 | 33 | - ‘The Work’: the Original Work or its Derivative Works. 34 | 35 | - ‘The Source Code’: the human-readable form of the Work which is the most 36 | convenient for people to study and modify. 37 | 38 | - ‘The Executable Code’: any code which has generally been compiled and which is 39 | meant to be interpreted by a computer as a program. 40 | 41 | - ‘The Licensor’: the natural or legal person that distributes or communicates 42 | the Work under the Licence. 43 | 44 | - ‘Contributor(s)’: any natural or legal person who modifies the Work under the 45 | Licence, or otherwise contributes to the creation of a Derivative Work. 46 | 47 | - ‘The Licensee’ or ‘You’: any natural or legal person who makes any usage of 48 | the Work under the terms of the Licence. 49 | 50 | - ‘Distribution’ or ‘Communication’: any act of selling, giving, lending, 51 | renting, distributing, communicating, transmitting, or otherwise making 52 | available, online or offline, copies of the Work or providing access to its 53 | essential functionalities at the disposal of any other natural or legal 54 | person. 55 | 56 | 2. Scope of the rights granted by the Licence 57 | 58 | The Licensor hereby grants You a worldwide, royalty-free, non-exclusive, 59 | sublicensable licence to do the following, for the duration of copyright vested 60 | in the Original Work: 61 | 62 | - use the Work in any circumstance and for all usage, 63 | - reproduce the Work, 64 | - modify the Work, and make Derivative Works based upon the Work, 65 | - communicate to the public, including the right to make available or display 66 | the Work or copies thereof to the public and perform publicly, as the case may 67 | be, the Work, 68 | - distribute the Work or copies thereof, 69 | - lend and rent the Work or copies thereof, 70 | - sublicense rights in the Work or copies thereof. 71 | 72 | Those rights can be exercised on any media, supports and formats, whether now 73 | known or later invented, as far as the applicable law permits so. 74 | 75 | In the countries where moral rights apply, the Licensor waives his right to 76 | exercise his moral right to the extent allowed by law in order to make effective 77 | the licence of the economic rights here above listed. 78 | 79 | The Licensor grants to the Licensee royalty-free, non-exclusive usage rights to 80 | any patents held by the Licensor, to the extent necessary to make use of the 81 | rights granted on the Work under this Licence. 82 | 83 | 3. Communication of the Source Code 84 | 85 | The Licensor may provide the Work either in its Source Code form, or as 86 | Executable Code. If the Work is provided as Executable Code, the Licensor 87 | provides in addition a machine-readable copy of the Source Code of the Work 88 | along with each copy of the Work that the Licensor distributes or indicates, in 89 | a notice following the copyright notice attached to the Work, a repository where 90 | the Source Code is easily and freely accessible for as long as the Licensor 91 | continues to distribute or communicate the Work. 92 | 93 | 4. Limitations on copyright 94 | 95 | Nothing in this Licence is intended to deprive the Licensee of the benefits from 96 | any exception or limitation to the exclusive rights of the rights owners in the 97 | Work, of the exhaustion of those rights or of other applicable limitations 98 | thereto. 99 | 100 | 5. Obligations of the Licensee 101 | 102 | The grant of the rights mentioned above is subject to some restrictions and 103 | obligations imposed on the Licensee. Those obligations are the following: 104 | 105 | Attribution right: The Licensee shall keep intact all copyright, patent or 106 | trademarks notices and all notices that refer to the Licence and to the 107 | disclaimer of warranties. The Licensee must include a copy of such notices and a 108 | copy of the Licence with every copy of the Work he/she distributes or 109 | communicates. The Licensee must cause any Derivative Work to carry prominent 110 | notices stating that the Work has been modified and the date of modification. 111 | 112 | Copyleft clause: If the Licensee distributes or communicates copies of the 113 | Original Works or Derivative Works, this Distribution or Communication will be 114 | done under the terms of this Licence or of a later version of this Licence 115 | unless the Original Work is expressly distributed only under this version of the 116 | Licence — for example by communicating ‘EUPL v. 1.2 only’. The Licensee 117 | (becoming Licensor) cannot offer or impose any additional terms or conditions on 118 | the Work or Derivative Work that alter or restrict the terms of the Licence. 119 | 120 | Compatibility clause: If the Licensee Distributes or Communicates Derivative 121 | Works or copies thereof based upon both the Work and another work licensed under 122 | a Compatible Licence, this Distribution or Communication can be done under the 123 | terms of this Compatible Licence. For the sake of this clause, ‘Compatible 124 | Licence’ refers to the licences listed in the appendix attached to this Licence. 125 | Should the Licensee's obligations under the Compatible Licence conflict with 126 | his/her obligations under this Licence, the obligations of the Compatible 127 | Licence shall prevail. 128 | 129 | Provision of Source Code: When distributing or communicating copies of the Work, 130 | the Licensee will provide a machine-readable copy of the Source Code or indicate 131 | a repository where this Source will be easily and freely available for as long 132 | as the Licensee continues to distribute or communicate the Work. 133 | 134 | Legal Protection: This Licence does not grant permission to use the trade names, 135 | trademarks, service marks, or names of the Licensor, except as required for 136 | reasonable and customary use in describing the origin of the Work and 137 | reproducing the content of the copyright notice. 138 | 139 | 6. Chain of Authorship 140 | 141 | The original Licensor warrants that the copyright in the Original Work granted 142 | hereunder is owned by him/her or licensed to him/her and that he/she has the 143 | power and authority to grant the Licence. 144 | 145 | Each Contributor warrants that the copyright in the modifications he/she brings 146 | to the Work are owned by him/her or licensed to him/her and that he/she has the 147 | power and authority to grant the Licence. 148 | 149 | Each time You accept the Licence, the original Licensor and subsequent 150 | Contributors grant You a licence to their contributions to the Work, under the 151 | terms of this Licence. 152 | 153 | 7. Disclaimer of Warranty 154 | 155 | The Work is a work in progress, which is continuously improved by numerous 156 | Contributors. It is not a finished work and may therefore contain defects or 157 | ‘bugs’ inherent to this type of development. 158 | 159 | For the above reason, the Work is provided under the Licence on an ‘as is’ basis 160 | and without warranties of any kind concerning the Work, including without 161 | limitation merchantability, fitness for a particular purpose, absence of defects 162 | or errors, accuracy, non-infringement of intellectual property rights other than 163 | copyright as stated in Article 6 of this Licence. 164 | 165 | This disclaimer of warranty is an essential part of the Licence and a condition 166 | for the grant of any rights to the Work. 167 | 168 | 8. Disclaimer of Liability 169 | 170 | Except in the cases of wilful misconduct or damages directly caused to natural 171 | persons, the Licensor will in no event be liable for any direct or indirect, 172 | material or moral, damages of any kind, arising out of the Licence or of the use 173 | of the Work, including without limitation, damages for loss of goodwill, work 174 | stoppage, computer failure or malfunction, loss of data or any commercial 175 | damage, even if the Licensor has been advised of the possibility of such damage. 176 | However, the Licensor will be liable under statutory product liability laws as 177 | far such laws apply to the Work. 178 | 179 | 9. Additional agreements 180 | 181 | While distributing the Work, You may choose to conclude an additional agreement, 182 | defining obligations or services consistent with this Licence. However, if 183 | accepting obligations, You may act only on your own behalf and on your sole 184 | responsibility, not on behalf of the original Licensor or any other Contributor, 185 | and only if You agree to indemnify, defend, and hold each Contributor harmless 186 | for any liability incurred by, or claims asserted against such Contributor by 187 | the fact You have accepted any warranty or additional liability. 188 | 189 | 10. Acceptance of the Licence 190 | 191 | The provisions of this Licence can be accepted by clicking on an icon ‘I agree’ 192 | placed under the bottom of a window displaying the text of this Licence or by 193 | affirming consent in any other similar way, in accordance with the rules of 194 | applicable law. Clicking on that icon indicates your clear and irrevocable 195 | acceptance of this Licence and all of its terms and conditions. 196 | 197 | Similarly, you irrevocably accept this Licence and all of its terms and 198 | conditions by exercising any rights granted to You by Article 2 of this Licence, 199 | such as the use of the Work, the creation by You of a Derivative Work or the 200 | Distribution or Communication by You of the Work or copies thereof. 201 | 202 | 11. Information to the public 203 | 204 | In case of any Distribution or Communication of the Work by means of electronic 205 | communication by You (for example, by offering to download the Work from a 206 | remote location) the distribution channel or media (for example, a website) must 207 | at least provide to the public the information requested by the applicable law 208 | regarding the Licensor, the Licence and the way it may be accessible, concluded, 209 | stored and reproduced by the Licensee. 210 | 211 | 12. Termination of the Licence 212 | 213 | The Licence and the rights granted hereunder will terminate automatically upon 214 | any breach by the Licensee of the terms of the Licence. 215 | 216 | Such a termination will not terminate the licences of any person who has 217 | received the Work from the Licensee under the Licence, provided such persons 218 | remain in full compliance with the Licence. 219 | 220 | 13. Miscellaneous 221 | 222 | Without prejudice of Article 9 above, the Licence represents the complete 223 | agreement between the Parties as to the Work. 224 | 225 | If any provision of the Licence is invalid or unenforceable under applicable 226 | law, this will not affect the validity or enforceability of the Licence as a 227 | whole. Such provision will be construed or reformed so as necessary to make it 228 | valid and enforceable. 229 | 230 | The European Commission may publish other linguistic versions or new versions of 231 | this Licence or updated versions of the Appendix, so far this is required and 232 | reasonable, without reducing the scope of the rights granted by the Licence. New 233 | versions of the Licence will be published with a unique version number. 234 | 235 | All linguistic versions of this Licence, approved by the European Commission, 236 | have identical value. Parties can take advantage of the linguistic version of 237 | their choice. 238 | 239 | 14. Jurisdiction 240 | 241 | Without prejudice to specific agreement between parties, 242 | 243 | - any litigation resulting from the interpretation of this License, arising 244 | between the European Union institutions, bodies, offices or agencies, as a 245 | Licensor, and any Licensee, will be subject to the jurisdiction of the Court 246 | of Justice of the European Union, as laid down in article 272 of the Treaty on 247 | the Functioning of the European Union, 248 | 249 | - any litigation arising between other parties and resulting from the 250 | interpretation of this License, will be subject to the exclusive jurisdiction 251 | of the competent court where the Licensor resides or conducts its primary 252 | business. 253 | 254 | 15. Applicable Law 255 | 256 | Without prejudice to specific agreement between parties, 257 | 258 | - this Licence shall be governed by the law of the European Union Member State 259 | where the Licensor has his seat, resides or has his registered office, 260 | 261 | - this licence shall be governed by Belgian law if the Licensor has no seat, 262 | residence or registered office inside a European Union Member State. 263 | 264 | Appendix 265 | 266 | ‘Compatible Licences’ according to Article 5 EUPL are: 267 | 268 | - GNU General Public License (GPL) v. 2, v. 3 269 | - GNU Affero General Public License (AGPL) v. 3 270 | - Open Software License (OSL) v. 2.1, v. 3.0 271 | - Eclipse Public License (EPL) v. 1.0 272 | - CeCILL v. 2.0, v. 2.1 273 | - Mozilla Public Licence (MPL) v. 2 274 | - GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3 275 | - Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for 276 | works other than software 277 | - European Union Public Licence (EUPL) v. 1.1, v. 1.2 278 | - Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong 279 | Reciprocity (LiLiQ-R+). 280 | 281 | The European Commission may update this Appendix to later versions of the above 282 | licences without producing a new version of the EUPL, as long as they provide 283 | the rights granted in Article 2 of this Licence and protect the covered Source 284 | Code from exclusive appropriation. 285 | 286 | All other changes or additions to this Appendix require the production of a new 287 | EUPL version. 288 | -------------------------------------------------------------------------------- /packages/nestjs-eventstore/README.md: -------------------------------------------------------------------------------- 1 | # EventSource module for NestJS 2 | 3 | [![Contributors][contributors-shield]][contributors-url] 4 | [![Forks][forks-shield]][forks-url] 5 | [![Stargazers][stars-shield]][stars-url] 6 | [![Issues][issues-shield]][issues-url] 7 | [![MIT License][license-shield]][license-url] 8 | 9 | 10 |
11 |

12 | 13 | Aula Software Libre de la UCO 14 | 15 | 16 |

EventSource module for NestJS

17 | 18 |

19 | NestJS module for eventsourcing development with eventstore database 20 |

21 |

22 | 23 | 24 | 25 | ## About The Project 26 | 27 | This module allows you to connect to a [EventstoreDB](https://www.eventstore.com/) to do event sourcing with nestjs. 28 | 29 | **This is a Work In Progress**, not ready to use it in producction. 30 | 31 | ## Getting Started 32 | 33 | WIP 34 | 35 | See [example](./example) 36 | 37 | ### Prerequisites 38 | 39 | You require to have a nestjs project with this modules already installed and loaded: 40 | 41 | - [@nestjs/cqrs](https://www.npmjs.com/package/@nestjs/cqrs) 42 | - [nestjs-console](https://www.npmjs.com/package/nestjs-console) 43 | 44 | ### Installation 45 | 46 | - npm 47 | 48 | npm install @aulasoftwarelibre/nestjs-eventstore 49 | 50 | - yarn 51 | 52 | yarn add @aulasoftwarelibre/nestjs-eventstore 53 | 54 | ## Usage 55 | 56 | ### Loading the module 57 | 58 | ## Contributing 59 | 60 | ## License 61 | 62 | Distributed under the EUPL-1.2 License. See `LICENSE` for more information. 63 | 64 | ## Running the example 65 | 66 | To run the example you will need docker. Just run: 67 | 68 | ```shell 69 | docker compose up -d 70 | ``` 71 | 72 | And a few minutes later you will access to the example application in the next urls: 73 | 74 | - [Swagger API](http://localhost:3000/api/) 75 | - [EventStore Database (Write model)](http://localhost:2113) 76 | - [Mongo Database (Read model)](http://admin:pass@localhost:8081/) 77 | 78 | ## Acknowledgements 79 | 80 | This module was created following next articles: 81 | 82 | - https://medium.com/@qasimsoomro/building-microservices-using-node-js-with-ddd-cqrs-and-event-sourcing-part-1-of-2-52e0dc3d81df 83 | - https://medium.com/@qasimsoomro/building-microservices-using-node-js-with-ddd-cqrs-and-event-sourcing-part-2-of-2-9a5f6708e0f 84 | - https://nordfjord.io/blog/event-sourcing-in-nestjs 85 | 86 | 87 | 88 | 89 | [contributors-shield]: https://img.shields.io/github/contributors/aulasoftwarelibre/nestjs-eventstore.svg?style=for-the-badge 90 | [contributors-url]: https://github.com/aulasoftwarelibre/nestjs-eventstore/graphs/contributors 91 | [forks-shield]: https://img.shields.io/github/forks/aulasoftwarelibre/nestjs-eventstore.svg?style=for-the-badge 92 | [forks-url]: https://github.com/aulasoftwarelibre/nestjs-eventstore/network/members 93 | [stars-shield]: https://img.shields.io/github/stars/aulasoftwarelibre/nestjs-eventstore.svg?style=for-the-badge 94 | [stars-url]: https://github.com/aulasoftwarelibre/nestjs-eventstore/stargazers 95 | [issues-shield]: https://img.shields.io/github/issues/aulasoftwarelibre/nestjs-eventstore.svg?style=for-the-badge 96 | [issues-url]: https://github.com/aulasoftwarelibre/nestjs-eventstore/issues 97 | [license-shield]: https://img.shields.io/github/license/aulasoftwarelibre/nestjs-eventstore.svg?style=for-the-badge 98 | [license-url]: https://github.com/aulasoftwarelibre/nestjs-eventstore/blob/master/LICENSE 99 | -------------------------------------------------------------------------------- /packages/nestjs-eventstore/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@aulasoftwarelibre/nestjs-eventstore", 3 | "description": "NestJS module for eventsourcing development with eventstore database", 4 | "version": "0.8.0", 5 | "license": "EUPL-1.2", 6 | "author": "Sergio Gómez Bachiller ", 7 | "main": "./dist/index.js", 8 | "module": "./dist/index.mjs", 9 | "types": "./dist/index.d.ts", 10 | "files": [ 11 | "dist/**/*" 12 | ], 13 | "scripts": { 14 | "build": "tsup", 15 | "clean": "rm -rf dist", 16 | "dev": "tsup --watch", 17 | "lint": "eslint \"./**/*.ts*\"", 18 | "type-check": "tsc --noEmit", 19 | "prettier-check": "prettier --check \"./**/*.ts*\"", 20 | "test": "vitest" 21 | }, 22 | "keywords": [ 23 | "nestjs", 24 | "eventstore", 25 | "eventsourcing" 26 | ], 27 | "repository": { 28 | "type": "git", 29 | "url": "https://github.com/aulasoftwarelibre/nestjs-eventstore" 30 | }, 31 | "bugs": "https://github.com/aulasoftwarelibre/nestjs-eventstore/issues", 32 | "dependencies": { 33 | "@eventstore/db-client": "^6.2.1", 34 | "lodash.clonedeep": "^4.5.0", 35 | "nest-commander": "^3.11.0", 36 | "shallow-equal-object": "^1.1.1", 37 | "uuid": "^9.0.0" 38 | }, 39 | "devDependencies": { 40 | "@nestjs/common": "^10.0.4", 41 | "@nestjs/config": "^3.0.0", 42 | "@nestjs/core": "^10.0.4", 43 | "@nestjs/cqrs": "^10.0.1", 44 | "@nestjs/mongoose": "^10.0.0", 45 | "@nestjs/testing": "^10.0.4", 46 | "@swc/core": "^1.7.10", 47 | "@types/lodash.clonedeep": "^4.5.9", 48 | "@types/node": "^20.3.3", 49 | "@types/uuid": "^9.0.2", 50 | "mongoose": "^7.3.1", 51 | "reflect-metadata": "0.1.13", 52 | "rxjs": "^7.8.1", 53 | "tsup": "^8.2.4", 54 | "typescript": "^5.1.6" 55 | }, 56 | "peerDependencies": { 57 | "@eventstore/db-client": "^5.0.0 || ^6.0.0", 58 | "@nestjs/common": "^10.0.0", 59 | "@nestjs/config": "^3.0.0", 60 | "@nestjs/core": "^10.0.0", 61 | "@nestjs/cqrs": "^10.0.0", 62 | "@nestjs/mongoose": "^10.0.0", 63 | "mongoose": "^7.3.0", 64 | "nest-commander": "^3.11.0", 65 | "reflect-metadata": "^0.1.13", 66 | "rxjs": "^7.0.0" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /packages/nestjs-eventstore/src/aggregate.repository.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common' 2 | import { EventPublisher } from '@nestjs/cqrs' 3 | 4 | import { AggregateRoot, Event, Id } from './domain' 5 | import { EventStore } from './eventstore' 6 | import { KeyService } from './services' 7 | 8 | export class AggregateRepository { 9 | constructor( 10 | private readonly Aggregate: Type, 11 | private readonly eventStore: EventStore, 12 | private readonly publisher: EventPublisher, 13 | private readonly keyService: KeyService, 14 | ) {} 15 | 16 | public async find(id: U): Promise | null { 17 | return this.eventStore.read(this.Aggregate, id.value) 18 | } 19 | 20 | public save(entity: T) { 21 | entity = this.publisher.mergeObjectContext(entity) 22 | entity.commit() 23 | } 24 | 25 | public async delete(entity: T) { 26 | this.save(entity) 27 | 28 | await this.keyService.delete(entity.aggregateId()) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/nestjs-eventstore/src/crypto/aes/create-aes-key.ts: -------------------------------------------------------------------------------- 1 | import { generateHash } from '../hash' 2 | import { AESKey } from './types' 3 | 4 | export const createAesKey = async ( 5 | password: string | Buffer, 6 | salt: string | Buffer, 7 | ): Promise => { 8 | const derivedKey = await generateHash(password, salt, 4096, 48, 'sha256') 9 | const keyBuffer = Buffer.from(derivedKey) 10 | const key = keyBuffer.subarray(0, 32).toString('hex') 11 | const iv = derivedKey.subarray(32).toString('hex') 12 | 13 | return { iv, key } 14 | } 15 | -------------------------------------------------------------------------------- /packages/nestjs-eventstore/src/crypto/aes/decrypt-with-aes-key.ts: -------------------------------------------------------------------------------- 1 | import * as crypto from 'node:crypto' 2 | 3 | import { AESKey } from './types' 4 | 5 | export const decryptWithAesKey = (data: Buffer, aesKey: AESKey): Buffer => { 6 | const decipher = crypto.createDecipheriv( 7 | 'aes-256-cbc', 8 | Buffer.from(aesKey.key, 'hex'), 9 | Buffer.from(aesKey.iv, 'hex'), 10 | ) 11 | 12 | const bufferDecrypted = decipher.update(Buffer.from(data)) 13 | const bufferFinal = decipher.final() 14 | 15 | const decrypted = Buffer.concat([bufferDecrypted, bufferFinal]) 16 | 17 | return decrypted 18 | } 19 | -------------------------------------------------------------------------------- /packages/nestjs-eventstore/src/crypto/aes/encrypt-with-aes-key.ts: -------------------------------------------------------------------------------- 1 | import * as crypto from 'node:crypto' 2 | 3 | import { AESKey } from './types' 4 | 5 | export const encryptWithAesKey = (data: Buffer, aesKey: AESKey): Buffer => { 6 | const cipher = crypto.createCipheriv( 7 | 'aes-256-cbc', 8 | Buffer.from(aesKey.key, 'hex'), 9 | Buffer.from(aesKey.iv, 'hex'), 10 | ) 11 | 12 | const bufferEncrypted = cipher.update(Buffer.from(data)) 13 | const bufferFinal = cipher.final() 14 | 15 | const encrypted = Buffer.concat([bufferEncrypted, bufferFinal]) 16 | 17 | return encrypted 18 | } 19 | -------------------------------------------------------------------------------- /packages/nestjs-eventstore/src/crypto/aes/index.ts: -------------------------------------------------------------------------------- 1 | export * from './create-aes-key' 2 | export * from './decrypt-with-aes-key' 3 | export * from './encrypt-with-aes-key' 4 | export * from './types' 5 | -------------------------------------------------------------------------------- /packages/nestjs-eventstore/src/crypto/aes/types.ts: -------------------------------------------------------------------------------- 1 | export type AESKey = { 2 | iv: string 3 | key: string 4 | } 5 | -------------------------------------------------------------------------------- /packages/nestjs-eventstore/src/crypto/hash/generate-hash.ts: -------------------------------------------------------------------------------- 1 | import * as crypto from 'node:crypto' 2 | 3 | export const generateHash = ( 4 | data: string | Buffer, 5 | salt: string | Buffer, 6 | iterations: number, 7 | keylen: number, 8 | digest: string, 9 | ): Promise => { 10 | return new Promise((resolve, reject) => { 11 | crypto.pbkdf2( 12 | data, 13 | salt, 14 | iterations, 15 | keylen, 16 | digest, 17 | (error, derivedKey) => { 18 | if (error) { 19 | reject(error) 20 | } else { 21 | resolve(derivedKey) 22 | } 23 | }, 24 | ) 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /packages/nestjs-eventstore/src/crypto/hash/index.ts: -------------------------------------------------------------------------------- 1 | export * from './generate-hash' 2 | -------------------------------------------------------------------------------- /packages/nestjs-eventstore/src/crypto/index.ts: -------------------------------------------------------------------------------- 1 | export * from './aes' 2 | export * from './hash' 3 | export * from './key.dto' 4 | export * from './key.schema' 5 | -------------------------------------------------------------------------------- /packages/nestjs-eventstore/src/crypto/key.dto.ts: -------------------------------------------------------------------------------- 1 | export class KeyDto { 2 | public readonly _id: string 3 | public readonly secret: string 4 | public readonly salt: string 5 | } 6 | -------------------------------------------------------------------------------- /packages/nestjs-eventstore/src/crypto/key.schema.ts: -------------------------------------------------------------------------------- 1 | import { Document, Schema } from 'mongoose' 2 | 3 | import { KeyDto } from './key.dto' 4 | 5 | export const KEYS = 'keys' 6 | 7 | export type KeyDocument = KeyDto & Document 8 | 9 | export const KeySchema = new Schema( 10 | { 11 | _id: String, 12 | salt: String, 13 | secret: String, 14 | }, 15 | { 16 | versionKey: false, 17 | }, 18 | ) 19 | -------------------------------------------------------------------------------- /packages/nestjs-eventstore/src/decorators/index.ts: -------------------------------------------------------------------------------- 1 | export * from './inject-repository.decorator' 2 | -------------------------------------------------------------------------------- /packages/nestjs-eventstore/src/decorators/inject-repository.decorator.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Type } from '@nestjs/common' 2 | 3 | import { getRepositoryToken } from '../utils' 4 | 5 | export const InjectAggregateRepository = ( 6 | aggregate: Type, 7 | ): ParameterDecorator => Inject(getRepositoryToken(aggregate)) 8 | -------------------------------------------------------------------------------- /packages/nestjs-eventstore/src/domain/exceptions/domain-error.ts: -------------------------------------------------------------------------------- 1 | export class DomainError extends Error { 2 | protected constructor(stack?: string) { 3 | super(stack) 4 | } 5 | 6 | public static because(cause: string): DomainError { 7 | return new DomainError(cause) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/nestjs-eventstore/src/domain/exceptions/id-already-registered.error.ts: -------------------------------------------------------------------------------- 1 | import { Id } from '../models' 2 | 3 | export class IdAlreadyRegisteredError extends Error { 4 | public static withId(id: Id): IdAlreadyRegisteredError { 5 | return new IdAlreadyRegisteredError(`Id ${id.value} already taken.`) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/nestjs-eventstore/src/domain/exceptions/id-not-found.error.ts: -------------------------------------------------------------------------------- 1 | import { Id } from '../models' 2 | 3 | export class IdNotFoundError extends Error { 4 | public static withId(id: Id): IdNotFoundError { 5 | return new IdNotFoundError(`Id ${id.value} not found.`) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/nestjs-eventstore/src/domain/exceptions/index.ts: -------------------------------------------------------------------------------- 1 | export * from './domain-error' 2 | export * from './id-already-registered.error' 3 | export * from './id-not-found.error' 4 | export * from './invalid-event-error' 5 | export * from './invalid-id-error' 6 | -------------------------------------------------------------------------------- /packages/nestjs-eventstore/src/domain/exceptions/invalid-event-error.ts: -------------------------------------------------------------------------------- 1 | import { DomainError } from './domain-error' 2 | 3 | export class InvalidEventError extends DomainError { 4 | public static withType(type: string): InvalidEventError { 5 | return new InvalidEventError(`${type} is not a valid event.`) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/nestjs-eventstore/src/domain/exceptions/invalid-id-error.ts: -------------------------------------------------------------------------------- 1 | import { DomainError } from './domain-error' 2 | 3 | export class InvalidIdError extends DomainError { 4 | public static withString(value: string): InvalidIdError { 5 | return new InvalidIdError(`${value} is not a valid uuid v4.`) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/nestjs-eventstore/src/domain/index.ts: -------------------------------------------------------------------------------- 1 | export * from './exceptions' 2 | export * from './models' 3 | -------------------------------------------------------------------------------- /packages/nestjs-eventstore/src/domain/models/aggregate-root.ts: -------------------------------------------------------------------------------- 1 | import { AggregateRoot as BaseAggregateRoot } from '@nestjs/cqrs' 2 | 3 | import { InvalidEventError } from '../exceptions' 4 | import { Event } from './event' 5 | 6 | const VERSION = Symbol() 7 | const STREAM = Symbol() 8 | 9 | export abstract class AggregateRoot extends BaseAggregateRoot { 10 | protected [VERSION] = -1 11 | protected [STREAM] = this.constructor.name 12 | 13 | public abstract aggregateId(): string 14 | 15 | public get stream(): string { 16 | return this[STREAM] 17 | } 18 | 19 | public get version(): number { 20 | return this[VERSION] 21 | } 22 | 23 | apply = Event>( 24 | event: T, 25 | isFromHistory?: boolean, 26 | ): void 27 | apply = Event>( 28 | event: T, 29 | options?: { fromHistory?: boolean; skipHandler?: boolean }, 30 | ): void 31 | apply(event: unknown, options?: unknown): void { 32 | this[VERSION] += 1 33 | 34 | if (event instanceof Event === false) { 35 | throw InvalidEventError.withType(typeof event) 36 | } 37 | 38 | super.apply( 39 | event.withStream(this[STREAM]).withVersion(this[VERSION]), 40 | options, 41 | ) 42 | } 43 | } 44 | 45 | export abstract class EncryptedAggregateRoot extends AggregateRoot { 46 | apply = Event>( 47 | event: T, 48 | isFromHistory?: boolean, 49 | ): void 50 | apply = Event>( 51 | event: T, 52 | options?: { fromHistory?: boolean; skipHandler?: boolean }, 53 | ): void 54 | apply(event: unknown, options?: unknown): void { 55 | if (event instanceof Event === false) { 56 | throw InvalidEventError.withType(typeof event) 57 | } 58 | 59 | super.apply(event.withEncryptedAggregate(), options) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /packages/nestjs-eventstore/src/domain/models/event.ts: -------------------------------------------------------------------------------- 1 | import { IEvent } from '@nestjs/cqrs' 2 | import cloneDeep = require('lodash.clonedeep') 3 | import * as uuid from 'uuid' 4 | 5 | export type Metadata = { 6 | _aggregate_encrypted: boolean 7 | _aggregate_id: string 8 | _aggregate_version: number 9 | _encrypted_payload?: string | undefined 10 | _ocurred_on: number 11 | _stream?: string 12 | } 13 | 14 | export class Event

implements IEvent { 15 | public readonly eventId: string 16 | public readonly eventType: string 17 | private _payload: P 18 | private _metadata: Metadata 19 | 20 | public constructor(aggregateId: string, payload?: P) { 21 | this.eventId = uuid.v4() 22 | this._payload = { ...payload } 23 | this.eventType = Object.getPrototypeOf(this).constructor.name 24 | this._metadata = { 25 | _aggregate_encrypted: false, 26 | _aggregate_id: aggregateId, 27 | _aggregate_version: -2, 28 | _ocurred_on: Date.now(), 29 | } 30 | } 31 | 32 | get payload(): Readonly

{ 33 | return this._payload 34 | } 35 | 36 | get metadata(): Readonly { 37 | return this._metadata 38 | } 39 | 40 | get stream(): string { 41 | return this._metadata._stream 42 | } 43 | 44 | get aggregateId(): string { 45 | return this._metadata._aggregate_id 46 | } 47 | 48 | get aggregateEncrypted(): boolean { 49 | return this._metadata._aggregate_encrypted 50 | } 51 | 52 | get version(): number { 53 | return this._metadata._aggregate_version 54 | } 55 | 56 | get ocurredOn(): number { 57 | return this._metadata._ocurred_on 58 | } 59 | 60 | get encryptedPayload(): string | undefined { 61 | return this._metadata._encrypted_payload 62 | } 63 | 64 | withEncryptedAggregate(): Event { 65 | const event = cloneDeep(this) 66 | event._metadata = { 67 | ...this._metadata, 68 | _aggregate_encrypted: true, 69 | } 70 | 71 | return event 72 | } 73 | 74 | withEncryptedPayload(cryptedPayload: string): Event { 75 | const event = cloneDeep(this) 76 | 77 | return event.withPayload({} as P).withMetadata({ 78 | ...this._metadata, 79 | _encrypted_payload: cryptedPayload, 80 | }) 81 | } 82 | 83 | withMetadata(metadata: Metadata): Event

{ 84 | const event = cloneDeep(this) 85 | event._metadata = { 86 | ...metadata, 87 | } 88 | 89 | return event 90 | } 91 | 92 | withPayload(payload: P): Event { 93 | const event = cloneDeep(this) 94 | event._payload = { 95 | ...payload, 96 | } 97 | 98 | return event 99 | } 100 | 101 | withStream(stream: string): Event

{ 102 | const event = cloneDeep(this) 103 | event._metadata = { 104 | ...this._metadata, 105 | _stream: stream, 106 | } 107 | 108 | return event 109 | } 110 | 111 | withVersion(version: number): Event

{ 112 | const event = cloneDeep(this) 113 | event._metadata = { 114 | ...this._metadata, 115 | _aggregate_version: version, 116 | } 117 | 118 | return event 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /packages/nestjs-eventstore/src/domain/models/id.spec.ts: -------------------------------------------------------------------------------- 1 | import * as uuid from 'uuid' 2 | import { describe, expect, it } from 'vitest' 3 | 4 | import { InvalidIdError } from '../exceptions/invalid-id-error' 5 | import { Id } from './id' 6 | 7 | describe('Id', () => { 8 | it('creates a id value object', () => { 9 | const id = uuid.v4() 10 | const myId = MyId.fromString(id) 11 | 12 | expect(myId.value).toBe(id) 13 | }) 14 | 15 | it('returns exception if id is not valid', () => { 16 | expect(() => MyId.fromString('invalid')).toThrowError( 17 | InvalidIdError.withString('invalid'), 18 | ) 19 | }) 20 | }) 21 | 22 | class MyId extends Id { 23 | private constructor(id: string) { 24 | super(id) 25 | } 26 | 27 | static fromString(id: string): MyId { 28 | return new this(id) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/nestjs-eventstore/src/domain/models/id.ts: -------------------------------------------------------------------------------- 1 | import { validate, version } from 'uuid' 2 | 3 | import { InvalidIdError } from '../exceptions' 4 | import { ValueObject } from './value-object' 5 | 6 | export abstract class Id extends ValueObject<{ 7 | value: string 8 | }> { 9 | protected constructor(id: string) { 10 | if (!validate(id) || version(id) !== 4) { 11 | throw InvalidIdError.withString(id) 12 | } 13 | 14 | super({ value: id }) 15 | } 16 | 17 | get value(): string { 18 | return this.props.value 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/nestjs-eventstore/src/domain/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './aggregate-root' 2 | export * from './event' 3 | export * from './id' 4 | export * from './value-object' 5 | -------------------------------------------------------------------------------- /packages/nestjs-eventstore/src/domain/models/value-object.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest' 2 | 3 | import { ValueObject } from './value-object' 4 | 5 | describe('ValueObject', () => { 6 | it('creates a value object with one attribute', () => { 7 | const foo = FooValueObject.fromString('foo') 8 | 9 | expect(foo.value).toBe('foo') 10 | }) 11 | 12 | it('creates a value object with many attributes', () => { 13 | const person = FullName.from('John', 'Doe') 14 | 15 | expect(person.first).toBe('John') 16 | expect(person.last).toBe('Doe') 17 | }) 18 | 19 | it('a value objects is equal with itself', () => { 20 | const foo = FooValueObject.fromString('foo') 21 | 22 | expect(foo.equals(foo)).toBeTruthy() 23 | }) 24 | 25 | it('two value objects with same values are equals', () => { 26 | const foo = FooValueObject.fromString('foo') 27 | const foo2 = FooValueObject.fromString('foo') 28 | 29 | expect(foo.equals(foo2)).toBeTruthy() 30 | }) 31 | 32 | it('two value objects with different values are not equals', () => { 33 | const foo = FooValueObject.fromString('foo') 34 | const bar = FooValueObject.fromString('bar') 35 | 36 | expect(foo.equals(bar)).toBeFalsy() 37 | }) 38 | 39 | it('two different value objects with same values are not equals', () => { 40 | const foo = FooValueObject.fromString('foo') 41 | const bar = BarValueObject.fromString('foo') 42 | 43 | expect(foo.equals(bar)).toBeFalsy() 44 | }) 45 | }) 46 | 47 | class FooValueObject extends ValueObject<{ value: string }> { 48 | public static fromString(value: string): FooValueObject { 49 | return new FooValueObject({ value }) 50 | } 51 | 52 | get value(): string { 53 | return this.props.value 54 | } 55 | } 56 | 57 | class BarValueObject extends ValueObject<{ value: string }> { 58 | public static fromString(value: string): BarValueObject { 59 | return new BarValueObject({ value }) 60 | } 61 | 62 | get value(): string { 63 | return this.props.value 64 | } 65 | } 66 | 67 | class FullName extends ValueObject<{ first: string; last: string }> { 68 | public static from(first: string, last: string): FullName { 69 | return new FullName({ first, last }) 70 | } 71 | 72 | get first(): string { 73 | return this.props.first 74 | } 75 | 76 | get last(): string { 77 | return this.props.last 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /packages/nestjs-eventstore/src/domain/models/value-object.ts: -------------------------------------------------------------------------------- 1 | import { shallowEqual } from 'shallow-equal-object' 2 | 3 | interface ValueObjectProperties { 4 | [index: string]: unknown 5 | } 6 | 7 | export abstract class ValueObject { 8 | public readonly props: T 9 | 10 | protected constructor(properties: T) { 11 | this.props = Object.freeze(properties) 12 | } 13 | 14 | public equals(other: ValueObject): boolean { 15 | if (this.constructor !== other.constructor) { 16 | return false 17 | } 18 | 19 | return shallowEqual(this.props, other.props) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/nestjs-eventstore/src/errors/index.ts: -------------------------------------------------------------------------------- 1 | export * from './key-not-found.error' 2 | export * from './transformer-not-found.error' 3 | -------------------------------------------------------------------------------- /packages/nestjs-eventstore/src/errors/key-not-found.error.ts: -------------------------------------------------------------------------------- 1 | export class KeyNotFoundError extends Error { 2 | public static withId(id: string) { 3 | return new KeyNotFoundError(`Decrypt key for [${id}] does not exists`) 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/nestjs-eventstore/src/errors/transformer-not-found.error.ts: -------------------------------------------------------------------------------- 1 | export class TransformerNotFoundError extends Error { 2 | public static withType(type: string) { 3 | return new TransformerNotFoundError(`Missed ${type} event map transformer`) 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/nestjs-eventstore/src/eventstore-core.module.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DynamicModule, 3 | Global, 4 | Module, 5 | OnModuleInit, 6 | Provider, 7 | } from '@nestjs/common' 8 | import { CqrsModule, EventBus } from '@nestjs/cqrs' 9 | import { ExplorerService } from '@nestjs/cqrs/dist/services/explorer.service' 10 | import { MongooseModule } from '@nestjs/mongoose' 11 | 12 | import { KEYS, KeySchema } from './crypto' 13 | import { Event } from './domain' 14 | import { EventStore } from './eventstore' 15 | import { EventStoreRestoreCommand } from './eventstore.cli' 16 | import { Config } from './eventstore.config' 17 | import { 18 | EVENTSTORE_KEYSTORE_CONNECTION, 19 | EVENTSTORE_SETTINGS_TOKEN, 20 | } from './eventstore.constants' 21 | import { EventStoreMapper } from './eventstore.mapper' 22 | import { ConfigService, EventStoreModuleAsyncOptions } from './interfaces' 23 | import { KeyService, ProjectionsService, TransformerService } from './services' 24 | 25 | @Global() 26 | @Module({ 27 | exports: [EventStore, KeyService], 28 | imports: [ 29 | CqrsModule, 30 | MongooseModule.forFeature( 31 | [ 32 | { 33 | name: KEYS, 34 | schema: KeySchema, 35 | }, 36 | ], 37 | EVENTSTORE_KEYSTORE_CONNECTION, 38 | ), 39 | ], 40 | providers: [ 41 | EventStore, 42 | EventStoreMapper, 43 | EventStoreRestoreCommand, 44 | ExplorerService, 45 | KeyService, 46 | ProjectionsService, 47 | TransformerService, 48 | ], 49 | }) 50 | export class EventStoreCoreModule implements OnModuleInit { 51 | constructor( 52 | private readonly event$: EventBus, 53 | private readonly eventStore: EventStore, 54 | ) {} 55 | 56 | public static forRoot(config: Config): DynamicModule { 57 | return { 58 | exports: [EventStore], 59 | module: EventStoreCoreModule, 60 | providers: [{ provide: EVENTSTORE_SETTINGS_TOKEN, useValue: config }], 61 | } 62 | } 63 | 64 | public static forRootAsync( 65 | options: EventStoreModuleAsyncOptions, 66 | ): DynamicModule { 67 | return { 68 | module: EventStoreCoreModule, 69 | providers: [this.createAsyncProvider(options)], 70 | } 71 | } 72 | 73 | private static createAsyncProvider( 74 | options: EventStoreModuleAsyncOptions, 75 | ): Provider { 76 | if ('useFactory' in options) { 77 | return { 78 | provide: EVENTSTORE_SETTINGS_TOKEN, 79 | ...options, 80 | } 81 | } 82 | 83 | return { 84 | provide: EVENTSTORE_SETTINGS_TOKEN, 85 | useFactory: async (optionsFactory: ConfigService) => 86 | optionsFactory.createEventStoreConfig(), 87 | ...('useClass' in options 88 | ? { inject: [options.useClass], scope: options.scope } 89 | : { inject: [options.useExisting] }), 90 | } 91 | } 92 | 93 | onModuleInit() { 94 | this.eventStore.bridgeEventsTo(this.event$.subject$) 95 | this.event$.publisher = this.eventStore 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /packages/nestjs-eventstore/src/eventstore.cli.ts: -------------------------------------------------------------------------------- 1 | import { EventStoreDBClient, FORWARDS, START } from '@eventstore/db-client' 2 | import { Inject, Logger } from '@nestjs/common' 3 | import { Command, CommandRunner } from 'nest-commander' 4 | 5 | import { Event } from './domain' 6 | import { type Config } from './eventstore.config' 7 | import { EVENTSTORE_SETTINGS_TOKEN } from './eventstore.constants' 8 | import { EventStoreMapper } from './eventstore.mapper' 9 | import { ProjectionsService } from './services' 10 | 11 | @Command({ 12 | description: 'Restore read model', 13 | name: 'eventstore:readmodel:restore', 14 | }) 15 | export class EventStoreRestoreCommand extends CommandRunner { 16 | private readonly client: EventStoreDBClient 17 | private readonly logger = new Logger(EventStoreRestoreCommand.name) 18 | private readonly eventHandlers 19 | 20 | constructor( 21 | private readonly mapper: EventStoreMapper, 22 | projections: ProjectionsService, 23 | @Inject(EVENTSTORE_SETTINGS_TOKEN) config: Config, 24 | ) { 25 | super() 26 | this.client = EventStoreDBClient.connectionString(config.connection) 27 | this.eventHandlers = projections.eventHandlers() 28 | } 29 | 30 | async run(): Promise { 31 | const resolvedEvents = this.client.readAll({ 32 | direction: FORWARDS, 33 | fromPosition: START, 34 | resolveLinkTos: false, 35 | }) 36 | 37 | for await (const resolvedEvent of resolvedEvents) { 38 | if (resolvedEvent.event?.type.startsWith('$')) { 39 | continue 40 | } 41 | 42 | const event = await this.mapper.resolvedEventToDomainEvent(resolvedEvent) 43 | 44 | if (!event) continue 45 | 46 | await this.handleEvent(event) 47 | } 48 | 49 | this.logger.log('Projections have been restored!') 50 | process.exit(0) 51 | } 52 | 53 | private async handleEvent(event: Event) { 54 | const key = event.constructor.name 55 | for (const eventHandler of this.eventHandlers[key]) { 56 | await eventHandler.handle(event) 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /packages/nestjs-eventstore/src/eventstore.config.ts: -------------------------------------------------------------------------------- 1 | export interface Config { 2 | connection: string 3 | } 4 | -------------------------------------------------------------------------------- /packages/nestjs-eventstore/src/eventstore.constants.ts: -------------------------------------------------------------------------------- 1 | export const EVENTSTORE_SETTINGS_TOKEN = 'EVENTSTORE_SETTINGS_TOKEN' 2 | export const EVENTSTORE_KEYSTORE_CONNECTION = 'keystore' 3 | -------------------------------------------------------------------------------- /packages/nestjs-eventstore/src/eventstore.mapper.ts: -------------------------------------------------------------------------------- 1 | import { JSONType, ResolvedEvent } from '@eventstore/db-client' 2 | import { Inject, Injectable, Logger } from '@nestjs/common' 3 | 4 | import { Event, Metadata } from './domain' 5 | import { KeyNotFoundError } from './errors' 6 | import { type Config } from './eventstore.config' 7 | import { EVENTSTORE_SETTINGS_TOKEN } from './eventstore.constants' 8 | import { KeyService, TransformerService } from './services' 9 | 10 | @Injectable() 11 | export class EventStoreMapper { 12 | private readonly logger = new Logger(EventStoreMapper.name) 13 | 14 | constructor( 15 | @Inject(EVENTSTORE_SETTINGS_TOKEN) private readonly config: Config, 16 | private readonly transformers: TransformerService, 17 | private readonly keyService: KeyService, 18 | ) {} 19 | 20 | public async resolvedEventToDomainEvent( 21 | resolvedEvent: ResolvedEvent, 22 | ): Promise | null { 23 | if ( 24 | resolvedEvent.event === undefined || 25 | resolvedEvent.event.type.startsWith('$') 26 | ) { 27 | return null 28 | } 29 | 30 | try { 31 | const metadata = resolvedEvent.event.metadata as Metadata 32 | const payload = await this.extractPayload(resolvedEvent) 33 | const transformer = this.transformers.getTransformerToEvent(resolvedEvent) 34 | 35 | const event = transformer?.( 36 | new Event(metadata._aggregate_id, payload), 37 | ).withMetadata(metadata) 38 | 39 | return event 40 | } catch (error) { 41 | if (error instanceof KeyNotFoundError) { 42 | this.logger.error( 43 | `Error during decrypting ${resolvedEvent.event.type}: ${error.message}`, 44 | ) 45 | 46 | return null 47 | } 48 | 49 | throw error 50 | } 51 | } 52 | 53 | private async extractPayload( 54 | resolvedEvent: ResolvedEvent, 55 | ): Promise { 56 | const metadata = resolvedEvent.event.metadata as Metadata 57 | return metadata._encrypted_payload 58 | ? await this.keyService.decryptPayload( 59 | metadata._aggregate_id, 60 | metadata._encrypted_payload, 61 | ) 62 | : resolvedEvent.event.data 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /packages/nestjs-eventstore/src/eventstore.module.ts: -------------------------------------------------------------------------------- 1 | import { DynamicModule, Module, Provider, Type } from '@nestjs/common' 2 | import { CqrsModule, EventPublisher } from '@nestjs/cqrs' 3 | 4 | import { AggregateRepository } from './aggregate.repository' 5 | import { AggregateRoot } from './domain' 6 | import { EventStore } from './eventstore' 7 | import { Config } from './eventstore.config' 8 | import { EventStoreCoreModule } from './eventstore-core.module' 9 | import { EventStoreModuleAsyncOptions, TransformerRepo } from './interfaces' 10 | import { EVENT_STORE_TRANSFORMERS_TOKEN, KeyService } from './services' 11 | import { getRepositoryToken } from './utils' 12 | 13 | @Module({}) 14 | export class EventStoreModule { 15 | public static forRoot(options: Config): DynamicModule { 16 | return { 17 | imports: [EventStoreCoreModule.forRoot(options)], 18 | module: EventStoreModule, 19 | } 20 | } 21 | 22 | public static forRootAsync( 23 | options: EventStoreModuleAsyncOptions, 24 | ): DynamicModule { 25 | return { 26 | imports: [CqrsModule, EventStoreCoreModule.forRootAsync(options)], 27 | module: EventStoreModule, 28 | } 29 | } 30 | 31 | public static forFeature( 32 | aggregateRoots: Array>, 33 | transformer: TransformerRepo, 34 | ): DynamicModule { 35 | const aggregateRepoProviders = 36 | this.createAggregateRepositoryProviders(aggregateRoots) 37 | 38 | const transformersProvider = { 39 | provide: EVENT_STORE_TRANSFORMERS_TOKEN, 40 | useValue: transformer, 41 | } 42 | 43 | return { 44 | exports: [transformersProvider, ...aggregateRepoProviders], 45 | imports: [CqrsModule], 46 | module: EventStoreModule, 47 | providers: [transformersProvider, ...aggregateRepoProviders], 48 | } 49 | } 50 | 51 | private static createAggregateRepositoryProviders( 52 | aggregateRoots: Array>, 53 | ): Provider[] { 54 | return aggregateRoots.map((aggregateRoot) => ({ 55 | inject: [EventStore, EventPublisher, KeyService], 56 | provide: getRepositoryToken(aggregateRoot), 57 | useFactory: ( 58 | eventStore: EventStore, 59 | publisher: EventPublisher, 60 | keyService: KeyService, 61 | ) => 62 | new AggregateRepository( 63 | aggregateRoot, 64 | eventStore, 65 | publisher, 66 | keyService, 67 | ), 68 | })) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /packages/nestjs-eventstore/src/eventstore.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AppendExpectedRevision, 3 | END, 4 | ErrorType, 5 | EventData, 6 | EventStoreDBClient, 7 | FORWARDS, 8 | jsonEvent, 9 | JSONType, 10 | NO_STREAM, 11 | ResolvedEvent, 12 | START, 13 | } from '@eventstore/db-client' 14 | import { Inject, Injectable, Logger, Type } from '@nestjs/common' 15 | import { IEventPublisher, IMessageSource } from '@nestjs/cqrs' 16 | import { Subject } from 'rxjs' 17 | import { v4 as uuid } from 'uuid' 18 | 19 | import { AggregateRoot, Event } from './domain' 20 | import { type Config } from './eventstore.config' 21 | import { EVENTSTORE_SETTINGS_TOKEN } from './eventstore.constants' 22 | import { EventStoreMapper } from './eventstore.mapper' 23 | import { KeyService } from './services' 24 | 25 | @Injectable() 26 | export class EventStore 27 | implements IEventPublisher, IMessageSource 28 | { 29 | private client: EventStoreDBClient 30 | private readonly logger = new Logger(EventStore.name) 31 | 32 | constructor( 33 | @Inject(EVENTSTORE_SETTINGS_TOKEN) private readonly config: Config, 34 | private readonly mapper: EventStoreMapper, 35 | private readonly keyService: KeyService, 36 | ) { 37 | this.client = EventStoreDBClient.connectionString(config.connection) 38 | } 39 | 40 | async publishAll(events: T[]) { 41 | events = [...events] 42 | 43 | if (events.length === 0) { 44 | return 45 | } 46 | 47 | const streamName = this.getStreamName(events[0]) 48 | const expectedRevision = this.getExpectedRevision(events[0]) 49 | 50 | const eventsData = [] 51 | for (const event of events) { 52 | const eventData = await this.createEventData(event) 53 | 54 | eventsData.push(eventData) 55 | } 56 | 57 | try { 58 | this.client.appendToStream(streamName, eventsData, { 59 | expectedRevision: expectedRevision, 60 | }) 61 | } catch (error) { 62 | this.logger.error(`Error publishing all events: ${error.message}`) 63 | } 64 | } 65 | 66 | async publish(event: T) { 67 | const streamName = this.getStreamName(event) 68 | const expectedRevision = this.getExpectedRevision(event) 69 | 70 | const eventData = await this.createEventData(event) 71 | 72 | try { 73 | await this.client.appendToStream(streamName, eventData, { 74 | expectedRevision: expectedRevision, 75 | }) 76 | } catch (error) { 77 | this.logger.error(`Error publishing event: ${error.message}`) 78 | } 79 | } 80 | 81 | async read( 82 | aggregate: Type, 83 | id: string, 84 | ): Promise | null { 85 | const streamName = `${aggregate.name}-${id}` 86 | 87 | try { 88 | const entity = Reflect.construct(aggregate, []) 89 | const resolvedEvents = await this.client.readStream(streamName, { 90 | direction: FORWARDS, 91 | fromRevision: START, 92 | }) 93 | 94 | const events = [] as Event[] 95 | 96 | for await (const event of resolvedEvents) { 97 | events.push(await this.mapper.resolvedEventToDomainEvent(event)) 98 | } 99 | 100 | entity.loadFromHistory(events) 101 | 102 | return entity 103 | } catch (error) { 104 | if (error?.type === ErrorType.STREAM_NOT_FOUND) { 105 | return null 106 | } 107 | 108 | this.logger.error(error) 109 | } 110 | 111 | return null 112 | } 113 | 114 | async bridgeEventsTo(subject: Subject) { 115 | const onEvent = async (resolvedEvent: ResolvedEvent) => { 116 | if (resolvedEvent.event?.type.startsWith('$')) { 117 | return 118 | } 119 | 120 | subject.next( 121 | await this.mapper.resolvedEventToDomainEvent(resolvedEvent), 122 | ) 123 | } 124 | 125 | try { 126 | await this.client 127 | .subscribeToAll({ 128 | fromPosition: END, 129 | }) 130 | .on('data', onEvent) 131 | } catch (error) { 132 | this.logger.error(error) 133 | } 134 | } 135 | 136 | private getStreamName(event: T) { 137 | return `${event.stream}-${event.aggregateId}` 138 | } 139 | 140 | private getExpectedRevision( 141 | event: T, 142 | ): AppendExpectedRevision { 143 | return event.version <= 0 ? NO_STREAM : BigInt(event.version - 1) 144 | } 145 | 146 | private async createEventData(event: T): Promise { 147 | if (event.aggregateEncrypted) { 148 | event = (await this.keyService.encryptEvent(event)) as T 149 | } 150 | 151 | return jsonEvent({ 152 | data: event.payload as JSONType, 153 | id: uuid(), 154 | metadata: event.metadata, 155 | type: event.eventType, 156 | }) 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /packages/nestjs-eventstore/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './aggregate.repository' 2 | export * from './decorators/inject-repository.decorator' 3 | export * from './domain' 4 | export * from './errors' 5 | export * from './eventstore' 6 | export * from './eventstore.config' 7 | export * from './eventstore.constants' 8 | export * from './eventstore.module' 9 | export * from './interfaces/eventstore-module.interface' 10 | -------------------------------------------------------------------------------- /packages/nestjs-eventstore/src/interfaces/eventstore-module.interface.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ClassProvider, 3 | ExistingProvider, 4 | FactoryProvider, 5 | } from '@nestjs/common' 6 | 7 | import { Config } from '../eventstore.config' 8 | 9 | export interface ConfigService { 10 | createEventStoreConfig: () => Config | Promise 11 | } 12 | 13 | export type EventStoreModuleAsyncOptions = 14 | | Omit, 'provide'> 15 | | Omit, 'provide'> 16 | | Omit, 'provide'> 17 | -------------------------------------------------------------------------------- /packages/nestjs-eventstore/src/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './eventstore-module.interface' 2 | export * from './transformer.type' 3 | -------------------------------------------------------------------------------- /packages/nestjs-eventstore/src/interfaces/transformer.type.ts: -------------------------------------------------------------------------------- 1 | import { Event } from '../domain' 2 | 3 | export type Transformer = (event: Event) => Event 4 | 5 | export interface TransformerRepo { 6 | [aggregate: string]: Transformer 7 | } 8 | -------------------------------------------------------------------------------- /packages/nestjs-eventstore/src/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './key.service' 2 | export * from './projections.service' 3 | export * from './transformer.service' 4 | -------------------------------------------------------------------------------- /packages/nestjs-eventstore/src/services/key.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { getModelToken } from '@nestjs/mongoose' 2 | import { Test } from '@nestjs/testing' 3 | import { Model } from 'mongoose' 4 | import { v4 as uuid } from 'uuid' 5 | import { beforeEach, describe, expect, it, vi } from 'vitest' 6 | 7 | import { KeyDocument, KeyDto, KEYS } from '../crypto' 8 | import { Event } from '../domain' 9 | import { EVENTSTORE_KEYSTORE_CONNECTION } from '../eventstore.constants' 10 | import { KeyService } from './key.service' 11 | 12 | const generateKey = (_id: string): KeyDto => ({ 13 | _id, 14 | salt: 'salt', 15 | secret: 'secret', 16 | }) 17 | 18 | describe('KeyService', () => { 19 | const PAYLOAD = { foo: 'bar' } 20 | const ENCRYPTED_PAYLOAD = 'z6ZatDe7V2Dvkxyvx9fzazqjc5BOWqSTpUaQzrkUeR4=' 21 | 22 | let keyService: KeyService 23 | let keys: Model 24 | 25 | beforeEach(async () => { 26 | const moduleReference = await Test.createTestingModule({ 27 | providers: [ 28 | KeyService, 29 | { 30 | provide: getModelToken(KEYS, EVENTSTORE_KEYSTORE_CONNECTION), 31 | useValue: { 32 | findById: vi.fn(), 33 | findByIdAndRemove: vi.fn(), 34 | }, 35 | }, 36 | ], 37 | }).compile() 38 | 39 | keyService = moduleReference.get(KeyService) 40 | keys = moduleReference.get>( 41 | getModelToken(KEYS, EVENTSTORE_KEYSTORE_CONNECTION), 42 | ) 43 | }) 44 | 45 | describe('encrypt', () => { 46 | it('should be able to encrypt an event', async () => { 47 | const aggregateId = uuid() 48 | const event = new Event(aggregateId, PAYLOAD) 49 | 50 | vi.spyOn(keys, 'findById').mockReturnValue({ 51 | lean: vi.fn().mockResolvedValueOnce(generateKey(aggregateId)), 52 | } as unknown as ReturnType<(typeof keys)['findById']>) 53 | 54 | const result = await keyService.encryptEvent(event) 55 | 56 | expect(keys.findById).toBeCalledWith(aggregateId) 57 | expect(result.encryptedPayload).toEqual(ENCRYPTED_PAYLOAD) 58 | }) 59 | }) 60 | 61 | describe('decrypt', () => { 62 | it('should be able to decrypt a string', async () => { 63 | const aggregateId = uuid() 64 | 65 | vi.spyOn(keys, 'findById').mockReturnValue({ 66 | lean: vi.fn().mockResolvedValueOnce(generateKey(aggregateId)), 67 | } as unknown as ReturnType<(typeof keys)['findById']>) 68 | 69 | const result = await keyService.decryptPayload( 70 | aggregateId, 71 | ENCRYPTED_PAYLOAD, 72 | ) 73 | 74 | expect(keys.findById).toBeCalledWith(aggregateId) 75 | expect(result).toEqual(PAYLOAD) 76 | }) 77 | }) 78 | }) 79 | -------------------------------------------------------------------------------- /packages/nestjs-eventstore/src/services/key.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Logger } from '@nestjs/common' 2 | import { InjectModel } from '@nestjs/mongoose' 3 | import { Model } from 'mongoose' 4 | import { v4 as uuid } from 'uuid' 5 | 6 | import { 7 | createAesKey, 8 | decryptWithAesKey, 9 | encryptWithAesKey, 10 | KeyDocument, 11 | KeyDto, 12 | KEYS, 13 | } from '../crypto' 14 | import { Event } from '../domain' 15 | import { KeyNotFoundError } from '../errors' 16 | import { EVENTSTORE_KEYSTORE_CONNECTION } from '../eventstore.constants' 17 | 18 | @Injectable() 19 | export class KeyService { 20 | private readonly logger = new Logger(KeyService.name) 21 | 22 | constructor( 23 | @InjectModel(KEYS, EVENTSTORE_KEYSTORE_CONNECTION) 24 | private readonly keys: Model, 25 | ) {} 26 | 27 | async create(id: string): Promise { 28 | const secret = uuid() 29 | const salt = uuid() 30 | 31 | const key = new this.keys({ _id: id, salt, secret }) 32 | 33 | return key.save() 34 | } 35 | 36 | async find(id: string): Promise { 37 | return this.keys.findById(id).lean() 38 | } 39 | 40 | async delete(id: string): Promise { 41 | this.keys.findByIdAndRemove(id).exec() 42 | } 43 | 44 | async encryptEvent(event: Event): Promise { 45 | const key = 46 | event.version === 0 47 | ? await this.create(event.aggregateId) 48 | : await this.find(event.aggregateId) 49 | 50 | if (!key) { 51 | this.logger.error( 52 | `Key not found during encrypting event ${event.eventType} with id [${event.aggregateId}]`, 53 | ) 54 | return event 55 | } 56 | 57 | const data = JSON.stringify(event.payload) 58 | 59 | const aesKey = await createAesKey(key.secret, key.salt) 60 | const buffer = encryptWithAesKey(Buffer.from(data, 'utf16le'), aesKey) 61 | 62 | return event.withEncryptedPayload(buffer.toString('base64')) 63 | } 64 | 65 | async decryptPayload(id: string, encryptedPayload: string): Promise { 66 | const key = await this.find(id) 67 | 68 | if (!key) { 69 | throw KeyNotFoundError.withId(id) 70 | } 71 | 72 | const aesKey = await createAesKey(key.secret, key.salt) 73 | const buffer = decryptWithAesKey( 74 | Buffer.from(encryptedPayload, 'base64'), 75 | aesKey, 76 | ) 77 | const payload = JSON.parse(buffer.toString('utf16le')) 78 | 79 | return payload 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /packages/nestjs-eventstore/src/services/projections.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common' 2 | import { ModuleRef } from '@nestjs/core' 3 | import { IEventHandler } from '@nestjs/cqrs' 4 | import { EVENTS_HANDLER_METADATA } from '@nestjs/cqrs/dist/decorators/constants' 5 | import { ExplorerService } from '@nestjs/cqrs/dist/services/explorer.service' 6 | 7 | @Injectable() 8 | export class ProjectionsService { 9 | constructor( 10 | private readonly explorer: ExplorerService, 11 | private readonly moduleReference: ModuleRef, 12 | ) {} 13 | 14 | public eventHandlers(): Record { 15 | return this.explorer.explore().events.reduce((previous, handler) => { 16 | const instance = this.moduleReference.get(handler, { strict: false }) 17 | 18 | if (!instance) { 19 | return previous 20 | } 21 | 22 | const eventsNames = Reflect.getMetadata(EVENTS_HANDLER_METADATA, handler) 23 | 24 | eventsNames.map((event) => { 25 | const key = event.name 26 | 27 | previous[key] = previous[key] 28 | ? [...previous[key], instance] 29 | : [instance] 30 | }) 31 | 32 | return previous 33 | }, {}) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/nestjs-eventstore/src/services/transformer.service.ts: -------------------------------------------------------------------------------- 1 | import { ResolvedEvent } from '@eventstore/db-client' 2 | import { Injectable } from '@nestjs/common' 3 | import { ModulesContainer } from '@nestjs/core' 4 | 5 | import { TransformerNotFoundError } from '../errors' 6 | import { Transformer, TransformerRepo } from '../interfaces' 7 | 8 | export const EVENT_STORE_TRANSFORMERS_TOKEN = 'EVENT_STORE_TRANSFORMERS_TOKEN' 9 | 10 | @Injectable() 11 | export class TransformerService { 12 | private readonly repo: TransformerRepo 13 | 14 | constructor(private readonly modules: ModulesContainer) { 15 | const transformers = [...this.modules.values()] 16 | .flatMap((module) => [...module.providers.values()]) 17 | .filter(({ name }) => name === EVENT_STORE_TRANSFORMERS_TOKEN) 18 | .flatMap(({ instance }) => Object.entries(instance as TransformerRepo)) 19 | 20 | this.repo = Object.fromEntries(transformers) 21 | } 22 | 23 | public getTransformerToEvent( 24 | resolvedEvent: ResolvedEvent, 25 | ): Transformer | null { 26 | const type = resolvedEvent.event.type 27 | 28 | const transformer = this.repo[type] 29 | if (!transformer) { 30 | throw TransformerNotFoundError.withType(type) 31 | } 32 | 33 | return transformer 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/nestjs-eventstore/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './repository' 2 | -------------------------------------------------------------------------------- /packages/nestjs-eventstore/src/utils/repository.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common' 2 | 3 | export const getRepositoryToken = (aggregate: Type): string => 4 | `${aggregate.name}AggregateRepository` 5 | -------------------------------------------------------------------------------- /packages/nestjs-eventstore/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "target": "es2019", 9 | "sourceMap": false, 10 | "outDir": "./dist", 11 | "rootDir": "./src", 12 | "baseUrl": "./", 13 | "noLib": false 14 | }, 15 | "include": ["src/**/*"], 16 | "exclude": ["node_modules", "dist", "**/*.spec.ts"] 17 | } 18 | -------------------------------------------------------------------------------- /packages/nestjs-eventstore/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | 3 | export default defineConfig([ 4 | { 5 | dts: true, 6 | entry: ['src/index.ts'], 7 | format: ['cjs', 'esm'], 8 | sourcemap: true, 9 | }, 10 | ]) 11 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'examples/*' 3 | - 'packages/*' 4 | --------------------------------------------------------------------------------