├── .eslintignore ├── .eslintrc.json ├── .github └── workflows │ ├── codeql-analysis.yml │ └── publish_workflow.yml ├── .gitignore ├── .husky └── pre-commit ├── .nvmrc ├── .prettierrc ├── .releaserc ├── LICENSE ├── README.md ├── documentation ├── classes │ ├── AcknowledgeableEventStoreEvent.html │ ├── DropAncientItemCommand.html │ ├── EventStore.html │ ├── EventStoreAggregateRoot.html │ ├── EventStoreBusHealthIndicator.html │ ├── EventStoreEvent.html │ ├── EventStoreHealthIndicator.html │ ├── EventStoreObserverHealthIndicator.html │ ├── GetHeroesQuery.html │ ├── Hero.html │ ├── HeroDamagedEnemyEvent.html │ ├── HeroDropItemEvent.html │ ├── HeroFoundItemEvent.html │ ├── HeroKilledDragonEvent.html │ └── KillDragonCommand.html ├── coverage.html ├── fonts │ ├── FontAwesome.otf │ ├── fontawesome-webfont.eot │ ├── fontawesome-webfont.svg │ ├── fontawesome-webfont.ttf │ ├── fontawesome-webfont.woff │ ├── fontawesome-webfont.woff2 │ ├── roboto-v15-latin-300.eot │ ├── roboto-v15-latin-300.svg │ ├── roboto-v15-latin-300.ttf │ ├── roboto-v15-latin-300.woff │ ├── roboto-v15-latin-300.woff2 │ ├── roboto-v15-latin-700.eot │ ├── roboto-v15-latin-700.svg │ ├── roboto-v15-latin-700.ttf │ ├── roboto-v15-latin-700.woff │ ├── roboto-v15-latin-700.woff2 │ ├── roboto-v15-latin-regular.eot │ ├── roboto-v15-latin-regular.svg │ ├── roboto-v15-latin-regular.ttf │ ├── roboto-v15-latin-regular.woff │ └── roboto-v15-latin-regular.woff2 ├── graph │ └── dependencies.svg ├── images │ ├── compodoc-vectorise-inverted.svg │ ├── compodoc-vectorise.svg │ ├── compodoc.png │ ├── compodoc.svg │ └── favicon.ico ├── index.html ├── injectables │ ├── EventStoreBus.html │ ├── EventStoreInterceptor.html │ ├── EventStoreObserver.html │ ├── EventStorePublisher.html │ ├── HeroRepository.html │ └── HeroesGameSagas.html ├── interfaces │ ├── Constructor.html │ ├── IAcknowledgeableEvent.html │ ├── IAggregateEvent.html │ ├── IEventStoreBusConfig.html │ ├── IEventStoreConfig.html │ ├── IEventStoreEventOptions.html │ ├── IExpectedVersionEvent.html │ ├── IHttpEndpoint.html │ ├── IStreamConfig.html │ ├── IStreamMetadata.html │ ├── ISubscriptionStatus.html │ └── KillDragonDto.html ├── js │ ├── compodoc.js │ ├── libs │ │ ├── EventDispatcher.js │ │ ├── bootstrap-native.js │ │ ├── es6-shim.min.js │ │ ├── highlight.pack.js │ │ ├── highlightjs-line-numbers.min.js │ │ ├── promise.min.js │ │ ├── svg-pan-zoom.min.js │ │ └── zepto.min.js │ ├── menu.js │ ├── search │ │ ├── lunr.min.js │ │ ├── search-engine.js │ │ ├── search-lunr.js │ │ ├── search.js │ │ └── search_index.js │ └── svg-pan-zoom.controls.js ├── miscellaneous.html ├── modules.html ├── overview.html └── styles │ ├── bootstrap-card.css │ ├── bootstrap.min.css │ ├── compodoc.css │ ├── font-awesome.min.css │ ├── laravel.css │ ├── monokai-sublime.css │ ├── original.css │ ├── postmark.css │ ├── readthedocs.css │ ├── reset.css │ ├── stripe.css │ ├── style.css │ └── vagrant.css ├── examples ├── .gitignore ├── .prettierrc ├── README.md ├── package.json ├── src │ ├── all-exception.filter.ts │ ├── bin │ │ └── runner.ts │ └── heroes │ │ ├── aggregates │ │ └── hero.aggregate.ts │ │ ├── commands │ │ ├── handlers │ │ │ ├── drop-ancient-item.handler.ts │ │ │ ├── index.ts │ │ │ └── kill-dragon.handler.ts │ │ └── impl │ │ │ ├── drop-ancient-item.command.ts │ │ │ └── kill-dragon.command.ts │ │ ├── dto │ │ └── hero-damaged-enemy.dto.ts │ │ ├── event-store-heroes.module.ts │ │ ├── events │ │ ├── handlers │ │ │ ├── hero-found-item.handler.ts │ │ │ ├── hero-killed-dragon.handler.ts │ │ │ └── index.ts │ │ └── impl │ │ │ ├── hero-damaged-enemy.event.ts │ │ │ ├── hero-drop-item.event.ts │ │ │ ├── hero-found-item.event.ts │ │ │ ├── hero-killed-dragon.event.ts │ │ │ └── index.ts │ │ ├── health.controller.ts │ │ ├── heroes.controller.ts │ │ ├── heroes.module.ts │ │ ├── interfaces │ │ └── kill-dragon-dto.interface.ts │ │ ├── projections │ │ └── hero-dragon.js │ │ ├── queries │ │ ├── handlers │ │ │ ├── get-heroes.handler.ts │ │ │ └── index.ts │ │ └── impl │ │ │ ├── get-heroes.query.ts │ │ │ └── index.ts │ │ ├── repository │ │ ├── fixtures │ │ │ └── user.ts │ │ └── hero.repository.ts │ │ ├── sagas │ │ └── heroes.sagas.ts │ │ └── write.controller.ts ├── tsconfig.build.json ├── tsconfig.json ├── webpack-hmr.config.js └── yarn.lock ├── index.d.ts ├── index.js ├── index.ts ├── jest.setup.ts ├── package.json ├── src ├── cloudevents │ ├── index.ts │ └── write-events-prepublish.service.ts ├── constants.ts ├── cqrs-event-store.module.ts ├── cqrs │ ├── aggregate-root.ts │ ├── default-event-mapper.ts │ ├── event-bus-prepublish.service.ts │ ├── index.ts │ ├── read-event-bus.ts │ └── write-event-bus.ts ├── decorators │ ├── event-version.decorator.ts │ └── index.ts ├── dto │ ├── event-metadata.dto.ts │ ├── index.ts │ └── write-event.dto.ts ├── event-store │ ├── config │ │ ├── connector.ts │ │ ├── event-store-connection-config.ts │ │ ├── event-store-service-config.interface.ts │ │ └── index.ts │ ├── event-store-aggregate-root.ts │ ├── event-store.module.ts │ ├── events │ │ ├── event-store-acknowledgeable.event.ts │ │ ├── event-store.event.ts │ │ └── index.ts │ ├── health │ │ ├── event-store-health.status.ts │ │ ├── event-store.health-indicator.spec.ts │ │ ├── event-store.health-indicator.ts │ │ └── index.ts │ ├── index.ts │ ├── publisher │ │ ├── event-store.publisher.spec.ts │ │ ├── event-store.publisher.ts │ │ └── index.ts │ ├── reliability │ │ ├── implementations │ │ │ └── in-memory │ │ │ │ ├── in-memory-events-and-metadatas-stacker.spec.ts │ │ │ │ └── in-memory-events-and-metadatas-stacker.ts │ │ ├── index.ts │ │ └── interface │ │ │ ├── event-batch.ts │ │ │ ├── events-and-metadatas-stacker.ts │ │ │ └── metadatas-context-datas.ts │ ├── services │ │ ├── errors.constant.ts │ │ ├── event-store.constants.ts │ │ ├── event-store.service.interface.ts │ │ ├── event-store.service.spec.ts │ │ ├── event-store.service.ts │ │ ├── event.handler.helper.spec.ts │ │ ├── event.handler.helper.ts │ │ └── index.ts │ └── subscriptions │ │ ├── index.ts │ │ └── persistent-subscription-config.interface.ts ├── exceptions │ ├── index.ts │ ├── invalid-event.exception.ts │ └── invalid-publisher.exception.ts ├── index.ts ├── interfaces │ ├── config │ │ ├── event-bus-config.type.ts │ │ ├── event-bus-prepublish-config.interface.ts │ │ ├── event-bus-prepublish-prepare-callback.type.ts │ │ ├── event-bus-prepublish-prepare-provider.interface.ts │ │ ├── event-bus-prepublish-validate-provider.interface.ts │ │ ├── index.ts │ │ ├── read-event-bus-config.type.ts │ │ └── write-event-bus-config.interface.ts │ ├── events │ │ ├── acknowledgeable-event.interface.ts │ │ ├── base-event.interface.ts │ │ ├── event-options.type.ts │ │ ├── index.ts │ │ ├── persistent-subscription-nak-event-action.enum.ts │ │ ├── publication-context.interface.ts │ │ ├── read-event-options.type.ts │ │ ├── read-event.interface.ts │ │ └── write-event.interface.ts │ ├── index.ts │ ├── projection.type.ts │ └── write-event-bus.interface.ts └── tools │ ├── create-event-default-metadata.ts │ └── index.ts ├── tsconfig.build.json ├── tsconfig.json ├── tsconfig.spec.json ├── upgrade.md └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | *.js 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"], 7 | "parser": "@typescript-eslint/parser", 8 | "parserOptions": { 9 | "ecmaVersion": 12, 10 | "sourceType": "module" 11 | }, 12 | "plugins": ["@typescript-eslint"], 13 | "root": true, 14 | "rules": { 15 | "no-restricted-imports": [ 16 | "error", 17 | { 18 | "patterns": ["**/src/*"], 19 | "paths": ["node-eventstore-client", "geteventstore-promise"] 20 | } 21 | ], 22 | "no-unused-expressions": "error", 23 | "no-prototype-builtins": "warn", 24 | "@typescript-eslint/explicit-module-boundary-types": "warn", 25 | "@typescript-eslint/no-empty-interface": "off", 26 | "@typescript-eslint/ban-types": "off", 27 | "@typescript-eslint/ban-ts-comment": "off", 28 | "@typescript-eslint/no-unused-vars": "warn", 29 | "@typescript-eslint/no-empty-function": "off", 30 | "no-mixed-spaces-and-tabs": "off" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '19 23 * * 2' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'javascript' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v2 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v1 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v1 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v1 68 | -------------------------------------------------------------------------------- /.github/workflows/publish_workflow.yml: -------------------------------------------------------------------------------- 1 | name: publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | publish: 10 | name: build 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Get tag 14 | id: get_tag 15 | run: echo ::set-output name=TAG::$(echo $GITHUB_REF | cut -d / -f 3) 16 | - name: Checkout project 17 | uses: actions/checkout@v1 18 | 19 | - name: Setup node version 20 | uses: actions/setup-node@v1 21 | with: 22 | node-version: 14 23 | registry-url: https://registry.npmjs.org/ 24 | 25 | - name: Install 26 | run: yarn 27 | 28 | - name: Build 29 | run: yarn run build 30 | 31 | - name: Update npm version 32 | run: npm version $VERSION --no-git-tag-version --allow-same-version 33 | env: 34 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 35 | VERSION: ${{ steps.get_tag.outputs.TAG }} 36 | 37 | - name: Publish 38 | run: npm publish 39 | env: 40 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # OS 14 | .DS_Store 15 | 16 | # Tests 17 | /coverage 18 | /.nyc_output 19 | 20 | # IDEs and editors 21 | .idea 22 | .project 23 | .classpath 24 | .c9/ 25 | *.launch 26 | .settings/ 27 | *.sublime-workspace 28 | 29 | # IDE - VSCode 30 | .vscode/* 31 | !.vscode/settings.json 32 | !.vscode/tasks.json 33 | !.vscode/launch.json 34 | !.vscode/extensions.json 35 | 36 | # Certificates 37 | *.key 38 | 39 | #Config 40 | .env 41 | 42 | package-lock.json 43 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn lint-staged && yarn pretty-quick --staged && yarn jest --forceExit 5 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v16.9.0 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } 5 | -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "@semantic-release/commit-analyzer", 4 | "@semantic-release/release-notes-generator", 5 | ["@semantic-release/npm", { 6 | "npmPublish": false, 7 | "tarballDir": "dist", 8 | }], 9 | ["@semantic-release/github", { 10 | "assets": "dist/*.tgz" 11 | }] 12 | ] 13 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 daypaio GmbH 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nestjs-eventstore 2 | 3 | Event store driven NestJS and CQRS 4 | 5 | ### How to use it 6 | 7 | You'll find how to use it [in the basic nest CQRS example documentation](./examples/README.md) 8 | 9 | Just as a reminder, you'll find here the commands for running the event store docker : 10 | 11 | ``` 12 | docker run --name esdb-node \ 13 | -it -p 20113:2113 -p 10113:1113 \ 14 | eventstore/eventstore:latest \ 15 | --insecure \ 16 | --run-projections=All \ 17 | --enable-atom-pub-over-http 18 | ``` 19 | -------------------------------------------------------------------------------- /documentation/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrestaShopCorp/nestjs-geteventstore/3a14375624d53521fa3aad698c6729f1234dbf59/documentation/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /documentation/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrestaShopCorp/nestjs-geteventstore/3a14375624d53521fa3aad698c6729f1234dbf59/documentation/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /documentation/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrestaShopCorp/nestjs-geteventstore/3a14375624d53521fa3aad698c6729f1234dbf59/documentation/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /documentation/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrestaShopCorp/nestjs-geteventstore/3a14375624d53521fa3aad698c6729f1234dbf59/documentation/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /documentation/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrestaShopCorp/nestjs-geteventstore/3a14375624d53521fa3aad698c6729f1234dbf59/documentation/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /documentation/fonts/roboto-v15-latin-300.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrestaShopCorp/nestjs-geteventstore/3a14375624d53521fa3aad698c6729f1234dbf59/documentation/fonts/roboto-v15-latin-300.eot -------------------------------------------------------------------------------- /documentation/fonts/roboto-v15-latin-300.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrestaShopCorp/nestjs-geteventstore/3a14375624d53521fa3aad698c6729f1234dbf59/documentation/fonts/roboto-v15-latin-300.ttf -------------------------------------------------------------------------------- /documentation/fonts/roboto-v15-latin-300.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrestaShopCorp/nestjs-geteventstore/3a14375624d53521fa3aad698c6729f1234dbf59/documentation/fonts/roboto-v15-latin-300.woff -------------------------------------------------------------------------------- /documentation/fonts/roboto-v15-latin-300.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrestaShopCorp/nestjs-geteventstore/3a14375624d53521fa3aad698c6729f1234dbf59/documentation/fonts/roboto-v15-latin-300.woff2 -------------------------------------------------------------------------------- /documentation/fonts/roboto-v15-latin-700.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrestaShopCorp/nestjs-geteventstore/3a14375624d53521fa3aad698c6729f1234dbf59/documentation/fonts/roboto-v15-latin-700.eot -------------------------------------------------------------------------------- /documentation/fonts/roboto-v15-latin-700.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrestaShopCorp/nestjs-geteventstore/3a14375624d53521fa3aad698c6729f1234dbf59/documentation/fonts/roboto-v15-latin-700.ttf -------------------------------------------------------------------------------- /documentation/fonts/roboto-v15-latin-700.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrestaShopCorp/nestjs-geteventstore/3a14375624d53521fa3aad698c6729f1234dbf59/documentation/fonts/roboto-v15-latin-700.woff -------------------------------------------------------------------------------- /documentation/fonts/roboto-v15-latin-700.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrestaShopCorp/nestjs-geteventstore/3a14375624d53521fa3aad698c6729f1234dbf59/documentation/fonts/roboto-v15-latin-700.woff2 -------------------------------------------------------------------------------- /documentation/fonts/roboto-v15-latin-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrestaShopCorp/nestjs-geteventstore/3a14375624d53521fa3aad698c6729f1234dbf59/documentation/fonts/roboto-v15-latin-regular.eot -------------------------------------------------------------------------------- /documentation/fonts/roboto-v15-latin-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrestaShopCorp/nestjs-geteventstore/3a14375624d53521fa3aad698c6729f1234dbf59/documentation/fonts/roboto-v15-latin-regular.ttf -------------------------------------------------------------------------------- /documentation/fonts/roboto-v15-latin-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrestaShopCorp/nestjs-geteventstore/3a14375624d53521fa3aad698c6729f1234dbf59/documentation/fonts/roboto-v15-latin-regular.woff -------------------------------------------------------------------------------- /documentation/fonts/roboto-v15-latin-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrestaShopCorp/nestjs-geteventstore/3a14375624d53521fa3aad698c6729f1234dbf59/documentation/fonts/roboto-v15-latin-regular.woff2 -------------------------------------------------------------------------------- /documentation/graph/dependencies.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | dependencies 11 | 12 | Legend 13 | 14 |  Declarations 15 | 16 |  Module 17 | 18 |  Bootstrap 19 | 20 |  Providers 21 | 22 |  Exports 23 | 24 | 25 | -------------------------------------------------------------------------------- /documentation/images/compodoc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrestaShopCorp/nestjs-geteventstore/3a14375624d53521fa3aad698c6729f1234dbf59/documentation/images/compodoc.png -------------------------------------------------------------------------------- /documentation/images/compodoc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 21 | 23 | 45 | 47 | 48 | 50 | image/svg+xml 51 | 53 | 54 | 55 | 56 | 57 | 62 | 65 | 70 | 75 | 80 | 86 | 91 | 96 | 101 | 106 | 111 | 112 | 115 | 123 | 131 | 132 | 135 | 143 | 148 | 149 | compo 172 | doc 184 | 185 | 186 | -------------------------------------------------------------------------------- /documentation/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrestaShopCorp/nestjs-geteventstore/3a14375624d53521fa3aad698c6729f1234dbf59/documentation/images/favicon.ico -------------------------------------------------------------------------------- /documentation/js/compodoc.js: -------------------------------------------------------------------------------- 1 | var compodoc = { 2 | EVENTS: { 3 | READY: 'compodoc.ready', 4 | SEARCH_READY: 'compodoc.search.ready' 5 | } 6 | }; 7 | 8 | Object.assign( compodoc, EventDispatcher.prototype ); 9 | 10 | document.addEventListener('DOMContentLoaded', function() { 11 | compodoc.dispatchEvent({ 12 | type: compodoc.EVENTS.READY 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /documentation/js/libs/EventDispatcher.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author mrdoob / http://mrdoob.com/ 3 | */ 4 | 5 | var EventDispatcher = function () {}; 6 | 7 | Object.assign( EventDispatcher.prototype, { 8 | 9 | addEventListener: function ( type, listener ) { 10 | 11 | if ( this._listeners === undefined ) this._listeners = {}; 12 | 13 | var listeners = this._listeners; 14 | 15 | if ( listeners[ type ] === undefined ) { 16 | 17 | listeners[ type ] = []; 18 | 19 | } 20 | 21 | if ( listeners[ type ].indexOf( listener ) === - 1 ) { 22 | 23 | listeners[ type ].push( listener ); 24 | 25 | } 26 | 27 | }, 28 | 29 | hasEventListener: function ( type, listener ) { 30 | 31 | if ( this._listeners === undefined ) return false; 32 | 33 | var listeners = this._listeners; 34 | 35 | if ( listeners[ type ] !== undefined && listeners[ type ].indexOf( listener ) !== - 1 ) { 36 | 37 | return true; 38 | 39 | } 40 | 41 | return false; 42 | 43 | }, 44 | 45 | removeEventListener: function ( type, listener ) { 46 | 47 | if ( this._listeners === undefined ) return; 48 | 49 | var listeners = this._listeners; 50 | var listenerArray = listeners[ type ]; 51 | 52 | if ( listenerArray !== undefined ) { 53 | 54 | var index = listenerArray.indexOf( listener ); 55 | 56 | if ( index !== - 1 ) { 57 | 58 | listenerArray.splice( index, 1 ); 59 | 60 | } 61 | 62 | } 63 | 64 | }, 65 | 66 | dispatchEvent: function ( event ) { 67 | 68 | if ( this._listeners === undefined ) return; 69 | 70 | var listeners = this._listeners; 71 | var listenerArray = listeners[ event.type ]; 72 | 73 | if ( listenerArray !== undefined ) { 74 | 75 | event.target = this; 76 | 77 | var array = [], i = 0; 78 | var length = listenerArray.length; 79 | 80 | for ( i = 0; i < length; i ++ ) { 81 | 82 | array[ i ] = listenerArray[ i ]; 83 | 84 | } 85 | 86 | for ( i = 0; i < length; i ++ ) { 87 | 88 | array[ i ].call( this, event ); 89 | 90 | } 91 | 92 | } 93 | 94 | } 95 | 96 | } ); 97 | -------------------------------------------------------------------------------- /documentation/js/libs/bootstrap-native.js: -------------------------------------------------------------------------------- 1 | // Native Javascript for Bootstrap 3 v1.1.0 | © dnp_theme | MIT-License 2 | (function(root, factory) { 3 | if (typeof define === 'function' && define.amd) { 4 | // AMD support: 5 | define([], factory); 6 | } else if (typeof module === 'object' && module.exports) { 7 | // CommonJS-like: 8 | module.exports = factory(); 9 | } else { 10 | // Browser globals (root is window) 11 | var bsn = factory(); 12 | root.Affix = bsn.Affix; 13 | root.Alert = bsn.Alert; 14 | root.Button = bsn.Button; 15 | root.Carousel = bsn.Carousel; 16 | root.Collapse = bsn.Collapse; 17 | root.Dropdown = bsn.Dropdown; 18 | root.Modal = bsn.Modal; 19 | root.Popover = bsn.Popover; 20 | root.ScrollSpy = bsn.ScrollSpy; 21 | root.Tab = bsn.Tab; 22 | root.Tooltip = bsn.Tooltip; 23 | } 24 | }(this, function() { 25 | // Native Javascript for Bootstrap 3 | Internal Utility Functions 26 | // by dnp_theme 27 | var addClass = function(el, c) { // where modern browsers fail, use classList 28 | if (el.classList) { 29 | el.classList.add(c); 30 | } else { 31 | el.className += ' ' + c; 32 | el.offsetWidth; 33 | } 34 | }, 35 | removeClass = function(el, c) { 36 | if (el.classList) { 37 | el.classList.remove(c); 38 | } else { 39 | el.className = el.className.replace(c, '').replace(/^\s+|\s+$/g, ''); 40 | } 41 | }, 42 | isIE = (new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})").exec(navigator.userAgent) != null) ? parseFloat(RegExp.$1) : false, 43 | getClosest = function(el, s) { //el is the element and s the selector of the closest item to find 44 | // source http://gomakethings.com/climbing-up-and-down-the-dom-tree-with-vanilla-javascript/ 45 | var f = s.charAt(0); 46 | for (; el && el !== document; el = el.parentNode) { // Get closest match 47 | if (f === '.') { // If selector is a class 48 | if (document.querySelector(s) !== undefined) { 49 | return el; 50 | } 51 | } 52 | if (f === '#') { // If selector is an ID 53 | if (el.id === s.substr(1)) { 54 | return el; 55 | } 56 | } 57 | } 58 | return false; 59 | }, 60 | 61 | 62 | // tooltip / popover stuff 63 | isElementInViewport = function(t) { // check if this.tooltip is in viewport 64 | var r = t.getBoundingClientRect(); 65 | return ( 66 | r.top >= 0 && r.left >= 0 && r.bottom <= (window.innerHeight || document.documentElement.clientHeight) && r.right <= (window.innerWidth || document.documentElement.clientWidth)) 67 | }, 68 | getScroll = function() { // also Affix and scrollSpy uses it 69 | return { 70 | y: window.pageYOffset || document.documentElement.scrollTop, 71 | x: window.pageXOffset || document.documentElement.scrollLeft 72 | } 73 | }, 74 | mouseHover = ('onmouseleave' in document) ? ['mouseenter', 'mouseleave'] : ['mouseover', 'mouseout'], 75 | tipPositions = /\b(top|bottom|left|top)+/; 76 | 77 | 78 | // Native Javascript for Bootstrap 3 | Collapse 79 | // by dnp_theme 80 | // COLLAPSE DEFINITION 81 | // =================== 82 | var Collapse = function(element, options) { 83 | options = options || {}; 84 | 85 | this.btn = typeof element === 'object' ? element : document.querySelector(element); 86 | this.accordion = null; 87 | this.collapse = null; 88 | this.duration = 300; // default collapse transition duration 89 | this.options = {}; 90 | this.options.duration = (isIE && isIE < 10) ? 0 : (options.duration || this.duration); 91 | var self = this; 92 | var getOuterHeight = function(el) { 93 | var s = el && (el.currentStyle || window.getComputedStyle(el)), 94 | // the getComputedStyle polyfill would do this for us, but we want to make sure it does 95 | btp = /px/.test(s.borderTopWidth) ? Math.round(s.borderTopWidth.replace('px', '')) : 0, 96 | mtp = /px/.test(s.marginTop) ? Math.round(s.marginTop.replace('px', '')) : 0, 97 | mbp = /px/.test(s.marginBottom) ? Math.round(s.marginBottom.replace('px', '')) : 0, 98 | mte = /em/.test(s.marginTop) ? Math.round(s.marginTop.replace('em', '') * parseInt(s.fontSize)) : 0, 99 | mbe = /em/.test(s.marginBottom) ? Math.round(s.marginBottom.replace('em', '') * parseInt(s.fontSize)) : 0; 100 | return el.clientHeight + parseInt(btp) + parseInt(mtp) + parseInt(mbp) + parseInt(mte) + parseInt(mbe); //we need an accurate margin value 101 | }; 102 | 103 | this.toggle = function(e) { 104 | e.preventDefault(); 105 | 106 | if (!/\bin/.test(self.collapse.className)) { 107 | self.open(); 108 | } else { 109 | self.close(); 110 | } 111 | }; 112 | this.close = function() { 113 | this._close(this.collapse); 114 | addClass(this.btn, 'collapsed'); 115 | }; 116 | this.open = function() { 117 | this._open(this.collapse); 118 | removeClass(this.btn, 'collapsed'); 119 | 120 | if (this.accordion !== null) { 121 | var active = this.accordion.querySelectorAll('.collapse.in'), 122 | al = active.length, 123 | i = 0; 124 | for (i; i < al; i++) { 125 | if (active[i] !== this.collapse) this._close(active[i]); 126 | } 127 | } 128 | }; 129 | this._open = function(c) { 130 | this.removeEvent(); 131 | addClass(c, 'in'); 132 | c.setAttribute('aria-expanded', 'true'); 133 | addClass(c, 'collapsing'); 134 | setTimeout(function() { 135 | c.style.height = self.getMaxHeight(c) + 'px' 136 | c.style.overflowY = 'hidden'; 137 | }, 0); 138 | setTimeout(function() { 139 | c.style.height = ''; 140 | c.style.overflowY = ''; 141 | removeClass(c, 'collapsing'); 142 | self.addEvent(); 143 | }, this.options.duration); 144 | }; 145 | this._close = function(c) { 146 | this.removeEvent(); 147 | c.setAttribute('aria-expanded', 'false'); 148 | c.style.height = this.getMaxHeight(c) + 'px' 149 | setTimeout(function() { 150 | c.style.height = '0px'; 151 | c.style.overflowY = 'hidden'; 152 | addClass(c, 'collapsing'); 153 | }, 0); 154 | 155 | setTimeout(function() { 156 | removeClass(c, 'collapsing'); 157 | removeClass(c, 'in'); 158 | c.style.overflowY = ''; 159 | c.style.height = ''; 160 | self.addEvent(); 161 | }, this.options.duration); 162 | }; 163 | this.getMaxHeight = function(l) { // get collapse trueHeight and border 164 | var h = 0; 165 | for (var k = 0, ll = l.children.length; k < ll; k++) { 166 | h += getOuterHeight(l.children[k]); 167 | } 168 | return h; 169 | }; 170 | this.removeEvent = function() { 171 | this.btn.removeEventListener('click', this.toggle, false); 172 | }; 173 | this.addEvent = function() { 174 | this.btn.addEventListener('click', this.toggle, false); 175 | }; 176 | this.getTarget = function() { 177 | var t = this.btn, 178 | h = t.href && t.getAttribute('href').replace('#', ''), 179 | d = t.getAttribute('data-target') && (t.getAttribute('data-target')), 180 | id = h || (d && /#/.test(d)) && d.replace('#', ''), 181 | cl = (d && d.charAt(0) === '.') && d, 182 | //the navbar collapse trigger targets a class 183 | c = id && document.getElementById(id) || cl && document.querySelector(cl); 184 | return c; 185 | }; 186 | 187 | // init 188 | this.addEvent(); 189 | this.collapse = this.getTarget(); 190 | this.accordion = this.btn.getAttribute('data-parent') && getClosest(this.btn, this.btn.getAttribute('data-parent')); 191 | }; 192 | 193 | // COLLAPSE DATA API 194 | // ================= 195 | var Collapses = document.querySelectorAll('[data-toggle="collapse"]'); 196 | for (var o = 0, cll = Collapses.length; o < cll; o++) { 197 | var collapse = Collapses[o], 198 | options = {}; 199 | options.duration = collapse.getAttribute('data-duration'); 200 | new Collapse(collapse, options); 201 | } 202 | 203 | // Native Javascript for Bootstrap 3 | Tab 204 | // by dnp_theme 205 | 206 | // TAB DEFINITION 207 | // =================== 208 | var Tab = function( element,options ) { 209 | options = options || {}; 210 | this.tab = typeof element === 'object' ? element : document.querySelector(element); 211 | this.tabs = this.tab.parentNode.parentNode; 212 | this.dropdown = this.tabs.querySelector('.dropdown'); 213 | if ( /\bdropdown-menu/.test(this.tabs.className) ) { 214 | this.dropdown = this.tabs.parentNode; 215 | this.tabs = this.tabs.parentNode.parentNode; 216 | } 217 | this.options = options; 218 | 219 | // default tab transition duration 220 | this.duration = 150; 221 | this.options.duration = (isIE && isIE < 10) ? 0 : (options.duration || this.duration); 222 | 223 | var self = this; 224 | 225 | this.handle = function(e) { 226 | e = e || window.e; e.preventDefault(); 227 | var next = e.target; //the tab we clicked is now the next tab 228 | var nextContent = document.getElementById(next.getAttribute('href').replace('#','')); //this is the actual object, the next tab content to activate 229 | 230 | // get current active tab and content 231 | var activeTab = self.getActiveTab(); 232 | var activeContent = self.getActiveContent(); 233 | 234 | if ( !/\bactive/.test(next.parentNode.className) ) { 235 | // toggle "active" class name 236 | removeClass(activeTab,'active'); 237 | addClass(next.parentNode,'active'); 238 | 239 | // handle dropdown menu "active" class name 240 | if ( self.dropdown ) { 241 | if ( !(/\bdropdown-menu/.test(self.tab.parentNode.parentNode.className)) ) { 242 | if (/\bactive/.test(self.dropdown.className)) removeClass(self.dropdown,'active'); 243 | } else { 244 | if (!/\bactive/.test(self.dropdown.className)) addClass(self.dropdown,'active'); 245 | } 246 | } 247 | 248 | //1. hide current active content first 249 | removeClass(activeContent,'in'); 250 | 251 | setTimeout(function() { 252 | //2. toggle current active content from view 253 | removeClass(activeContent,'active'); 254 | addClass(nextContent,'active'); 255 | }, self.options.duration); 256 | setTimeout(function() { 257 | //3. show next active content 258 | addClass(nextContent,'in'); 259 | }, self.options.duration*2); 260 | } 261 | }; 262 | this.getActiveTab = function() { 263 | var activeTabs = this.tabs.querySelectorAll('.active'); 264 | if ( activeTabs.length === 1 && !/\bdropdown/.test(activeTabs[0].className) ) { 265 | return activeTabs[0] 266 | } else if ( activeTabs.length > 1 ) { 267 | return activeTabs[activeTabs.length-1] 268 | } 269 | }; 270 | this.getActiveContent = function() { 271 | var active = this.getActiveTab().getElementsByTagName('A')[0].getAttribute('href').replace('#',''); 272 | return active && document.getElementById(active) 273 | }; 274 | 275 | // init 276 | this.tab.addEventListener('click', this.handle, false); 277 | }; 278 | 279 | // TAB DATA API 280 | // ================= 281 | var Tabs = document.querySelectorAll("[data-toggle='tab'], [data-toggle='pill']"); 282 | for ( var tb = 0, tbl = Tabs.length; tb1){for(var r="",c=0;n>c;c++)r+=c+1+"\n";var l=document.createElement("code");l.className="hljs hljs-line-numbers",l.style["float"]="left",l.textContent=r,t.insertBefore(l,e)}}}function o(e){if(0===e.length)return 0;var t=/\r\n|\r|\n/g,n=e.match(t);return n=n?n.length:0,e[e.length-1].match(t)||(n+=1),n}"undefined"==typeof e.hljs?console.error("highlight.js not detected!"):(e.hljs.initLineNumbersOnLoad=t,e.hljs.lineNumbersBlock=r)}(window); 2 | -------------------------------------------------------------------------------- /documentation/js/libs/promise.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 (c) Pierre Duquesne 3 | * Licensed under the New BSD License. 4 | * https://github.com/stackp/promisejs 5 | */ 6 | (function(a){function b(){this._callbacks=[];}b.prototype.then=function(a,c){var d;if(this._isdone)d=a.apply(c,this.result);else{d=new b();this._callbacks.push(function(){var b=a.apply(c,arguments);if(b&&typeof b.then==='function')b.then(d.done,d);});}return d;};b.prototype.done=function(){this.result=arguments;this._isdone=true;for(var a=0;a=300)&&j.status!==304);h.done(a,j.responseText,j);}};j.send(k);return h;}function h(a){return function(b,c,d){return g(a,b,c,d);};}var i={Promise:b,join:c,chain:d,ajax:g,get:h('GET'),post:h('POST'),put:h('PUT'),del:h('DELETE'),ENOXHR:1,ETIMEOUT:2,ajaxTimeout:0};if(typeof define==='function'&&define.amd)define(function(){return i;});else a.promise=i;})(this); -------------------------------------------------------------------------------- /documentation/js/menu.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', function() { 2 | var menuCollapsed = false, 3 | mobileMenu = document.getElementById('mobile-menu'); 4 | document.getElementById('btn-menu').addEventListener('click', function() { 5 | if (menuCollapsed) { 6 | mobileMenu.style.display = 'none'; 7 | } else { 8 | mobileMenu.style.display = 'block'; 9 | document.getElementsByTagName('body')[0].style['overflow-y'] = 'hidden'; 10 | } 11 | menuCollapsed = !menuCollapsed; 12 | }); 13 | 14 | // collapse menu 15 | var classnameMenuToggler = document.getElementsByClassName('menu-toggler'), 16 | faAngleUpClass = 'fa-angle-up', 17 | faAngleDownClass = 'fa-angle-down', 18 | toggleItemMenu = function(e) { 19 | e.preventDefault(); 20 | var element = $(e.target); 21 | if (element.hasClass(faAngleUpClass)) { 22 | element.addClass(faAngleDownClass); 23 | element.removeClass(faAngleUpClass); 24 | } else { 25 | element.addClass(faAngleUpClass); 26 | element.removeClass(faAngleDownClass); 27 | } 28 | }; 29 | 30 | for (var i = 0; i < classnameMenuToggler.length; i++) { 31 | classnameMenuToggler[i].addEventListener('click', toggleItemMenu, false); 32 | } 33 | 34 | // Scroll to active link 35 | var menus = document.querySelectorAll('.menu'), 36 | i = 0, 37 | len = menus.length, 38 | activeMenu, 39 | activeMenuClass, 40 | activeLink; 41 | 42 | for (i; i element for each result 45 | res.results.forEach(function(res) { 46 | var $li = $('
  • ', { 47 | 'class': 'search-results-item' 48 | }); 49 | 50 | var $title = $('

    '); 51 | 52 | var $link = $('', { 53 | 'href': res.url, 54 | 'text': res.title 55 | }); 56 | 57 | var content = res.body.trim(); 58 | if (content.length > MAX_DESCRIPTION_SIZE) { 59 | content = content.slice(0, MAX_DESCRIPTION_SIZE).trim()+'...'; 60 | } 61 | var $content = $('

    ').html(content); 62 | 63 | $link.appendTo($title); 64 | $title.appendTo($li); 65 | $content.appendTo($li); 66 | $li.appendTo($searchList); 67 | }); 68 | } 69 | 70 | function launchSearch(q) { 71 | $body.addClass('with-search'); 72 | 73 | if ($xsMenu.css('display') === 'block') { 74 | $mainContainer.css('height', 'calc(100% - 100px)'); 75 | $mainContainer.css('margin-top', '100px'); 76 | } 77 | 78 | throttle(compodoc.search.query(q, 0, MAX_RESULTS) 79 | .then(function(results) { 80 | displayResults(results); 81 | }), 1000); 82 | } 83 | 84 | function closeSearch() { 85 | $body.removeClass('with-search'); 86 | if ($xsMenu.css('display') === 'block') { 87 | $mainContainer.css('height', 'calc(100% - 50px)'); 88 | $mainContainer.css('margin-top', '50px'); 89 | } 90 | } 91 | 92 | function bindMenuButton() { 93 | document.getElementById('btn-menu').addEventListener('click', function() { 94 | if ($xsMenu.css('display') === 'none') { 95 | $body.removeClass('with-search'); 96 | $mainContainer.css('height', 'calc(100% - 50px)'); 97 | $mainContainer.css('margin-top', '50px'); 98 | } 99 | $.each($searchInputs, function(index, item){ 100 | var item = $(item); 101 | item.val(''); 102 | }); 103 | }); 104 | } 105 | 106 | function bindSearch() { 107 | // Bind DOM 108 | $searchInputs = $('#book-search-input input'); 109 | 110 | $searchResults = $('.search-results'); 111 | $searchList = $searchResults.find('.search-results-list'); 112 | $searchTitle = $searchResults.find('.search-results-title'); 113 | $searchResultsCount = $searchTitle.find('.search-results-count'); 114 | $searchQuery = $searchTitle.find('.search-query'); 115 | $mainContainer = $('.container-fluid'); 116 | $xsMenu = $('.xs-menu'); 117 | 118 | // Launch query based on input content 119 | function handleUpdate(item) { 120 | var q = item.val(); 121 | 122 | if (q.length == 0) { 123 | closeSearch(); 124 | } else { 125 | launchSearch(q); 126 | } 127 | } 128 | 129 | // Detect true content change in search input 130 | var propertyChangeUnbound = false; 131 | 132 | $.each($searchInputs, function(index, item){ 133 | var item = $(item); 134 | // HTML5 (IE9 & others) 135 | item.on('input', function(e) { 136 | // Unbind propertychange event for IE9+ 137 | if (!propertyChangeUnbound) { 138 | $(this).unbind('propertychange'); 139 | propertyChangeUnbound = true; 140 | } 141 | 142 | handleUpdate($(this)); 143 | }); 144 | // Workaround for IE < 9 145 | item.on('propertychange', function(e) { 146 | if (e.originalEvent.propertyName == 'value') { 147 | handleUpdate($(this)); 148 | } 149 | }); 150 | // Push to history on blur 151 | item.on('blur', function(e) { 152 | // Update history state 153 | if (usePushState) { 154 | var uri = updateQueryString('q', $(this).val()); 155 | history.pushState({ path: uri }, null, uri); 156 | } 157 | }); 158 | }); 159 | } 160 | 161 | function launchSearchFromQueryString() { 162 | var q = getParameterByName('q'); 163 | if (q && q.length > 0) { 164 | // Update search inputs 165 | $.each($searchInputs, function(index, item){ 166 | var item = $(item); 167 | item.val(q) 168 | }); 169 | // Launch search 170 | launchSearch(q); 171 | } 172 | } 173 | 174 | compodoc.addEventListener(compodoc.EVENTS.SEARCH_READY, function(event) { 175 | bindSearch(); 176 | 177 | bindMenuButton(); 178 | 179 | launchSearchFromQueryString(); 180 | }); 181 | 182 | function getParameterByName(name) { 183 | var url = window.location.href; 184 | name = name.replace(/[\[\]]/g, '\\$&'); 185 | var regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)', 'i'), 186 | results = regex.exec(url); 187 | if (!results) return null; 188 | if (!results[2]) return ''; 189 | return decodeURIComponent(results[2].replace(/\+/g, ' ')); 190 | } 191 | 192 | function updateQueryString(key, value) { 193 | value = encodeURIComponent(value); 194 | 195 | var url = window.location.href; 196 | var re = new RegExp('([?&])' + key + '=.*?(&|#|$)(.*)', 'gi'), 197 | hash; 198 | 199 | if (re.test(url)) { 200 | if (typeof value !== 'undefined' && value !== null) 201 | return url.replace(re, '$1' + key + '=' + value + '$2$3'); 202 | else { 203 | hash = url.split('#'); 204 | url = hash[0].replace(re, '$1$3').replace(/(&|\?)$/, ''); 205 | if (typeof hash[1] !== 'undefined' && hash[1] !== null) 206 | url += '#' + hash[1]; 207 | return url; 208 | } 209 | } 210 | else { 211 | if (typeof value !== 'undefined' && value !== null) { 212 | var separator = url.indexOf('?') !== -1 ? '&' : '?'; 213 | hash = url.split('#'); 214 | url = hash[0] + separator + key + '=' + value; 215 | if (typeof hash[1] !== 'undefined' && hash[1] !== null) 216 | url += '#' + hash[1]; 217 | return url; 218 | } 219 | else 220 | return url; 221 | } 222 | } 223 | })(compodoc); 224 | -------------------------------------------------------------------------------- /documentation/js/svg-pan-zoom.controls.js: -------------------------------------------------------------------------------- 1 | document.getElementById('demo-svg').addEventListener('load', function() { 2 | panZoom = svgPanZoom('#demo-svg', { 3 | zoomEnabled: true, 4 | minZoom: 1, 5 | maxZoom: 5 6 | }); 7 | 8 | document.getElementById('zoom-in').addEventListener('click', function(ev) { 9 | ev.preventDefault() 10 | panZoom.zoomIn() 11 | }); 12 | 13 | document.getElementById('zoom-out').addEventListener('click', function(ev) { 14 | ev.preventDefault() 15 | panZoom.zoomOut() 16 | }); 17 | 18 | document.getElementById('reset').addEventListener('click', function(ev) { 19 | ev.preventDefault() 20 | panZoom.resetZoom(); 21 | panZoom.resetPan(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /documentation/styles/bootstrap-card.css: -------------------------------------------------------------------------------- 1 | .card { 2 | position: relative; 3 | display: block; 4 | margin-bottom: 20px; 5 | background-color: #fff; 6 | border: 1px solid #ddd; 7 | border-radius: 4px; 8 | } 9 | 10 | .card-block { 11 | padding: 15px; 12 | } 13 | .card-block:before, .card-block:after { 14 | content: " "; 15 | display: table; 16 | } 17 | .card-block:after { 18 | clear: both; 19 | } 20 | 21 | .card-title { 22 | margin: 5px; 23 | margin-bottom: 2px; 24 | text-align: center; 25 | } 26 | 27 | .card-subtitle { 28 | margin-top: -10px; 29 | margin-bottom: 0; 30 | } 31 | 32 | .card-text:last-child { 33 | margin-bottom: 0; 34 | margin-top: 10px; 35 | } 36 | 37 | .card-link:hover { 38 | text-decoration: none; 39 | } 40 | .card-link + .card-link { 41 | margin-left: 15px; 42 | } 43 | 44 | .card > .list-group:first-child .list-group-item:first-child { 45 | border-top-right-radius: 4px; 46 | border-top-left-radius: 4px; 47 | } 48 | .card > .list-group:last-child .list-group-item:last-child { 49 | border-bottom-right-radius: 4px; 50 | border-bottom-left-radius: 4px; 51 | } 52 | 53 | .card-header { 54 | padding: 10px 15px; 55 | background-color: #f5f5f5; 56 | border-bottom: 1px solid #ddd; 57 | } 58 | .card-header:before, .card-header:after { 59 | content: " "; 60 | display: table; 61 | } 62 | .card-header:after { 63 | clear: both; 64 | } 65 | .card-header:first-child { 66 | border-radius: 4px 4px 0 0; 67 | } 68 | 69 | .card-footer { 70 | padding: 10px 15px; 71 | background-color: #f5f5f5; 72 | border-top: 1px solid #ddd; 73 | } 74 | .card-footer:before, .card-footer:after { 75 | content: " "; 76 | display: table; 77 | } 78 | .card-footer:after { 79 | clear: both; 80 | } 81 | .card-footer:last-child { 82 | border-radius: 0 0 4px 4px; 83 | } 84 | 85 | .card-header-tabs { 86 | margin-right: -5px; 87 | margin-bottom: -10px; 88 | margin-left: -5px; 89 | border-bottom: 0; 90 | } 91 | 92 | .card-header-pills { 93 | margin-right: -5px; 94 | margin-left: -5px; 95 | } 96 | 97 | .card-primary { 98 | background-color: #337ab7; 99 | border-color: #337ab7; 100 | } 101 | .card-primary .card-header, 102 | .card-primary .card-footer { 103 | background-color: transparent; 104 | } 105 | 106 | .card-success { 107 | background-color: #5cb85c; 108 | border-color: #5cb85c; 109 | } 110 | .card-success .card-header, 111 | .card-success .card-footer { 112 | background-color: transparent; 113 | } 114 | 115 | .card-info { 116 | background-color: #5bc0de; 117 | border-color: #5bc0de; 118 | } 119 | .card-info .card-header, 120 | .card-info .card-footer { 121 | background-color: transparent; 122 | } 123 | 124 | .card-warning { 125 | background-color: #f0ad4e; 126 | border-color: #f0ad4e; 127 | } 128 | .card-warning .card-header, 129 | .card-warning .card-footer { 130 | background-color: transparent; 131 | } 132 | 133 | .card-danger { 134 | background-color: #d9534f; 135 | border-color: #d9534f; 136 | } 137 | .card-danger .card-header, 138 | .card-danger .card-footer { 139 | background-color: transparent; 140 | } 141 | 142 | .card-outline-primary { 143 | background-color: transparent; 144 | border-color: #337ab7; 145 | } 146 | 147 | .card-outline-secondary { 148 | background-color: transparent; 149 | border-color: #ccc; 150 | } 151 | 152 | .card-outline-info { 153 | background-color: transparent; 154 | border-color: #5bc0de; 155 | } 156 | 157 | .card-outline-success { 158 | background-color: transparent; 159 | border-color: #5cb85c; 160 | } 161 | 162 | .card-outline-warning { 163 | background-color: transparent; 164 | border-color: #f0ad4e; 165 | } 166 | 167 | .card-outline-danger { 168 | background-color: transparent; 169 | border-color: #d9534f; 170 | } 171 | 172 | .card-inverse .card-header, 173 | .card-inverse .card-footer { 174 | border-color: rgba(255, 255, 255, 0.2); 175 | } 176 | .card-inverse .card-header, 177 | .card-inverse .card-footer, 178 | .card-inverse .card-title, 179 | .card-inverse .card-blockquote { 180 | color: #fff; 181 | } 182 | .card-inverse .card-link, 183 | .card-inverse .card-text, 184 | .card-inverse .card-subtitle, 185 | .card-inverse .card-blockquote .blockquote-footer { 186 | color: rgba(255, 255, 255, 0.65); 187 | } 188 | .card-inverse .card-link:hover, .card-inverse .card-link:focus { 189 | color: #fff; 190 | } 191 | 192 | .card-blockquote { 193 | padding: 0; 194 | margin-bottom: 0; 195 | border-left: 0; 196 | } 197 | 198 | .card-img { 199 | border-radius: .25em; 200 | } 201 | 202 | .card-img-overlay { 203 | position: absolute; 204 | top: 0; 205 | right: 0; 206 | bottom: 0; 207 | left: 0; 208 | padding: 15px; 209 | } 210 | 211 | .card-img-top { 212 | border-top-right-radius: 4px; 213 | border-top-left-radius: 4px; 214 | } 215 | 216 | .card-img-bottom { 217 | border-bottom-right-radius: 4px; 218 | border-bottom-left-radius: 4px; 219 | } 220 | -------------------------------------------------------------------------------- /documentation/styles/compodoc.css: -------------------------------------------------------------------------------- 1 | body { 2 | position: absolute; 3 | width: 100%; 4 | height: 100%; 5 | font-family: 'Roboto', sans-serif; 6 | } 7 | 8 | /* roboto-300 - latin */ 9 | @font-face { 10 | font-family: 'Roboto'; 11 | font-style: normal; 12 | font-weight: 300; 13 | src: url('../fonts/roboto-v15-latin-300.eot'); /* IE9 Compat Modes */ 14 | src: local('Roboto Light'), local('Roboto-Light'), 15 | url('../fonts/roboto-v15-latin-300.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ 16 | url('../fonts/roboto-v15-latin-300.woff2') format('woff2'), /* Super Modern Browsers */ 17 | url('../fonts/roboto-v15-latin-300.woff') format('woff'), /* Modern Browsers */ 18 | url('../fonts/roboto-v15-latin-300.ttf') format('truetype'), /* Safari, Android, iOS */ 19 | url('../fonts/roboto-v15-latin-300.svg#Roboto') format('svg'); /* Legacy iOS */ 20 | } 21 | /* roboto-regular - latin */ 22 | @font-face { 23 | font-family: 'Roboto'; 24 | font-style: normal; 25 | font-weight: 400; 26 | src: url('../fonts/roboto-v15-latin-regular.eot'); /* IE9 Compat Modes */ 27 | src: local('Roboto'), local('Roboto-Regular'), 28 | url('../fonts/roboto-v15-latin-regular.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ 29 | url('../fonts/roboto-v15-latin-regular.woff2') format('woff2'), /* Super Modern Browsers */ 30 | url('../fonts/roboto-v15-latin-regular.woff') format('woff'), /* Modern Browsers */ 31 | url('../fonts/roboto-v15-latin-regular.ttf') format('truetype'), /* Safari, Android, iOS */ 32 | url('../fonts/roboto-v15-latin-regular.svg#Roboto') format('svg'); /* Legacy iOS */ 33 | } 34 | /* roboto-700 - latin */ 35 | @font-face { 36 | font-family: 'Roboto'; 37 | font-style: normal; 38 | font-weight: 700; 39 | src: url('../fonts/roboto-v15-latin-700.eot'); /* IE9 Compat Modes */ 40 | src: local('Roboto Bold'), local('Roboto-Bold'), 41 | url('../fonts/roboto-v15-latin-700.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ 42 | url('../fonts/roboto-v15-latin-700.woff2') format('woff2'), /* Super Modern Browsers */ 43 | url('../fonts/roboto-v15-latin-700.woff') format('woff'), /* Modern Browsers */ 44 | url('../fonts/roboto-v15-latin-700.ttf') format('truetype'), /* Safari, Android, iOS */ 45 | url('../fonts/roboto-v15-latin-700.svg#Roboto') format('svg'); /* Legacy iOS */ 46 | } 47 | 48 | h1 { 49 | font-size: 26px; 50 | } 51 | h2 { 52 | font-size: 22px; 53 | } 54 | h3 { 55 | font-size: 20px; 56 | } 57 | h4, h5 { 58 | font-size: 18px; 59 | } 60 | 61 | /** 62 | * Mobile navbar 63 | */ 64 | 65 | .navbar { 66 | min-height: 50px; 67 | } 68 | 69 | .navbar-brand { 70 | height: 50px; 71 | font-size: 14px; 72 | line-height: 20px; 73 | padding: 15px; 74 | } 75 | 76 | .navbar-static-top { 77 | margin-bottom: 0; 78 | height: 50px; 79 | } 80 | 81 | 82 | /** 83 | * Main container 84 | */ 85 | 86 | .container-fluid { 87 | overflow-y: hidden; 88 | overflow-x: hidden; 89 | } 90 | 91 | .container-fluid.main { 92 | height: 100%; 93 | padding: 0; 94 | } 95 | 96 | .container-fluid.overview { 97 | margin-top: 50px; 98 | } 99 | 100 | .container-fluid.modules, .container-fluid.components, .container-fluid.directives, .container-fluid.classes, .container-fluid.injectables, .container-fluid.pipes, .content.routes table { 101 | margin-top: 25px; 102 | } 103 | 104 | .container-fluid.module { 105 | padding: 0; 106 | margin-top: 0; 107 | } 108 | 109 | .container-fluid.module h3 a { 110 | margin-left: 10px; 111 | color: #333; 112 | } 113 | 114 | .row.main { 115 | height: 100%; 116 | margin: 0; 117 | } 118 | 119 | 120 | /** 121 | * Copyright 122 | */ 123 | 124 | .copyright { 125 | margin: 0; 126 | padding: 15px; 127 | text-align: center; 128 | display: flex; 129 | flex-direction: column; 130 | display: -webkit-flex; 131 | -webkit-flex-direction: column; 132 | align-items: center; 133 | -webkit-align-items: center; 134 | z-index: 1; 135 | } 136 | 137 | .copyright img { 138 | width: 80px; 139 | margin-top: 10px; 140 | } 141 | 142 | .copyright a { 143 | color: #009dff; 144 | text-decoration: underline; 145 | } 146 | 147 | 148 | /** 149 | * Content 150 | */ 151 | 152 | .content { 153 | height: 100%; 154 | overflow-y: auto; 155 | -webkit-overflow-scrolling: touch; 156 | width: calc(100% - 300px); 157 | position: absolute; 158 | top: 0; 159 | left: 300px; 160 | padding: 15px 30px; 161 | } 162 | 163 | .content>h1:first-of-type { 164 | margin-top: 15px 165 | } 166 | 167 | .content>h3:first-of-type { 168 | margin-top: 5px; 169 | } 170 | 171 | .content.readme h1:first-of-type { 172 | margin-top: 0; 173 | } 174 | 175 | .content table { 176 | margin-top: 20px; 177 | } 178 | 179 | 180 | /** 181 | * Icons 182 | */ 183 | 184 | .glyphicon, .fa { 185 | margin-right: 10px; 186 | } 187 | 188 | .fa-code-fork { 189 | margin-right: 14px; 190 | } 191 | 192 | .fa-long-arrow-down { 193 | margin-right: 16px; 194 | } 195 | 196 | 197 | /** 198 | * Menu 199 | */ 200 | 201 | #book-search-input { 202 | padding: 6px; 203 | background: 0 0; 204 | transition: top .5s ease; 205 | background: #fff; 206 | border-bottom: 1px solid rgba(0, 0, 0, .07); 207 | border-top: 1px solid rgba(0, 0, 0, .07); 208 | margin-bottom: 5px; 209 | margin-top: -1px 210 | } 211 | 212 | #book-search-input input, #book-search-input input:focus, #book-search-input input:hover { 213 | width: 100%; 214 | background: 0 0; 215 | border: 1px solid transparent; 216 | box-shadow: none; 217 | outline: 0; 218 | line-height: 22px; 219 | padding: 7px 7px; 220 | color: inherit 221 | } 222 | 223 | .panel-body { 224 | padding: 0px; 225 | } 226 | 227 | .panel-group .panel-heading+.panel-collapse>.list-group, .panel-group .panel-heading+.panel-collapse>.panel-body { 228 | border-top: 0; 229 | } 230 | 231 | .panel-body table tr td { 232 | padding-left: 15px 233 | } 234 | 235 | .panel-body .table { 236 | margin-bottom: 0px; 237 | } 238 | 239 | .panel-group .panel:first-child { 240 | border-top: 0; 241 | } 242 | 243 | .menu { 244 | background: #fafafa; 245 | border-right: 1px solid #e7e7e7; 246 | height: 100%; 247 | padding: 0; 248 | width: 300px; 249 | overflow-y: auto; 250 | -webkit-overflow-scrolling: touch; 251 | } 252 | 253 | .menu ul.list { 254 | list-style: none; 255 | margin: 0; 256 | padding: 0; 257 | } 258 | 259 | .menu ul.list li a { 260 | display: block; 261 | padding: 10px 15px; 262 | border-bottom: none; 263 | color: #364149; 264 | background: 0 0; 265 | text-overflow: ellipsis; 266 | overflow: hidden; 267 | white-space: nowrap; 268 | position: relative 269 | } 270 | 271 | .menu ul.list li a.active { 272 | color: #008cff; 273 | } 274 | 275 | .menu ul.list li.divider { 276 | height: 1px; 277 | margin: 7px 0; 278 | overflow: hidden; 279 | background: rgba(0, 0, 0, .07) 280 | } 281 | 282 | .menu ul.list li.chapter ul.links { 283 | padding-left: 20px; 284 | } 285 | 286 | .menu ul.list li.chapter .simple { 287 | padding: 10px 15px; 288 | position: relative; 289 | } 290 | 291 | .menu .panel-group { 292 | width: 100%; 293 | height: 100%; 294 | overflow-y: auto; 295 | } 296 | 297 | .menu .panel-default { 298 | border-right: none; 299 | border-left: none; 300 | border-bottom: none; 301 | } 302 | 303 | .menu .panel-group .panel-heading+.panel-collapse>.panel-body { 304 | border-top: none; 305 | overflow-y: auto; 306 | max-height: 350px; 307 | } 308 | 309 | .menu .panel-default:last-of-type { 310 | border-bottom: 1px solid #ddd; 311 | } 312 | 313 | .panel-group .panel+.panel { 314 | margin-top: 0; 315 | } 316 | 317 | .panel-group .panel { 318 | z-index: 2; 319 | position: relative; 320 | border-radius: 0; 321 | box-shadow: none; 322 | border-left: 0; 323 | border-right: 0; 324 | } 325 | 326 | .menu a { 327 | color: #3c3c3c; 328 | } 329 | 330 | .xs-menu ul.list li:nth-child(2){ 331 | margin: 0; 332 | background: none; 333 | } 334 | .menu ul.list li:nth-child(2){ 335 | margin: 0; 336 | background: none; 337 | } 338 | .menu .title { 339 | padding: 8px 0; 340 | } 341 | 342 | .menu-toggler { 343 | cursor: pointer; 344 | padding: 5px 10px; 345 | font-size: 16px; 346 | position: absolute; 347 | right: 0; 348 | top: 7px; 349 | } 350 | 351 | .overview .card-title .fa { 352 | font-size: 50px; 353 | } 354 | 355 | .breadcrumb { 356 | background: none; 357 | padding-left: 0; 358 | margin-bottom: 10px; 359 | font-size: 24px; 360 | padding-top: 0; 361 | } 362 | 363 | .breadcrumb a { 364 | text-decoration: underline; 365 | color: #333; 366 | } 367 | 368 | .comment { 369 | margin: 15px 0; 370 | } 371 | 372 | .io-description { 373 | margin: 10px 0; 374 | } 375 | 376 | .io-file { 377 | margin: 20px 0; 378 | } 379 | 380 | .navbar .btn-menu { 381 | position: absolute; 382 | right: 0; 383 | margin: 10px; 384 | } 385 | 386 | .xs-menu { 387 | height: calc(100% - 50px); 388 | display: none; 389 | width: 100%; 390 | overflow-y: scroll; 391 | z-index: 1; 392 | top: 50px; 393 | position: absolute; 394 | } 395 | 396 | .xs-menu .copyright { 397 | margin-top: 20px; 398 | position: relative; 399 | } 400 | 401 | .tab-source-code { 402 | padding: 10px 0; 403 | } 404 | 405 | pre { 406 | padding: 12px 12px; 407 | border: none; 408 | background: #23241f; 409 | } 410 | code { 411 | background: none; 412 | padding: 2px 0; 413 | } 414 | 415 | @media (max-width: 767px) { 416 | .container-fluid { 417 | margin-top: 50px; 418 | } 419 | .container-fluid.main { 420 | height: calc(100% - 50px); 421 | } 422 | .content { 423 | width: 100%; 424 | left: 0; 425 | position: relative; 426 | } 427 | .menu ul.list li.title { 428 | display: none; 429 | } 430 | } 431 | 432 | /** 433 | * Search 434 | */ 435 | 436 | .search-results { 437 | display: none; 438 | max-width: 800px; 439 | margin: 0 auto; 440 | padding: 20px 15px 40px 15px 441 | } 442 | .search-results .no-results { 443 | display: none; 444 | } 445 | 446 | .with-search .search-results { 447 | display: block; 448 | } 449 | .with-search .content-data { 450 | display: none; 451 | } 452 | 453 | .with-search .xs-menu { 454 | height: 51px; 455 | } 456 | .with-search .xs-menu nav { 457 | display: none; 458 | } 459 | 460 | .search-results.no-results .has-results { 461 | display: none; 462 | } 463 | 464 | .search-results.no-results .no-results { 465 | display: block; 466 | } 467 | .search-results .search-results-title { 468 | text-transform: uppercase; 469 | text-align: center; 470 | font-weight: 200; 471 | margin-bottom: 35px; 472 | opacity: .6 473 | } 474 | .search-results ul.search-results-list { 475 | list-style-type: none; 476 | padding-left: 0; 477 | } 478 | .search-results ul.search-results-list li { 479 | margin-bottom: 1.5rem; 480 | padding-bottom: 0.5rem; 481 | } 482 | .search-results ul.search-results-list li p em { 483 | background-color: rgba(255, 220, 0, 0.4); 484 | font-style: normal; 485 | } 486 | 487 | .hljs-line-numbers { 488 | text-align: right; 489 | border-right: 1px solid #ccc; 490 | color: #999; 491 | -webkit-touch-callout: none; 492 | -webkit-user-select: none; 493 | -khtml-user-select: none; 494 | -moz-user-select: none; 495 | -ms-user-select: none; 496 | user-select: none; 497 | } 498 | 499 | .jsdoc-params { 500 | list-style: square; 501 | padding-left: 20px; 502 | margin-top: 10px; 503 | margin-bottom: 0 !important; 504 | } 505 | .jsdoc-params li { 506 | padding-bottom: 10px; 507 | } 508 | 509 | i { 510 | font-style: italic; 511 | } 512 | 513 | .coverage a { 514 | color: #333; 515 | text-decoration: underline; 516 | } 517 | 518 | .coverage tr.low { 519 | background: rgba(216, 96, 75, 0.75); 520 | } 521 | .coverage tr.medium { 522 | background: rgba(218, 178, 38, 0.75); 523 | } 524 | .coverage tr.good { 525 | background: rgba(143, 189, 8, 0.75); 526 | } 527 | .coverage tr.very-good { 528 | background: rgba(77, 199, 31, 0.75); 529 | } 530 | 531 | .coverage-header { 532 | background: #fafafa; 533 | } 534 | thead.coverage-header >tr>td, thead.coverage-header>tr>th { 535 | border-bottom-width: 0; 536 | } 537 | .coverage-count { 538 | color: grey; 539 | font-size: 12px; 540 | margin-left: 10px; 541 | display: inline-block; 542 | width: 50px; 543 | } 544 | .coverage-badge { 545 | background: #5d5d5d; 546 | border-radius: 4px; 547 | display: inline-block; 548 | color: white; 549 | padding: 4px; 550 | padding-right: 0; 551 | padding-left: 8px; 552 | } 553 | .coverage-badge .count{ 554 | padding: 6px; 555 | margin-left: 5px; 556 | border-top-right-radius: 4px; 557 | border-bottom-right-radius: 4px; 558 | } 559 | .coverage-badge .count.low { 560 | background: #d8624c; 561 | } 562 | .coverage-badge .count.medium { 563 | background: #dab226; 564 | } 565 | .coverage-badge .count.good { 566 | background: #8fbd08; 567 | } 568 | .coverage-badge .count.very-good { 569 | background: #4dc71f; 570 | } 571 | 572 | .content ul { 573 | list-style: disc; 574 | padding-left: 2em; 575 | margin-top: 0; 576 | margin-bottom: 16px; 577 | } 578 | .content ul ul { 579 | list-style-type: circle; 580 | } 581 | .compodoc-table { 582 | width: inherit; 583 | } 584 | .compodoc-table thead { 585 | font-weight: bold; 586 | } 587 | .modifier { 588 | background: #9a9a9a; 589 | padding: 1px 5px; 590 | color: white; 591 | border-radius: 4px; 592 | } 593 | .modifier-icon { 594 | color: #c7254e; 595 | } 596 | .modifier-icon.method { 597 | color: white; 598 | background: #c7254e; 599 | padding: 4px; 600 | border-radius: 8px; 601 | font-size: 10px; 602 | margin-right: 2px; 603 | } 604 | .modifier-icon.method.square { 605 | border-radius: 4px; 606 | } 607 | .modifier-icon.method.export { 608 | display: none; 609 | } 610 | .modifier-icon.method .fa-circle, .modifier-icon.method .fa-square { 611 | display: none; 612 | } 613 | .modifier-icon.method .fa-lock { 614 | margin-right: 0; 615 | } 616 | -------------------------------------------------------------------------------- /documentation/styles/laravel.css: -------------------------------------------------------------------------------- 1 | .navbar-default .navbar-brand { 2 | color: #f4645f; 3 | text-decoration: none; 4 | font-size: 16px; 5 | } 6 | 7 | .menu ul.list li a[data-type="chapter-link"], .menu ul.list li.chapter .simple { 8 | color: #525252; 9 | border-bottom: 1px dashed rgba(0,0,0,.1); 10 | } 11 | 12 | .content h1, .content h2, .content h3, .content h4, .content h5 { 13 | color: #292e31; 14 | font-weight: normal; 15 | } 16 | 17 | .content { 18 | color: #4c555a; 19 | } 20 | 21 | a { 22 | color: #f4645f; 23 | text-decoration: underline; 24 | } 25 | a:hover { 26 | color: #f1362f; 27 | } 28 | 29 | .menu ul.list li:nth-child(2) { 30 | margin-top: 0; 31 | } 32 | 33 | .menu ul.list li.title a { 34 | color: #f4645f; 35 | text-decoration: none; 36 | font-size: 16px; 37 | } 38 | 39 | .menu ul.list li a { 40 | color: #f4645f; 41 | text-decoration: none; 42 | } 43 | .menu ul.list li a.active { 44 | color: #f4645f; 45 | font-weight: bold; 46 | } 47 | 48 | code { 49 | box-sizing: border-box; 50 | display: inline-block; 51 | padding: 0 5px; 52 | background: #f0f2f1; 53 | border: 1px solid #f0f4f7; 54 | border-radius: 3px; 55 | color: #b93d6a; 56 | font-size: 13px; 57 | line-height: 20px; 58 | box-shadow: 0 1px 1px rgba(0,0,0,.125); 59 | } 60 | 61 | pre { 62 | margin: 0; 63 | padding: 12px 12px; 64 | background: rgba(238,238,238,.35); 65 | border-radius: 3px; 66 | font-size: 13px; 67 | line-height: 1.5em; 68 | font-weight: 500; 69 | box-shadow: 0 1px 1px rgba(0,0,0,.125); 70 | } 71 | 72 | pre code.hljs { 73 | border: none; 74 | background: none; 75 | box-shadow: none; 76 | } 77 | 78 | /* 79 | Atom One Light by Daniel Gamage 80 | Original One Light Syntax theme from https://github.com/atom/one-light-syntax 81 | base: #fafafa 82 | mono-1: #383a42 83 | mono-2: #686b77 84 | mono-3: #a0a1a7 85 | hue-1: #0184bb 86 | hue-2: #4078f2 87 | hue-3: #a626a4 88 | hue-4: #50a14f 89 | hue-5: #e45649 90 | hue-5-2: #c91243 91 | hue-6: #986801 92 | hue-6-2: #c18401 93 | */ 94 | 95 | .hljs { 96 | display: block; 97 | overflow-x: auto; 98 | padding: 0.5em; 99 | color: #383a42; 100 | background: #fafafa; 101 | } 102 | 103 | .hljs-comment, 104 | .hljs-quote { 105 | color: #a0a1a7; 106 | font-style: italic; 107 | } 108 | 109 | .hljs-doctag, 110 | .hljs-keyword, 111 | .hljs-formula { 112 | color: #a626a4; 113 | } 114 | 115 | .hljs-section, 116 | .hljs-name, 117 | .hljs-selector-tag, 118 | .hljs-deletion, 119 | .hljs-subst { 120 | color: #e45649; 121 | } 122 | 123 | .hljs-literal { 124 | color: #0184bb; 125 | } 126 | 127 | .hljs-string, 128 | .hljs-regexp, 129 | .hljs-addition, 130 | .hljs-attribute, 131 | .hljs-meta-string { 132 | color: #50a14f; 133 | } 134 | 135 | .hljs-built_in, 136 | .hljs-class .hljs-title { 137 | color: #c18401; 138 | } 139 | 140 | .hljs-attr, 141 | .hljs-variable, 142 | .hljs-template-variable, 143 | .hljs-type, 144 | .hljs-selector-class, 145 | .hljs-selector-attr, 146 | .hljs-selector-pseudo, 147 | .hljs-number { 148 | color: #986801; 149 | } 150 | 151 | .hljs-symbol, 152 | .hljs-bullet, 153 | .hljs-link, 154 | .hljs-meta, 155 | .hljs-selector-id, 156 | .hljs-title { 157 | color: #4078f2; 158 | } 159 | 160 | .hljs-emphasis { 161 | font-style: italic; 162 | } 163 | 164 | .hljs-strong { 165 | font-weight: bold; 166 | } 167 | 168 | .hljs-link { 169 | text-decoration: underline; 170 | } 171 | -------------------------------------------------------------------------------- /documentation/styles/monokai-sublime.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Monokai Sublime style. Derived from Monokai by noformnocontent http://nn.mit-license.org/ 4 | 5 | */ 6 | 7 | .hljs { 8 | display: block; 9 | overflow-x: auto; 10 | padding: 0.5em; 11 | background: #23241f; 12 | } 13 | 14 | .hljs, 15 | .hljs-tag, 16 | .hljs-subst { 17 | color: #f8f8f2; 18 | } 19 | 20 | .hljs-strong, 21 | .hljs-emphasis { 22 | color: #a8a8a2; 23 | } 24 | 25 | .hljs-bullet, 26 | .hljs-quote, 27 | .hljs-number, 28 | .hljs-regexp, 29 | .hljs-literal, 30 | .hljs-link { 31 | color: #ae81ff; 32 | } 33 | 34 | .hljs-code, 35 | .hljs-title, 36 | .hljs-section, 37 | .hljs-selector-class { 38 | color: #a6e22e; 39 | } 40 | 41 | .hljs-strong { 42 | font-weight: bold; 43 | } 44 | 45 | .hljs-emphasis { 46 | font-style: italic; 47 | } 48 | 49 | .hljs-keyword, 50 | .hljs-selector-tag, 51 | .hljs-name, 52 | .hljs-attr { 53 | color: #f92672; 54 | } 55 | 56 | .hljs-symbol, 57 | .hljs-attribute { 58 | color: #66d9ef; 59 | } 60 | 61 | .hljs-params, 62 | .hljs-class .hljs-title { 63 | color: #f8f8f2; 64 | } 65 | 66 | .hljs-string, 67 | .hljs-type, 68 | .hljs-built_in, 69 | .hljs-builtin-name, 70 | .hljs-selector-id, 71 | .hljs-selector-attr, 72 | .hljs-selector-pseudo, 73 | .hljs-addition, 74 | .hljs-variable, 75 | .hljs-template-variable { 76 | color: #e6db74; 77 | } 78 | 79 | .hljs-comment, 80 | .hljs-deletion, 81 | .hljs-meta { 82 | color: #75715e; 83 | } 84 | -------------------------------------------------------------------------------- /documentation/styles/original.css: -------------------------------------------------------------------------------- 1 | .navbar-default .navbar-brand, .menu ul.list li.title { 2 | font-weight: bold; 3 | color: #3c3c3c; 4 | padding-bottom: 5px; 5 | } 6 | 7 | .menu ul.list li a[data-type="chapter-link"], .menu ul.list li.chapter .simple { 8 | font-weight: bold; 9 | border-top: 1px solid #ddd; 10 | border-bottom: 1px solid #ddd; 11 | font-size: 14px; 12 | } 13 | 14 | .menu ul.list li a[href="./routes.html"] { 15 | border-bottom: none; 16 | } 17 | 18 | .menu ul.list > li:nth-child(2) { 19 | display: none; 20 | } 21 | 22 | .menu ul.list li.chapter ul.links { 23 | background: #fff; 24 | padding-left: 0; 25 | } 26 | 27 | .menu ul.list li.chapter ul.links li { 28 | border-bottom: 1px solid #ddd; 29 | padding-left: 20px; 30 | } 31 | 32 | .menu ul.list li.chapter ul.links li:last-child { 33 | border-bottom: none; 34 | } 35 | 36 | .menu ul.list li a.active { 37 | color: inherit; 38 | font-weight: bold; 39 | } 40 | 41 | #book-search-input { 42 | margin-bottom: 0; 43 | border-bottom: none; 44 | } 45 | .menu ul.list li.divider { 46 | margin: 0; 47 | } 48 | -------------------------------------------------------------------------------- /documentation/styles/postmark.css: -------------------------------------------------------------------------------- 1 | .navbar-default { 2 | background: #FFDE00; 3 | border: none; 4 | } 5 | .navbar-default .navbar-brand { 6 | color: #333; 7 | font-weight: bold; 8 | } 9 | .menu { 10 | background: #333; 11 | color: #fcfcfc; 12 | } 13 | .menu ul.list li a { 14 | color: #333; 15 | } 16 | 17 | .menu ul.list li.title { 18 | background: #FFDE00; 19 | color: #333; 20 | padding-bottom: 5px; 21 | } 22 | 23 | .menu ul.list li:nth-child(2) { 24 | margin-top: 0; 25 | } 26 | 27 | .menu ul.list li.chapter a, .menu ul.list li.chapter .simple { 28 | color: white; 29 | text-decoration: none; 30 | } 31 | 32 | .menu ul.list li.chapter ul.links a { 33 | color: #949494; 34 | text-transform: none; 35 | padding-left: 35px; 36 | } 37 | .menu ul.list li.chapter ul.links a:hover, .menu ul.list li.chapter ul.links a.active { 38 | color: #FFDE00; 39 | } 40 | 41 | .menu ul.list li.chapter ul.links { 42 | padding-left: 0; 43 | } 44 | 45 | .menu ul.list li.divider { 46 | background: rgba(255, 255, 255, 0.07); 47 | } 48 | 49 | #book-search-input input, #book-search-input input:focus, #book-search-input input:hover { 50 | color: #949494; 51 | } 52 | 53 | .copyright { 54 | color: #b3b3b3; 55 | } 56 | 57 | .content { 58 | background: #fcfcfc; 59 | } 60 | 61 | .content a { 62 | color: #007DCC; 63 | } 64 | .content a:visited { 65 | color: #0165a5; 66 | } 67 | .copyright { 68 | background: #272525; 69 | } 70 | .menu ul.list li:nth-last-child(2) { 71 | background: none; 72 | } 73 | .list-group-item:first-child, .list-group-item:last-child { 74 | border-radius: 0; 75 | } 76 | 77 | .menu ul.list li.title a { 78 | text-decoration: none; 79 | font-weight: bold; 80 | } 81 | .menu ul.list li.title a:hover { 82 | background: rgba(255,255,255,0.1); 83 | } 84 | 85 | .breadcrumb>li+li:before { 86 | content: "»\00a0" 87 | } 88 | 89 | .breadcrumb { 90 | padding-bottom: 15px; 91 | border-bottom: 1px solid #e1e4e5; 92 | } 93 | code { 94 | white-space: nowrap; 95 | max-width: 100%; 96 | background: #F5F5F5; 97 | border: solid 1px #e1e4e5; 98 | padding: 2px 5px; 99 | color: #666666; 100 | overflow-x: auto; 101 | border-radius: 0; 102 | } 103 | pre { 104 | white-space: pre; 105 | margin: 0; 106 | padding: 12px 12px; 107 | font-size: 12px; 108 | line-height: 1.5; 109 | display: block; 110 | overflow: auto; 111 | color: #404040; 112 | background: #f3f3f3; 113 | } 114 | pre code.hljs { 115 | border: none; 116 | background: inherit; 117 | } 118 | 119 | /* 120 | Atom One Light by Daniel Gamage 121 | Original One Light Syntax theme from https://github.com/atom/one-light-syntax 122 | base: #fafafa 123 | mono-1: #383a42 124 | mono-2: #686b77 125 | mono-3: #a0a1a7 126 | hue-1: #0184bb 127 | hue-2: #4078f2 128 | hue-3: #a626a4 129 | hue-4: #50a14f 130 | hue-5: #e45649 131 | hue-5-2: #c91243 132 | hue-6: #986801 133 | hue-6-2: #c18401 134 | */ 135 | 136 | .hljs { 137 | display: block; 138 | overflow-x: auto; 139 | padding: 0.5em; 140 | color: #383a42; 141 | background: #fafafa; 142 | } 143 | 144 | .hljs-comment, 145 | .hljs-quote { 146 | color: #a0a1a7; 147 | font-style: italic; 148 | } 149 | 150 | .hljs-doctag, 151 | .hljs-keyword, 152 | .hljs-formula { 153 | color: #a626a4; 154 | } 155 | 156 | .hljs-section, 157 | .hljs-name, 158 | .hljs-selector-tag, 159 | .hljs-deletion, 160 | .hljs-subst { 161 | color: #e45649; 162 | } 163 | 164 | .hljs-literal { 165 | color: #0184bb; 166 | } 167 | 168 | .hljs-string, 169 | .hljs-regexp, 170 | .hljs-addition, 171 | .hljs-attribute, 172 | .hljs-meta-string { 173 | color: #50a14f; 174 | } 175 | 176 | .hljs-built_in, 177 | .hljs-class .hljs-title { 178 | color: #c18401; 179 | } 180 | 181 | .hljs-attr, 182 | .hljs-variable, 183 | .hljs-template-variable, 184 | .hljs-type, 185 | .hljs-selector-class, 186 | .hljs-selector-attr, 187 | .hljs-selector-pseudo, 188 | .hljs-number { 189 | color: #986801; 190 | } 191 | 192 | .hljs-symbol, 193 | .hljs-bullet, 194 | .hljs-link, 195 | .hljs-meta, 196 | .hljs-selector-id, 197 | .hljs-title { 198 | color: #4078f2; 199 | } 200 | 201 | .hljs-emphasis { 202 | font-style: italic; 203 | } 204 | 205 | .hljs-strong { 206 | font-weight: bold; 207 | } 208 | 209 | .hljs-link { 210 | text-decoration: underline; 211 | } 212 | -------------------------------------------------------------------------------- /documentation/styles/readthedocs.css: -------------------------------------------------------------------------------- 1 | .navbar-default { 2 | background: #2980B9; 3 | border: none; 4 | } 5 | .navbar-default .navbar-brand { 6 | color: #fcfcfc; 7 | } 8 | .menu { 9 | background: #343131; 10 | color: #fcfcfc; 11 | } 12 | .menu ul.list li a { 13 | color: #fcfcfc; 14 | } 15 | 16 | .menu ul.list li a.active { 17 | color: #0099e5; 18 | } 19 | 20 | .menu ul.list li.title { 21 | background: #2980B9; 22 | padding-bottom: 5px; 23 | } 24 | 25 | .menu ul.list li:nth-child(2) { 26 | margin-top: 0; 27 | } 28 | 29 | .menu ul.list li.chapter a, .menu ul.list li.chapter .simple { 30 | color: #555; 31 | text-transform: uppercase; 32 | text-decoration: none; 33 | } 34 | 35 | .menu ul.list li.chapter ul.links a { 36 | color: #b3b3b3; 37 | text-transform: none; 38 | padding-left: 35px; 39 | } 40 | .menu ul.list li.chapter ul.links a:hover { 41 | background: #4E4A4A; 42 | } 43 | 44 | .menu ul.list li.chapter ul.links { 45 | padding-left: 0; 46 | } 47 | 48 | .menu ul.list li.divider { 49 | background: rgba(255, 255, 255, 0.07); 50 | } 51 | 52 | #book-search-input input, #book-search-input input:focus, #book-search-input input:hover { 53 | color: #949494; 54 | } 55 | 56 | .copyright { 57 | color: #b3b3b3; 58 | } 59 | 60 | .content { 61 | background: #fcfcfc; 62 | } 63 | 64 | .content a { 65 | color: #2980B9; 66 | } 67 | .content a:hover { 68 | color: #3091d1; 69 | } 70 | .content a:visited { 71 | color: #9B59B6; 72 | } 73 | .copyright { 74 | background: #272525; 75 | } 76 | .menu ul.list li:nth-last-child(2) { 77 | background: none; 78 | } 79 | code { 80 | white-space: nowrap; 81 | max-width: 100%; 82 | background: #fff; 83 | border: solid 1px #e1e4e5; 84 | padding: 2px 5px; 85 | color: #E74C3C; 86 | overflow-x: auto; 87 | border-radius: 0; 88 | } 89 | pre { 90 | white-space: pre; 91 | margin: 0; 92 | padding: 12px 12px; 93 | font-size: 12px; 94 | line-height: 1.5; 95 | display: block; 96 | overflow: auto; 97 | color: #404040; 98 | background: rgba(238,238,238,.35); 99 | } 100 | pre code.hljs { 101 | border: none; 102 | background: inherit; 103 | } 104 | 105 | /* 106 | Atom One Light by Daniel Gamage 107 | Original One Light Syntax theme from https://github.com/atom/one-light-syntax 108 | base: #fafafa 109 | mono-1: #383a42 110 | mono-2: #686b77 111 | mono-3: #a0a1a7 112 | hue-1: #0184bb 113 | hue-2: #4078f2 114 | hue-3: #a626a4 115 | hue-4: #50a14f 116 | hue-5: #e45649 117 | hue-5-2: #c91243 118 | hue-6: #986801 119 | hue-6-2: #c18401 120 | */ 121 | 122 | .hljs { 123 | display: block; 124 | overflow-x: auto; 125 | padding: 0.5em; 126 | color: #383a42; 127 | background: #fafafa; 128 | } 129 | 130 | .hljs-comment, 131 | .hljs-quote { 132 | color: #a0a1a7; 133 | font-style: italic; 134 | } 135 | 136 | .hljs-doctag, 137 | .hljs-keyword, 138 | .hljs-formula { 139 | color: #a626a4; 140 | } 141 | 142 | .hljs-section, 143 | .hljs-name, 144 | .hljs-selector-tag, 145 | .hljs-deletion, 146 | .hljs-subst { 147 | color: #e45649; 148 | } 149 | 150 | .hljs-literal { 151 | color: #0184bb; 152 | } 153 | 154 | .hljs-string, 155 | .hljs-regexp, 156 | .hljs-addition, 157 | .hljs-attribute, 158 | .hljs-meta-string { 159 | color: #50a14f; 160 | } 161 | 162 | .hljs-built_in, 163 | .hljs-class .hljs-title { 164 | color: #c18401; 165 | } 166 | 167 | .hljs-attr, 168 | .hljs-variable, 169 | .hljs-template-variable, 170 | .hljs-type, 171 | .hljs-selector-class, 172 | .hljs-selector-attr, 173 | .hljs-selector-pseudo, 174 | .hljs-number { 175 | color: #986801; 176 | } 177 | 178 | .hljs-symbol, 179 | .hljs-bullet, 180 | .hljs-link, 181 | .hljs-meta, 182 | .hljs-selector-id, 183 | .hljs-title { 184 | color: #4078f2; 185 | } 186 | 187 | .hljs-emphasis { 188 | font-style: italic; 189 | } 190 | 191 | .hljs-strong { 192 | font-weight: bold; 193 | } 194 | 195 | .hljs-link { 196 | text-decoration: underline; 197 | } 198 | 199 | .list-group-item:first-child, .list-group-item:last-child { 200 | border-radius: 0; 201 | } 202 | 203 | .menu ul.list li.title a { 204 | text-decoration: none; 205 | } 206 | .menu ul.list li.title a:hover { 207 | background: rgba(255,255,255,0.1); 208 | } 209 | 210 | .breadcrumb>li+li:before { 211 | content: "»\00a0" 212 | } 213 | 214 | .breadcrumb { 215 | padding-bottom: 15px; 216 | border-bottom: 1px solid #e1e4e5; 217 | } 218 | -------------------------------------------------------------------------------- /documentation/styles/reset.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, body, div, span, applet, object, iframe, 7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 8 | a, abbr, acronym, address, big, cite, code, 9 | del, dfn, em, img, ins, kbd, q, s, samp, 10 | small, strike, strong, sub, sup, tt, var, 11 | b, u, i, center, 12 | dl, dt, dd, ol, ul, li, 13 | fieldset, form, label, legend, 14 | table, caption, tbody, tfoot, thead, tr, th, td, 15 | article, aside, canvas, details, embed, 16 | figure, figcaption, footer, header, hgroup, 17 | menu, nav, output, ruby, section, summary, 18 | time, mark, audio, video { 19 | margin: 0; 20 | padding: 0; 21 | border: 0; 22 | font-size: 100%; 23 | font: inherit; 24 | vertical-align: baseline; 25 | } 26 | /* HTML5 display-role reset for older browsers */ 27 | article, aside, details, figcaption, figure, 28 | footer, header, hgroup, menu, nav, section { 29 | display: block; 30 | } 31 | body { 32 | line-height: 1; 33 | } 34 | ol, ul { 35 | list-style: none; 36 | } 37 | blockquote, q { 38 | quotes: none; 39 | } 40 | blockquote:before, blockquote:after, 41 | q:before, q:after { 42 | content: ''; 43 | content: none; 44 | } 45 | table { 46 | border-collapse: collapse; 47 | border-spacing: 0; 48 | } -------------------------------------------------------------------------------- /documentation/styles/stripe.css: -------------------------------------------------------------------------------- 1 | .navbar-default .navbar-brand { 2 | color: #0099e5; 3 | } 4 | 5 | .menu ul.list li a[data-type="chapter-link"], .menu ul.list li.chapter .simple { 6 | color: #939da3; 7 | text-transform: uppercase; 8 | } 9 | 10 | .content h1, .content h2, .content h3, .content h4, .content h5 { 11 | color: #292e31; 12 | font-weight: normal; 13 | } 14 | 15 | .content { 16 | color: #4c555a; 17 | } 18 | 19 | .menu ul.list li.title { 20 | padding: 5px 0; 21 | } 22 | 23 | a { 24 | color: #0099e5; 25 | text-decoration: none; 26 | } 27 | a:hover { 28 | color: #292e31; 29 | text-decoration: none; 30 | } 31 | 32 | .menu ul.list li:nth-child(2) { 33 | margin-top: 0; 34 | } 35 | 36 | .menu ul.list li.title a, .navbar a { 37 | color: #0099e5; 38 | text-decoration: none; 39 | font-size: 16px; 40 | } 41 | 42 | .menu ul.list li a.active { 43 | color: #0099e5; 44 | } 45 | 46 | code { 47 | box-sizing: border-box; 48 | display: inline-block; 49 | padding: 0 5px; 50 | background: #fafcfc; 51 | border: 1px solid #f0f4f7; 52 | border-radius: 4px; 53 | color: #b93d6a; 54 | font-size: 13px; 55 | line-height: 20px 56 | } 57 | 58 | pre { 59 | margin: 0; 60 | padding: 12px 12px; 61 | background: #272b2d; 62 | border-radius: 5px; 63 | font-size: 13px; 64 | line-height: 1.5em; 65 | font-weight: 500 66 | } 67 | 68 | pre code.hljs { 69 | border: none; 70 | background: #272b2d; 71 | } 72 | -------------------------------------------------------------------------------- /documentation/styles/style.css: -------------------------------------------------------------------------------- 1 | @import "./reset.css"; 2 | @import "./bootstrap.min.css"; 3 | @import "./bootstrap-card.css"; 4 | @import "./monokai-sublime.css"; 5 | @import "./font-awesome.min.css"; 6 | @import "./compodoc.css"; 7 | -------------------------------------------------------------------------------- /documentation/styles/vagrant.css: -------------------------------------------------------------------------------- 1 | .navbar-default .navbar-brand { 2 | background: white; 3 | color: #8d9ba8; 4 | } 5 | 6 | .menu .list { 7 | background: #0c5593; 8 | } 9 | 10 | .menu .chapter { 11 | padding: 0 20px; 12 | } 13 | 14 | .menu ul.list li a[data-type="chapter-link"], .menu ul.list li.chapter .simple { 15 | color: white; 16 | text-transform: uppercase; 17 | border-bottom: 1px solid rgba(255,255,255,0.4); 18 | } 19 | 20 | .content h1, .content h2, .content h3, .content h4, .content h5 { 21 | color: #292e31; 22 | font-weight: normal; 23 | } 24 | 25 | .content { 26 | color: #4c555a; 27 | } 28 | 29 | a { 30 | color: #0094bf; 31 | text-decoration: underline; 32 | } 33 | a:hover { 34 | color: #f1362f; 35 | } 36 | 37 | .menu ul.list li.title { 38 | background: white; 39 | padding-bottom: 5px; 40 | } 41 | 42 | .menu ul.list li:nth-child(2) { 43 | margin-top: 0; 44 | } 45 | 46 | .menu ul.list li:nth-last-child(2) { 47 | background: none; 48 | } 49 | 50 | .menu ul.list li.title a { 51 | padding: 10px 15px; 52 | } 53 | 54 | .menu ul.list li.title a, .navbar a { 55 | color: #8d9ba8; 56 | text-decoration: none; 57 | font-size: 16px; 58 | font-weight: 300; 59 | } 60 | 61 | .menu ul.list li a { 62 | color: white; 63 | padding: 10px; 64 | font-weight: 300; 65 | text-decoration: none; 66 | } 67 | .menu ul.list li a.active { 68 | color: white; 69 | font-weight: bold; 70 | } 71 | 72 | .copyright { 73 | color: white; 74 | background: #000; 75 | } 76 | 77 | code { 78 | box-sizing: border-box; 79 | display: inline-block; 80 | padding: 0 5px; 81 | background: rgba(0,148,191,0.1); 82 | border: 1px solid #f0f4f7; 83 | border-radius: 3px; 84 | color: #0094bf; 85 | font-size: 13px; 86 | line-height: 20px; 87 | } 88 | 89 | pre { 90 | margin: 0; 91 | padding: 12px 12px; 92 | background: rgba(238,238,238,.35); 93 | border-radius: 3px; 94 | font-size: 13px; 95 | line-height: 1.5em; 96 | font-weight: 500; 97 | } 98 | 99 | pre code.hljs { 100 | border: none; 101 | background: none; 102 | box-shadow: none; 103 | } 104 | 105 | /* 106 | Atom One Light by Daniel Gamage 107 | Original One Light Syntax theme from https://github.com/atom/one-light-syntax 108 | base: #fafafa 109 | mono-1: #383a42 110 | mono-2: #686b77 111 | mono-3: #a0a1a7 112 | hue-1: #0184bb 113 | hue-2: #4078f2 114 | hue-3: #a626a4 115 | hue-4: #50a14f 116 | hue-5: #e45649 117 | hue-5-2: #c91243 118 | hue-6: #986801 119 | hue-6-2: #c18401 120 | */ 121 | 122 | .hljs { 123 | display: block; 124 | overflow-x: auto; 125 | padding: 0.5em; 126 | color: #383a42; 127 | background: #fafafa; 128 | } 129 | 130 | .hljs-comment, 131 | .hljs-quote { 132 | color: #a0a1a7; 133 | font-style: italic; 134 | } 135 | 136 | .hljs-doctag, 137 | .hljs-keyword, 138 | .hljs-formula { 139 | color: #a626a4; 140 | } 141 | 142 | .hljs-section, 143 | .hljs-name, 144 | .hljs-selector-tag, 145 | .hljs-deletion, 146 | .hljs-subst { 147 | color: #e45649; 148 | } 149 | 150 | .hljs-literal { 151 | color: #0184bb; 152 | } 153 | 154 | .hljs-string, 155 | .hljs-regexp, 156 | .hljs-addition, 157 | .hljs-attribute, 158 | .hljs-meta-string { 159 | color: #50a14f; 160 | } 161 | 162 | .hljs-built_in, 163 | .hljs-class .hljs-title { 164 | color: #c18401; 165 | } 166 | 167 | .hljs-attr, 168 | .hljs-variable, 169 | .hljs-template-variable, 170 | .hljs-type, 171 | .hljs-selector-class, 172 | .hljs-selector-attr, 173 | .hljs-selector-pseudo, 174 | .hljs-number { 175 | color: #986801; 176 | } 177 | 178 | .hljs-symbol, 179 | .hljs-bullet, 180 | .hljs-link, 181 | .hljs-meta, 182 | .hljs-selector-id, 183 | .hljs-title { 184 | color: #4078f2; 185 | } 186 | 187 | .hljs-emphasis { 188 | font-style: italic; 189 | } 190 | 191 | .hljs-strong { 192 | font-weight: bold; 193 | } 194 | 195 | .hljs-link { 196 | text-decoration: underline; 197 | } 198 | -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # OS 14 | .DS_Store 15 | 16 | # Tests 17 | /coverage 18 | /.nyc_output 19 | 20 | # IDEs and editors 21 | /.idea 22 | .project 23 | .classpath 24 | .c9/ 25 | *.launch 26 | .settings/ 27 | *.sublime-workspace 28 | 29 | # IDE - VSCode 30 | .vscode/* 31 | !.vscode/settings.json 32 | !.vscode/tasks.json 33 | !.vscode/launch.json 34 | !.vscode/extensions.json -------------------------------------------------------------------------------- /examples/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Trying EventStoreDb connector 2 | 3 | ## Note on nodeJS version 4 | 5 | You'll find a compatible version you refer to in [this .nvmrc](../.nvmrc). You might want to use nvm direcly, and in this case, nvm will read direcly this configuration file and adjust the version. 6 | 7 | ## Installation 8 | 9 | ``` 10 | yarn install 11 | ``` 12 | 13 | ## Start 14 | 15 | You have to run a eventstore container with correct ports. You can put the ports you want, but it will work natively with this conf : 16 | 17 | ``` 18 | docker run --name esdb-node -d \ 19 | -it -p 2113:2113 -p 1113:1113 \ 20 | eventstore/eventstore:latest \ 21 | --insecure \ 22 | --run-projections=All \ 23 | --enable-atom-pub-over-http 24 | ``` 25 | 26 | Then, the REST API will start by running : 27 | 28 | ``` 29 | yarn start 30 | ``` 31 | 32 | ## Usage 33 | 34 | #### running basic CQRS example with eventStore 35 | 36 | The example is based on [CQRS module](https://github.com/kamilmysliwiec/nest-cqrs) usage example of [Nest](https://github.com/kamilmysliwiec/nest) framework. 37 | 38 | # Story 39 | 40 | The hero fight the dragon in an epic fight. 41 | he kills the dragon, and a dragon can be killed only once. 42 | We need to keep only the last 5 move of the fight and delete them after 3 days for faery RGPD. 43 | 44 | When it's done the hero search and find an item. 45 | He can find this special item only once until he drop hit. 46 | 47 | #### How to connect 48 | 49 | You can see how the import is done in the [EventStoreHeroesModule](./src/heroes/event-store-heroes.module.ts) : 50 | 51 | ```typescript 52 | CqrsEventStoreModule.register( 53 | eventStoreConnectionConfig, 54 | eventStoreSubsystems, 55 | eventBusConfig, 56 | ), 57 | ``` 58 | 59 | # Let's run it 60 | 61 | Once the API is running, you only have to run this REST request : 62 | 63 | ``` 64 | curl -XPUT localhost:3000/hero/200/kill -d'{ "dragonId" : "test3" }' 65 | ``` 66 | 67 | All the events emitted by the example will be stored in the event store. 68 | 69 | You can call it multiple time to try idemptotency 70 | see events on [the dashboard](http://localhost:20113/web/index.html#/streams/hero-200) of event store 71 | 72 | ## What the code does 73 | 74 | - `heroes.controller` send to the command bus `kill-dragon.command` configured with http body 75 | - `kill-dragon.handler` fetch the hero and build a `hero.aggregate` merging db and command data 76 | - `kill-dragon.handler` call killEnemy() on `hero.aggregate` 77 | - `hero.aggregate` kill the enemy and apply `hero-killed-dragon.event` 78 | - `kill-dragon.handler` commit() on `hero.aggregate` (write all events on aggregate) 79 | - Eventstore stores the event on event's stream, applying idempotency (mean if you do it twice event is ignored the other times) 80 | - `hero-killed-dragon.handler` receive the event from Eventstore and log (can run in another process) 81 | - `heroes-sagas` receive the event from Eventstore do some logic and send to commandBus `drop-ancient-item-command` 82 | - `drop-ancient-item.handler` fetch the hero and build a `hero.aggregate` merging db and command data 83 | - `drop-ancient-item.handler` addItem() on `hero.aggregate` 84 | - `hero.aggregate` apply `hero-found-item.event` 85 | - `drop-ancient-item.handler` dropItem() on `hero.aggregate` 86 | - `hero.aggregate` apply `hero-drop-item.event` 87 | - `kill-dragon.handler` commit() on `hero.aggregate` (can be done after each call) 88 | - Eventstore store the event on event's stream 89 | 90 | ## Data transfer object 91 | 92 | Stupid data object with optionnals validation rules 93 | 94 | ## Repository 95 | 96 | Link with the database (mocked here) 97 | 98 | ## Aggregate Root 99 | 100 | Where everything on an entity and it's child appends. 101 | Updated only using events 102 | 103 | ## Command 104 | 105 | Data transfer object with a name that you must execute 106 | 107 | ## Command handler 108 | 109 | Do the logic : 110 | Read the Command, merge DB and command's data on the Aggregate, play with aggregate's methods, commit one or multiple time when needed 111 | 112 | ## Event 113 | 114 | Data transfer object with a name and a stream name. 115 | Can have a UUID, metadata and an expected version 116 | 117 | ## Event Handler 118 | 119 | Do the side effects. 120 | Receive event and do logic with them : usually update or insert on the database 121 | 122 | ## Saga 123 | 124 | Side effects that create new commands. 125 | Receive events and return one or more commands. 126 | -------------------------------------------------------------------------------- /examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nest-cqrs-example", 3 | "version": "1.0.0", 4 | "description": "Nest CQRS module usage example", 5 | "license": "MIT", 6 | "scripts": { 7 | "postinstall": "link-module-alias", 8 | "start": "yarn start:heroes:cqrs:eventstore", 9 | "start:heroes:cqrs:eventstore": "ts-node-dev src/bin/runner.ts" 10 | }, 11 | "dependencies": { 12 | "@godaddy/terminus": "^4.4.1", 13 | "@nestjs/common": "^7.0.0", 14 | "@nestjs/core": "^7.0.0", 15 | "@nestjs/cqrs": "^7.0.1", 16 | "@nestjs/platform-express": "^6.0.0", 17 | "@nestjs/terminus": "^7.0.1", 18 | "class-transformer": "^0.4.0", 19 | "class-validator": "^0.13.1", 20 | "cli-color": "^1.4.0", 21 | "global": "^4.4.0", 22 | "nestjs-context": "^0.12.0", 23 | "nestjs-geteventstore": "^5.0.19", 24 | "nestjs-pino-stackdriver": "^2.1.0", 25 | "reflect-metadata": "^0.1.13", 26 | "rxjs": "^7.0.0" 27 | }, 28 | "devDependencies": { 29 | "@compodoc/compodoc": "^1.1.11", 30 | "@types/express": "^4.16.1", 31 | "@types/node": "^11.9.4", 32 | "@typescript-eslint/eslint-plugin": "^4.28.4", 33 | "@typescript-eslint/parser": "^4.28.4", 34 | "dot-prop": ">=4.2.1", 35 | "eslint": "^7.31.0", 36 | "eslint-plugin-import": "^2.23.4", 37 | "eslint-plugin-jest": "^24.4.0", 38 | "eslint-plugin-local": "^1.0.0", 39 | "link-module-alias": "^1.2.0", 40 | "nodemon": "^1.18.10", 41 | "prettier": "^1.12.1", 42 | "serialize-javascript": ">=3.1.0", 43 | "start-server-webpack-plugin": "^2.2.5", 44 | "ts-loader": "^7.0.4", 45 | "ts-node": "^8.0.2", 46 | "ts-node-dev": "^1.1.6", 47 | "typescript": "^4.3.4", 48 | "webpack": "^4.43.0", 49 | "webpack-node-externals": "^1.7.2" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /examples/src/all-exception.filter.ts: -------------------------------------------------------------------------------- 1 | import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common'; 2 | import { Request, Response } from 'express'; 3 | 4 | @Catch() 5 | export class AllExceptionFilter implements ExceptionFilter { 6 | catch(exception: unknown, host: ArgumentsHost): void { 7 | console.log('Main exception handler'); 8 | const ctx = host.switchToHttp(); 9 | const response = ctx.getResponse(); 10 | const request = ctx.getRequest(); 11 | 12 | const status = (exception as any).status ?? 500; 13 | const message = (exception as any).message ?? ''; 14 | 15 | response.status(status).json({ 16 | statusCode: status, 17 | timestamp: new Date().toISOString(), 18 | path: request.url, 19 | message, 20 | }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/src/bin/runner.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AllExceptionFilter } from '../all-exception.filter'; 3 | import { EventStoreHeroesModule } from '../heroes/event-store-heroes.module'; 4 | import { INestApplication } from '@nestjs/common'; 5 | 6 | async function bootstrap() { 7 | const app: INestApplication = await NestFactory.create( 8 | EventStoreHeroesModule, 9 | { 10 | logger: ['log', 'error', 'warn', 'debug', 'verbose'], 11 | }, 12 | ); 13 | 14 | app.useGlobalFilters(new AllExceptionFilter()); 15 | await app.listen(3000, () => { 16 | console.log('Application is listening on port 3000.'); 17 | }); 18 | } 19 | 20 | process.once('uncaughtException', (e: Error) => { 21 | if (e['code'] !== 'ERR_STREAM_WRITE_AFTER_END') { 22 | throw e; 23 | } 24 | }); 25 | bootstrap(); 26 | -------------------------------------------------------------------------------- /examples/src/heroes/aggregates/hero.aggregate.ts: -------------------------------------------------------------------------------- 1 | import { HeroFoundItemEvent } from '../events/impl/hero-found-item.event'; 2 | import { HeroKilledDragonEvent } from '../events/impl/hero-killed-dragon.event'; 3 | import { HeroDropItemEvent } from '../events/impl/hero-drop-item.event'; 4 | import { HeroDamagedEnemyEvent } from '../events/impl/hero-damaged-enemy.event'; 5 | import { EventStoreAggregateRoot } from '../../../../src'; 6 | 7 | export class Hero extends EventStoreAggregateRoot { 8 | constructor(private readonly id) { 9 | super(); 10 | // comment this line to test correlation-id auto-generated stream 11 | this.streamName = `hero-${id}`; 12 | } 13 | 14 | damageEnemy(dragonId: string, hitPoint: number) { 15 | return this.apply( 16 | new HeroDamagedEnemyEvent({ 17 | heroId: this.id, 18 | // comment dragonId if you want to test validation, 19 | dragonId, 20 | hitPoint: hitPoint, 21 | } as any), 22 | ); 23 | } 24 | 25 | killEnemy(dragonId: string) { 26 | // logic 27 | return this.apply( 28 | new HeroKilledDragonEvent({ 29 | heroId: this.id, 30 | dragonId, 31 | }), 32 | ); 33 | } 34 | 35 | addItem(itemId: string) { 36 | // logic 37 | return this.apply( 38 | new HeroFoundItemEvent({ 39 | heroId: this.id, 40 | itemId, 41 | }), 42 | ); 43 | } 44 | 45 | dropItem(itemId: string) { 46 | return this.apply( 47 | new HeroDropItemEvent({ 48 | heroId: this.id, 49 | itemId, 50 | }), 51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /examples/src/heroes/commands/handlers/drop-ancient-item.handler.ts: -------------------------------------------------------------------------------- 1 | import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; 2 | import * as clc from 'cli-color'; 3 | import { HeroRepository } from '../../repository/hero.repository'; 4 | import { DropAncientItemCommand } from '../impl/drop-ancient-item.command'; 5 | import { WriteEventBus } from '../../../../../src/'; 6 | 7 | @CommandHandler(DropAncientItemCommand) 8 | export class DropAncientItemHandler 9 | implements ICommandHandler 10 | { 11 | constructor( 12 | private readonly repository: HeroRepository, 13 | private readonly publisher: WriteEventBus, 14 | ) {} 15 | 16 | async execute(command: DropAncientItemCommand) { 17 | console.log(clc.yellowBright('Async DropAncientItemCommand...')); 18 | 19 | const { heroId, itemId } = command; 20 | const hero = await this.repository.findOneById(+heroId); 21 | hero.autoCommit = true; 22 | hero.maxAge = 600; // 10 min 23 | hero.addPublisher((events, context) => 24 | this.publisher.publishAll(events, context), 25 | ); 26 | await hero.addItem(itemId); 27 | await hero.dropItem(itemId); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/src/heroes/commands/handlers/index.ts: -------------------------------------------------------------------------------- 1 | import { KillDragonHandler } from './kill-dragon.handler'; 2 | import { DropAncientItemHandler } from './drop-ancient-item.handler'; 3 | 4 | export const CommandHandlers = [KillDragonHandler, DropAncientItemHandler]; 5 | -------------------------------------------------------------------------------- /examples/src/heroes/commands/handlers/kill-dragon.handler.ts: -------------------------------------------------------------------------------- 1 | import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; 2 | import * as clc from 'cli-color'; 3 | import { HeroRepository } from '../../repository/hero.repository'; 4 | import { KillDragonCommand } from '../impl/kill-dragon.command'; 5 | import { WriteEventBus } from 'nestjs-geteventstore'; 6 | import * as constants from '@eventstore/db-client/dist/constants'; 7 | import { Hero } from '../../aggregates/hero.aggregate'; 8 | 9 | @CommandHandler(KillDragonCommand) 10 | export class KillDragonHandler implements ICommandHandler { 11 | constructor( 12 | private readonly repository: HeroRepository, 13 | private readonly publisher: WriteEventBus, 14 | ) {} 15 | 16 | async execute(command: KillDragonCommand): Promise { 17 | const { heroId, dragonId } = command; 18 | 19 | console.log( 20 | clc.greenBright( 21 | `KillDragonCommand... for hero ${heroId} on enemy ${dragonId}`, 22 | ), 23 | ); 24 | // build aggregate by fetching data from database && add publisher 25 | // const hero = (await this.repository.findOneById(+heroId)).addPublisher( 26 | // this.publisher.publishAll.bind(this.publisher), 27 | // ); 28 | const hero: Hero = ( 29 | await this.repository.findOneById(+heroId) 30 | ).addPublisher(this.publisher); 31 | 32 | await hero.damageEnemy(dragonId, 2); 33 | await hero.damageEnemy(dragonId, -8); 34 | await hero.damageEnemy(dragonId, 10); 35 | await hero.damageEnemy(dragonId, 10); 36 | await hero.damageEnemy(dragonId, -1); 37 | await hero.damageEnemy(dragonId, 10); 38 | await hero.damageEnemy(dragonId, 10); 39 | await hero.damageEnemy(dragonId, 10); 40 | await hero.damageEnemy(dragonId, 10); 41 | // await hero.commit(constants.ANY); 42 | await hero.killEnemy(dragonId); 43 | await hero.commit(constants.ANY); 44 | 45 | return command; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /examples/src/heroes/commands/impl/drop-ancient-item.command.ts: -------------------------------------------------------------------------------- 1 | export class DropAncientItemCommand { 2 | constructor(public readonly heroId: string, public readonly itemId: string) {} 3 | } 4 | -------------------------------------------------------------------------------- /examples/src/heroes/commands/impl/kill-dragon.command.ts: -------------------------------------------------------------------------------- 1 | export class KillDragonCommand { 2 | constructor( 3 | public readonly heroId: string, 4 | public readonly dragonId: string, 5 | ) {} 6 | } 7 | -------------------------------------------------------------------------------- /examples/src/heroes/dto/hero-damaged-enemy.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, IsNumber, IsString } from 'class-validator'; 2 | 3 | export class HeroDamagedEnemyDto { 4 | @IsNotEmpty() 5 | @IsString() 6 | heroId: string; 7 | 8 | @IsNotEmpty() 9 | @IsString() 10 | dragonId: string; 11 | 12 | @IsNumber() 13 | hitPoint: number; 14 | } 15 | -------------------------------------------------------------------------------- /examples/src/heroes/event-store-heroes.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TerminusModule } from '@nestjs/terminus'; 3 | import { ContextModule } from 'nestjs-context'; 4 | import { LoggerModule } from 'nestjs-pino-stackdriver/dist'; 5 | import { resolve } from 'path'; 6 | 7 | import { CommandHandlers } from './commands/handlers'; 8 | import { EventHandlers } from './events/handlers'; 9 | import { heroesEvents } from './events/impl'; 10 | import { HealthController } from './health.controller'; 11 | import { HeroesGameController } from './heroes.controller'; 12 | import { QueryHandlers } from './queries/handlers'; 13 | import { HeroRepository } from './repository/hero.repository'; 14 | import { HeroesGameSagas } from './sagas/heroes.sagas'; 15 | import { 16 | CqrsEventStoreModule, 17 | EventBusConfigType, 18 | EventStoreConnectionConfig, 19 | IEventStoreSubsystems, 20 | } from 'nestjs-geteventstore'; 21 | 22 | const eventStoreConnectionConfig: EventStoreConnectionConfig = { 23 | connectionSettings: { 24 | connectionString: 25 | process.env.CONNECTION_STRING || 'esdb://localhost:2113?tls=false', 26 | }, 27 | defaultUserCredentials: { 28 | username: process.env.EVENTSTORE_CREDENTIALS_USERNAME || 'admin', 29 | password: process.env.EVENTSTORE_CREDENTIALS_PASSWORD || 'changeit', 30 | }, 31 | }; 32 | 33 | const eventStoreSubsystems: IEventStoreSubsystems = { 34 | subscriptions: { 35 | persistent: [ 36 | { 37 | // Event stream category (before the -) 38 | stream: '$ce-hero', 39 | group: 'data', 40 | settingsForCreation: { 41 | subscriptionSettings: { 42 | resolveLinkTos: true, 43 | minCheckpointCount: 1, 44 | }, 45 | }, 46 | onError: (err: Error) => 47 | console.log(`An error occurred : ${err.message}`), 48 | }, 49 | ], 50 | }, 51 | projections: [ 52 | { 53 | name: 'hero-dragon', 54 | file: resolve(`${__dirname}/projections/hero-dragon.js`), 55 | mode: 'continuous', 56 | enabled: true, 57 | checkPointsEnabled: true, 58 | emitEnabled: true, 59 | }, 60 | ], 61 | onConnectionFail: (err: Error) => 62 | console.log(`Connection to Event store hooked : ${err}`), 63 | }; 64 | 65 | const eventBusConfig: EventBusConfigType = { 66 | read: { 67 | allowedEvents: { ...heroesEvents }, 68 | }, 69 | write: { 70 | serviceName: 'test', 71 | }, 72 | }; 73 | 74 | @Module({ 75 | controllers: [HealthController, HeroesGameController], 76 | providers: [ 77 | HeroRepository, 78 | ...CommandHandlers, 79 | ...EventHandlers, 80 | ...QueryHandlers, 81 | HeroesGameSagas, 82 | ], 83 | imports: [ 84 | ContextModule.register(), 85 | TerminusModule, 86 | LoggerModule.forRoot(), 87 | CqrsEventStoreModule.register( 88 | eventStoreConnectionConfig, 89 | eventStoreSubsystems, 90 | eventBusConfig, 91 | ), 92 | ], 93 | }) 94 | export class EventStoreHeroesModule {} 95 | -------------------------------------------------------------------------------- /examples/src/heroes/events/handlers/hero-found-item.handler.ts: -------------------------------------------------------------------------------- 1 | import { EventsHandler, IEventHandler } from '@nestjs/cqrs'; 2 | import * as clc from 'cli-color'; 3 | import { HeroFoundItemEvent } from '../impl/hero-found-item.event'; 4 | 5 | @EventsHandler(HeroFoundItemEvent) 6 | export class HeroFoundItemHandler implements IEventHandler { 7 | handle(event: HeroFoundItemEvent) { 8 | console.log(clc.yellowBright('Async HeroFoundItemEventHandler...')); 9 | //throw new Error(`Error handling ${event.constructor.name}`); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/src/heroes/events/handlers/hero-killed-dragon.handler.ts: -------------------------------------------------------------------------------- 1 | import { IEventHandler } from '@nestjs/cqrs'; 2 | import { EventsHandler } from '@nestjs/cqrs/dist/decorators/events-handler.decorator'; 3 | import * as clc from 'cli-color'; 4 | import { HeroKilledDragonEvent } from '../impl/hero-killed-dragon.event'; 5 | 6 | @EventsHandler(HeroKilledDragonEvent) 7 | export class HeroKilledDragonHandler 8 | implements IEventHandler { 9 | async handle(event: HeroKilledDragonEvent) { 10 | console.log(clc.greenBright('HeroKilledDragonEventHandler...')); 11 | await event.ack(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/src/heroes/events/handlers/index.ts: -------------------------------------------------------------------------------- 1 | import { HeroKilledDragonHandler } from './hero-killed-dragon.handler'; 2 | import { HeroFoundItemHandler } from './hero-found-item.handler'; 3 | 4 | export const EventHandlers = [HeroKilledDragonHandler, HeroFoundItemHandler]; 5 | -------------------------------------------------------------------------------- /examples/src/heroes/events/impl/hero-damaged-enemy.event.ts: -------------------------------------------------------------------------------- 1 | import { ValidateNested } from 'class-validator'; 2 | import { Type } from 'class-transformer'; 3 | import { EventStoreEvent } from '../../../../../src'; 4 | import { HeroDamagedEnemyDto } from '../../dto/hero-damaged-enemy.dto'; 5 | 6 | export class HeroDamagedEnemyEvent extends EventStoreEvent { 7 | @ValidateNested() 8 | @Type(() => HeroDamagedEnemyDto) 9 | public declare data: HeroDamagedEnemyDto; 10 | } 11 | -------------------------------------------------------------------------------- /examples/src/heroes/events/impl/hero-drop-item.event.ts: -------------------------------------------------------------------------------- 1 | import { EventStoreEvent } from '../../../../../src'; 2 | 3 | export class HeroDropItemEvent extends EventStoreEvent { 4 | constructor( 5 | public readonly data: { 6 | heroId: string; 7 | itemId: string; 8 | }, 9 | options?, 10 | ) { 11 | super(data, options); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/src/heroes/events/impl/hero-found-item.event.ts: -------------------------------------------------------------------------------- 1 | import { EventStoreEvent } from '../../../../../src'; 2 | 3 | export class HeroFoundItemEvent extends EventStoreEvent { 4 | constructor( 5 | public readonly data: { 6 | heroId: string; 7 | itemId: string; 8 | }, 9 | options?, 10 | ) { 11 | super(data, options); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/src/heroes/events/impl/hero-killed-dragon.event.ts: -------------------------------------------------------------------------------- 1 | import { 2 | EventStoreAcknowledgeableEvent, 3 | EventVersion, 4 | } from 'nestjs-geteventstore'; 5 | 6 | // This is the second version of this event 7 | @EventVersion(2) 8 | export class HeroKilledDragonEvent extends EventStoreAcknowledgeableEvent { 9 | public declare readonly data: { 10 | heroId: string; 11 | dragonId: string; 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /examples/src/heroes/events/impl/index.ts: -------------------------------------------------------------------------------- 1 | import { HeroFoundItemEvent } from './hero-found-item.event'; 2 | import { HeroKilledDragonEvent } from './hero-killed-dragon.event'; 3 | import { HeroDropItemEvent } from './hero-drop-item.event'; 4 | import { HeroDamagedEnemyEvent } from './hero-damaged-enemy.event'; 5 | 6 | export const heroesEvents = { 7 | HeroDamagedEnemyEvent, 8 | HeroKilledDragonEvent, 9 | HeroFoundItemEvent, 10 | HeroDropItemEvent, 11 | }; 12 | -------------------------------------------------------------------------------- /examples/src/heroes/health.controller.ts: -------------------------------------------------------------------------------- 1 | import { HealthCheck, HealthIndicatorResult } from '@nestjs/terminus'; 2 | import { Controller, Get } from '@nestjs/common'; 3 | import { EventStoreHealthIndicator } from '../../../src'; 4 | 5 | @Controller('health') 6 | export class HealthController { 7 | constructor( 8 | private readonly eventStoreHealthIndicator: EventStoreHealthIndicator, 9 | ) {} 10 | 11 | @Get() 12 | @HealthCheck() 13 | public async healthCheck(): Promise { 14 | return this.eventStoreHealthIndicator.check(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/src/heroes/heroes.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Get, Param, Put } from '@nestjs/common'; 2 | import { CommandBus, QueryBus } from '@nestjs/cqrs'; 3 | import { KillDragonCommand } from './commands/impl/kill-dragon.command'; 4 | import { KillDragonDto } from './interfaces/kill-dragon-dto.interface'; 5 | import { Hero } from './aggregates/hero.aggregate'; 6 | import { GetHeroesQuery } from './queries/impl'; 7 | 8 | @Controller('hero') 9 | export class HeroesGameController { 10 | constructor( 11 | private readonly commandBus: CommandBus, 12 | private readonly queryBus: QueryBus, 13 | ) {} 14 | 15 | @Put(':id/kill') 16 | async killDragon(@Param('id') id: string, @Body() dto: KillDragonDto) { 17 | return this.commandBus 18 | .execute(new KillDragonCommand(id, dto.dragonId)) 19 | .catch((e) => console.log('e : ', e)); 20 | } 21 | 22 | @Get() 23 | async findAll(): Promise { 24 | return this.queryBus.execute(new GetHeroesQuery()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/src/heroes/heroes.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { CqrsModule } from '@nestjs/cqrs'; 3 | import { CommandHandlers } from './commands/handlers'; 4 | import { EventHandlers } from './events/handlers'; 5 | import { HeroesGameController } from './heroes.controller'; 6 | import { QueryHandlers } from './queries/handlers'; 7 | import { HeroRepository } from './repository/hero.repository'; 8 | import { HeroesGameSagas } from './sagas/heroes.sagas'; 9 | 10 | @Module({ 11 | imports: [CqrsModule], 12 | controllers: [HeroesGameController], 13 | providers: [ 14 | HeroRepository, 15 | ...CommandHandlers, 16 | ...EventHandlers, 17 | ...QueryHandlers, 18 | HeroesGameSagas, 19 | ], 20 | }) 21 | export class HeroesGameModule {} 22 | -------------------------------------------------------------------------------- /examples/src/heroes/interfaces/kill-dragon-dto.interface.ts: -------------------------------------------------------------------------------- 1 | export interface KillDragonDto { 2 | dragonId: string; 3 | } 4 | -------------------------------------------------------------------------------- /examples/src/heroes/projections/hero-dragon.js: -------------------------------------------------------------------------------- 1 | fromCategory('$et-hero') 2 | .partitionBy((ev) => ev.data.dragonId) 3 | .when({ 4 | HeroKilledDragonEvent: (state, event) => { 5 | emit(`dragon-${event.data.dragonId}`, 'KilledEvent', event.data, { 6 | specversion: event.metadata.specversion, 7 | type: event.metadata.type.replace( 8 | 'HeroKilledDragonEvent', 9 | 'KilledEvent', 10 | ), 11 | source: event.metadata.source, 12 | correlation_id: event.metadata.correlation_id, 13 | time: event.metadata.time, 14 | version: 1, 15 | }); 16 | return state; 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /examples/src/heroes/queries/handlers/get-heroes.handler.ts: -------------------------------------------------------------------------------- 1 | import { IQueryHandler, QueryHandler } from '@nestjs/cqrs'; 2 | import * as clc from 'cli-color'; 3 | import { HeroRepository } from '../../repository/hero.repository'; 4 | import { GetHeroesQuery } from '../impl'; 5 | 6 | @QueryHandler(GetHeroesQuery) 7 | export class GetHeroesHandler implements IQueryHandler { 8 | constructor(private readonly repository: HeroRepository) {} 9 | 10 | async execute(query: GetHeroesQuery) { 11 | console.log(clc.yellowBright('Async GetHeroesQuery...')); 12 | return this.repository.findAll(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/src/heroes/queries/handlers/index.ts: -------------------------------------------------------------------------------- 1 | import { GetHeroesHandler } from './get-heroes.handler'; 2 | 3 | export const QueryHandlers = [GetHeroesHandler]; 4 | -------------------------------------------------------------------------------- /examples/src/heroes/queries/impl/get-heroes.query.ts: -------------------------------------------------------------------------------- 1 | export class GetHeroesQuery {} 2 | -------------------------------------------------------------------------------- /examples/src/heroes/queries/impl/index.ts: -------------------------------------------------------------------------------- 1 | export * from './get-heroes.query'; 2 | -------------------------------------------------------------------------------- /examples/src/heroes/repository/fixtures/user.ts: -------------------------------------------------------------------------------- 1 | import { Hero } from '../../aggregates/hero.aggregate'; 2 | 3 | export const userHero = new Hero('greg'); 4 | -------------------------------------------------------------------------------- /examples/src/heroes/repository/hero.repository.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { Hero } from '../aggregates/hero.aggregate'; 3 | import { userHero } from './fixtures/user'; 4 | 5 | @Injectable() 6 | export class HeroRepository { 7 | async findOneById(id: number): Promise { 8 | return new Hero(id); 9 | } 10 | 11 | async findAll(): Promise { 12 | return [userHero]; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/src/heroes/sagas/heroes.sagas.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { ICommand, Saga } from '@nestjs/cqrs'; 3 | import * as clc from 'cli-color'; 4 | import { v4 } from 'uuid'; 5 | import { Context, CONTEXT_CORRELATION_ID } from 'nestjs-context'; 6 | import { Observable } from 'rxjs'; 7 | import { delay, filter, map } from 'rxjs/operators'; 8 | import { DropAncientItemCommand } from '../commands/impl/drop-ancient-item.command'; 9 | import { HeroKilledDragonEvent } from '../events/impl/hero-killed-dragon.event'; 10 | 11 | const itemId = '0'; 12 | 13 | @Injectable() 14 | export class HeroesGameSagas { 15 | constructor(private readonly context: Context) {} 16 | @Saga() 17 | dragonKilled = (events$: Observable): Observable => { 18 | return events$.pipe( 19 | //@ts-ignore 20 | filter((ev) => ev instanceof HeroKilledDragonEvent), 21 | //@ts-ignore 22 | delay(400), 23 | //@ts-ignore 24 | map((event: HeroKilledDragonEvent) => { 25 | this.context.setCachedValue( 26 | CONTEXT_CORRELATION_ID, 27 | event?.metadata?.correlation_id || v4(), 28 | ); 29 | console.log( 30 | clc.redBright('Inside [HeroesGameSagas] Saga after a little sleep'), 31 | ); 32 | console.log(event); 33 | return new DropAncientItemCommand(event.data.heroId, itemId); 34 | }), 35 | ); 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /examples/src/heroes/write.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Param, Put } from '@nestjs/common'; 2 | import { CommandBus } from '@nestjs/cqrs'; 3 | import { KillDragonCommand } from './commands/impl/kill-dragon.command'; 4 | import { KillDragonDto } from './interfaces/kill-dragon-dto.interface'; 5 | 6 | @Controller('hero') 7 | export class WriteController { 8 | constructor(private readonly commandBus: CommandBus) {} 9 | 10 | @Put(':id/kill') 11 | async killDragon(@Param('id') id: string, @Body() dto: KillDragonDto) { 12 | return this.commandBus.execute(new KillDragonCommand(id, dto.dragonId)); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /examples/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "module": "commonjs", 5 | "declaration": false, 6 | "noImplicitAny": false, 7 | "removeComments": true, 8 | "noLib": false, 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "strictPropertyInitialization": false, 12 | "useDefineForClassFields": true, 13 | "target": "ES2020", 14 | "sourceMap": true, 15 | "allowJs": true, 16 | "outDir": "./dist", 17 | "composite": false 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/webpack-hmr.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const nodeExternals = require('webpack-node-externals'); 3 | const StartServerPlugin = require('start-server-webpack-plugin'); 4 | 5 | module.exports = function(options) { 6 | return { 7 | ...options, 8 | entry: ['webpack/hot/poll?100', options.entry], 9 | watch: true, 10 | externals: [ 11 | nodeExternals({ 12 | whitelist: ['webpack/hot/poll?100'], 13 | }), 14 | ], 15 | plugins: [ 16 | ...options.plugins, 17 | new webpack.HotModuleReplacementPlugin(), 18 | new webpack.WatchIgnorePlugin([/\.js$/, /\.d\.ts$/]), 19 | new StartServerPlugin({ name: options.output.filename }), 20 | ], 21 | }; 22 | }; -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './dist'; 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | function __export(m) { 3 | for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; 4 | } 5 | exports.__esModule = true; 6 | __export(require("./dist")); 7 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | export * from './dist'; 2 | -------------------------------------------------------------------------------- /jest.setup.ts: -------------------------------------------------------------------------------- 1 | export default (): void => { 2 | console.log = () => { 3 | // do nothing 4 | }; 5 | console.debug = () => { 6 | // do nothing 7 | }; 8 | console.error = () => { 9 | // do nothing 10 | }; 11 | }; 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nestjs-geteventstore", 3 | "version": "5.0.19", 4 | "description": "Event Store connector for NestJS-Cqrs", 5 | "author": "Vincent Vermersch ", 6 | "contributors": [ 7 | "vinceveve", 8 | "jdharandas", 9 | "monocursive", 10 | "xGouley", 11 | "jokesterfr", 12 | "MaxencePerrinPrestashop", 13 | "prxmat", 14 | "maniolias" 15 | ], 16 | "license": "MIT", 17 | "readmeFilename": "README.md", 18 | "main": "dist/index.js", 19 | "scripts": { 20 | "start:dev": "tsc -w", 21 | "build": "tsc", 22 | "prepare": "npm run build && husky install", 23 | "format": "prettier --write \"src/**/*.ts\"", 24 | "lint": "eslint .", 25 | "check-lite": "npm run lint:fix && npm run prepare", 26 | "test": "jest", 27 | "semantic-release": "semantic-release", 28 | "test:watch": "jest --watch", 29 | "test:cov": "jest --coverage", 30 | "test:e2e": "jest --config ./test/jest-e2e.json" 31 | }, 32 | "keywords": [ 33 | "nestjs", 34 | "eventstore" 35 | ], 36 | "repository": "git@github.com:PrestaShopCorp/nestjs-eventstore.git", 37 | "publishConfig": { 38 | "access": "public" 39 | }, 40 | "bugs": "https://github.com/prestashopCorp/nestjs-eventstore/issues", 41 | "peerDependencies": { 42 | "@nestjs/common": "*", 43 | "@nestjs/core": "*", 44 | "@nestjs/cqrs": "*", 45 | "@nestjs/terminus": "*", 46 | "class-transformer": "*", 47 | "class-validator": "*", 48 | "nestjs-context": "^0.12.0", 49 | "reflect-metadata": "^0.1.13", 50 | "rimraf": "^3.0.2", 51 | "rxjs": "^6.6.3" 52 | }, 53 | "peerDependenciesMeta": { 54 | "@nesjs/cqrs": { 55 | "optional": true 56 | } 57 | }, 58 | "dependencies": { 59 | "@eventstore/db-client": "^2.1.0", 60 | "lodash": "^4.17.20", 61 | "uuid": "^8.3.2" 62 | }, 63 | "devDependencies": { 64 | "@nestjs/common": "^7.6.15", 65 | "@nestjs/core": "^7.6.15", 66 | "@nestjs/cqrs": "^7.0.1", 67 | "@nestjs/platform-express": "^7.6.11", 68 | "@nestjs/terminus": "^7.1.2", 69 | "@nestjs/testing": "^7.6.11", 70 | "@types/jest": "^26.0.20", 71 | "@types/node": "^14.14.25", 72 | "@types/supertest": "^2.0.10", 73 | "@typescript-eslint/eslint-plugin": "^4.28.4", 74 | "@typescript-eslint/parser": "^4.28.4", 75 | "class-transformer": "^0.4.0", 76 | "class-validator": "^0.13.1", 77 | "eslint": "^7.31.0", 78 | "eslint-plugin-import": "^2.23.4", 79 | "eslint-plugin-jest": "^24.4.0", 80 | "eslint-plugin-local": "^1.0.0", 81 | "husky": "^7.0.1", 82 | "jest": "^26.6.3", 83 | "lint-staged": "^11.1.1", 84 | "nestjs-context": "^0.11.0", 85 | "prettier": "^2.2.1", 86 | "pretty-quick": "^3.1.1", 87 | "reflect-metadata": "^0.1.13", 88 | "rxjs": "^7.0.0", 89 | "supertest": "6.1.3", 90 | "ts-jest": "^26.5.0", 91 | "ts-node": "^9.1.1", 92 | "tsc-watch": "^4.2.9", 93 | "tsconfig-paths": "^3.9.0", 94 | "typescript": "^4.3.4" 95 | }, 96 | "jest": { 97 | "moduleFileExtensions": [ 98 | "js", 99 | "json", 100 | "ts" 101 | ], 102 | "rootDir": "./", 103 | "testRegex": ".spec.ts$", 104 | "transform": { 105 | "^.+\\.(t|j)s$": "ts-jest" 106 | }, 107 | "globals": { 108 | "ts-jest": { 109 | "tsconfig": "./tsconfig.spec.json" 110 | } 111 | }, 112 | "globalSetup": "./jest.setup.ts", 113 | "coverageDirectory": "../coverage", 114 | "testEnvironment": "node" 115 | }, 116 | "lint-staged": { 117 | "./src/**/*.{ts}": [ 118 | "eslint . --fix", 119 | "git add" 120 | ], 121 | "examples/src/**/*.{ts}": [ 122 | "eslint . --fix", 123 | "git add" 124 | ] 125 | }, 126 | "husky": { 127 | "hooks": { 128 | "pre-commit": "yarn lint-staged && yarn pretty-quick --staged && yarn jest --forceExit" 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/cloudevents/index.ts: -------------------------------------------------------------------------------- 1 | export * from './write-events-prepublish.service'; 2 | -------------------------------------------------------------------------------- /src/cloudevents/write-events-prepublish.service.ts: -------------------------------------------------------------------------------- 1 | import { Logger, Injectable, Inject } from '@nestjs/common'; 2 | import { plainToClass } from 'class-transformer'; 3 | import { validate } from 'class-validator'; 4 | import { 5 | Context, 6 | CONTEXT_BIN, 7 | CONTEXT_CORRELATION_ID, 8 | CONTEXT_HOSTNAME, 9 | CONTEXT_PATH, 10 | } from 'nestjs-context'; 11 | import { 12 | IBaseEvent, 13 | IEventBusPrepublishPrepareProvider, 14 | IEventBusPrepublishValidateProvider, 15 | IWriteEventBusConfig, 16 | } from '../interfaces'; 17 | import { EventStoreEvent } from '../event-store/events'; 18 | import { EventMetadataDto } from '../dto'; 19 | import { WRITE_EVENT_BUS_CONFIG } from '../constants'; 20 | import { createEventDefaultMetadata } from '../tools/create-event-default-metadata'; 21 | import { isIPv4 } from 'net'; 22 | 23 | @Injectable() 24 | export class WriteEventsPrepublishService< 25 | T extends IBaseEvent = EventStoreEvent, 26 | > implements 27 | IEventBusPrepublishValidateProvider, 28 | IEventBusPrepublishPrepareProvider 29 | { 30 | private readonly logger = new Logger(this.constructor.name); 31 | constructor( 32 | private readonly context: Context, 33 | @Inject(WRITE_EVENT_BUS_CONFIG) 34 | private readonly config: IWriteEventBusConfig, 35 | ) {} 36 | // errors log 37 | async onValidationFail(events: T[], errors: any[]) { 38 | for (const error of errors) { 39 | this.logger.error(error); 40 | } 41 | } 42 | 43 | // transform to dto each event and validate it 44 | async validate(events: T[]) { 45 | let errors = []; 46 | for (const event of events) { 47 | this.logger.debug(`Validating ${event.constructor.name}`); 48 | // @todo JDM class-transformer is not converting data property ! 49 | // (metadata is working, so it might be related to inheritance) 50 | const validateEvent: any = plainToClass(event.constructor as any, event); 51 | errors = [...errors, ...(await validate(validateEvent))]; 52 | } 53 | return errors; 54 | } 55 | 56 | private getCloudEventMetadata(event: T): EventMetadataDto { 57 | try { 58 | const { version: defaultVersion, time } = createEventDefaultMetadata(); 59 | const version = event?.metadata?.version ?? defaultVersion; 60 | const hostnameRaw = this.context.get(CONTEXT_HOSTNAME); 61 | const hostname = isIPv4(hostnameRaw) 62 | ? `${hostnameRaw.split(/[.]/).join('-')}.ip` 63 | : hostnameRaw; 64 | const hostnameArr = hostname.split('.'); 65 | const eventType = `${hostnameArr[1] ? hostnameArr[1] + '.' : ''}${ 66 | hostnameArr[0] 67 | }.${this.config.serviceName ?? this.context.get(CONTEXT_BIN)}.${ 68 | event.eventType 69 | }.${version}`; 70 | const source = `${hostname}${this.context.get(CONTEXT_PATH)}`; 71 | return { 72 | specversion: 1, 73 | time, 74 | version, 75 | correlation_id: this.context.get(CONTEXT_CORRELATION_ID), 76 | type: eventType, 77 | source, 78 | created_at: new Date().toISOString(), 79 | }; 80 | } catch (e) { 81 | this.logger.error(e); 82 | throw e; 83 | } 84 | } 85 | 86 | // add cloud events metadata 87 | async prepare(events: T[]) { 88 | const preparedEvents = []; 89 | for (const event of events) { 90 | this.logger.debug(`Preparing ${event.constructor.name}`); 91 | const preparedEvent = event; 92 | preparedEvent.metadata = { 93 | ...this.getCloudEventMetadata(event), 94 | ...(event.metadata ?? {}), 95 | }; 96 | preparedEvents.push(preparedEvent); 97 | } 98 | return preparedEvents; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const WRITE_EVENT_BUS_CONFIG = Symbol(); 2 | export const READ_EVENT_BUS_CONFIG = Symbol(); 3 | export const EVENT_STORE_SUBSYSTEMS = Symbol(); 4 | -------------------------------------------------------------------------------- /src/cqrs-event-store.module.ts: -------------------------------------------------------------------------------- 1 | import { CommandBus, CqrsModule, EventBus, QueryBus } from '@nestjs/cqrs'; 2 | import { DynamicModule, Module } from '@nestjs/common'; 3 | 4 | import { EventStoreModule } from './event-store/event-store.module'; 5 | import { 6 | EventBusConfigType, 7 | IWriteEventBusConfig, 8 | ReadEventBusConfigType, 9 | } from './interfaces'; 10 | import { ReadEventBus, WriteEventBus } from './cqrs'; 11 | import { READ_EVENT_BUS_CONFIG, WRITE_EVENT_BUS_CONFIG } from './constants'; 12 | import { EventBusPrepublishService } from './cqrs/event-bus-prepublish.service'; 13 | import { WriteEventsPrepublishService } from './cloudevents'; 14 | import { ContextName } from 'nestjs-context'; 15 | import { IEventStoreSubsystems } from './event-store/config'; 16 | import { EventStoreConnectionConfig } from './event-store/config/event-store-connection-config'; 17 | import { ExplorerService } from '@nestjs/cqrs/dist/services/explorer.service'; 18 | import { IPersistentSubscriptionConfig } from './event-store'; 19 | 20 | const getDefaultEventBusConfiguration: IWriteEventBusConfig = { 21 | context: ContextName.HTTP, 22 | validate: WriteEventsPrepublishService, 23 | prepare: WriteEventsPrepublishService, 24 | }; 25 | 26 | @Module({ 27 | providers: [ 28 | WriteEventsPrepublishService, 29 | EventBusPrepublishService, 30 | ExplorerService, 31 | WriteEventBus, 32 | ReadEventBus, 33 | CommandBus, 34 | QueryBus, 35 | { provide: EventBus, useExisting: ReadEventBus }, 36 | ], 37 | exports: [ 38 | WriteEventsPrepublishService, 39 | EventBusPrepublishService, 40 | ExplorerService, 41 | WriteEventBus, 42 | ReadEventBus, 43 | CommandBus, 44 | QueryBus, 45 | EventBus, 46 | ], 47 | }) 48 | export class CqrsEventStoreModule extends CqrsModule { 49 | static register( 50 | eventStoreConfig: EventStoreConnectionConfig, 51 | eventStoreSubsystems: IEventStoreSubsystems = { 52 | onConnectionFail: (e) => console.log('e : ', e), 53 | }, 54 | eventBusConfig: EventBusConfigType = {}, 55 | ): DynamicModule { 56 | return { 57 | module: CqrsEventStoreModule, 58 | imports: [ 59 | EventStoreModule.register(eventStoreConfig, eventStoreSubsystems), 60 | ], 61 | providers: [ 62 | { provide: READ_EVENT_BUS_CONFIG, useValue: eventBusConfig.read }, 63 | { 64 | provide: WRITE_EVENT_BUS_CONFIG, 65 | useValue: { ...getDefaultEventBusConfiguration, ...eventBusConfig }, 66 | }, 67 | ], 68 | exports: [EventStoreModule], 69 | }; 70 | } 71 | 72 | static registerReadBus( 73 | eventStoreConfig: EventStoreConnectionConfig, 74 | eventBusConfig: ReadEventBusConfigType, 75 | subscriptions: IPersistentSubscriptionConfig[] = [], 76 | ): DynamicModule { 77 | return { 78 | module: CqrsEventStoreModule, 79 | imports: [ 80 | EventStoreModule.register(eventStoreConfig, { 81 | subscriptions: { persistent: subscriptions }, 82 | onConnectionFail: (e) => console.log('e : ', e), 83 | }), 84 | ], 85 | providers: [ 86 | { provide: READ_EVENT_BUS_CONFIG, useValue: eventBusConfig }, 87 | { provide: EventBus, useExisting: ReadEventBus }, 88 | ], 89 | exports: [EventStoreModule], 90 | }; 91 | } 92 | 93 | static registerWriteBus( 94 | eventStoreConfig: EventStoreConnectionConfig, 95 | eventBusConfig: IWriteEventBusConfig = {}, 96 | ): DynamicModule { 97 | return { 98 | module: CqrsEventStoreModule, 99 | imports: [EventStoreModule.register(eventStoreConfig)], 100 | providers: [ 101 | { 102 | provide: WRITE_EVENT_BUS_CONFIG, 103 | useValue: { ...getDefaultEventBusConfiguration, ...eventBusConfig }, 104 | }, 105 | ], 106 | exports: [EventStoreModule], 107 | }; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/cqrs/aggregate-root.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from '@nestjs/common'; 2 | import { IEvent } from '@nestjs/cqrs'; 3 | import { InvalidPublisherException } from '../exceptions/invalid-publisher.exception'; 4 | 5 | const INTERNAL_EVENTS = Symbol(); 6 | const IS_AUTO_COMMIT_ENABLED = Symbol(); 7 | 8 | export abstract class AggregateRoot { 9 | protected logger = new Logger(this.constructor.name); 10 | public [IS_AUTO_COMMIT_ENABLED] = false; 11 | private readonly [INTERNAL_EVENTS]: EventBase[] = []; 12 | private readonly _publishers: Function[] = []; 13 | 14 | set autoCommit(value: boolean) { 15 | this[IS_AUTO_COMMIT_ENABLED] = value; 16 | } 17 | 18 | get autoCommit(): boolean { 19 | return this[IS_AUTO_COMMIT_ENABLED]; 20 | } 21 | 22 | addPublisher( 23 | publisher: T, 24 | method: keyof T = 'publishAll' as keyof T, 25 | ) { 26 | const objectPublisher = publisher?.[method]; 27 | const addedPublisher = 28 | !!objectPublisher && typeof objectPublisher === 'function' 29 | ? objectPublisher.bind(publisher) 30 | : publisher; 31 | if (typeof addedPublisher === 'function') { 32 | this._publishers.push(addedPublisher); 33 | return this; 34 | } 35 | throw new InvalidPublisherException(publisher, method); 36 | } 37 | 38 | get publishers() { 39 | return this._publishers; 40 | } 41 | 42 | protected addEvent(event: T) { 43 | this[INTERNAL_EVENTS].push(event); 44 | return this; 45 | } 46 | 47 | protected clearEvents() { 48 | this[INTERNAL_EVENTS].length = 0; 49 | return this; 50 | } 51 | 52 | async commit() { 53 | this.logger.debug( 54 | `Aggregate will commit ${this.getUncommittedEvents().length} in ${ 55 | this.publishers.length 56 | } publishers`, 57 | ); 58 | 59 | // flush the queue first to avoid multiple commit of the same event on concurrent calls 60 | const events = this.getUncommittedEvents(); 61 | this.clearEvents(); 62 | 63 | // publish the event 64 | for (const publisher of this.publishers) { 65 | await publisher(events).catch((error) => { 66 | this[INTERNAL_EVENTS].unshift(...events); 67 | throw error; 68 | }); 69 | } 70 | return this; 71 | } 72 | 73 | uncommit() { 74 | this.clearEvents(); 75 | return this; 76 | } 77 | 78 | getUncommittedEvents(): EventBase[] { 79 | return this[INTERNAL_EVENTS]; 80 | } 81 | 82 | loadFromHistory(history: EventBase[]) { 83 | history.forEach((event) => this.apply(event, true)); 84 | } 85 | 86 | async apply( 87 | event: T, 88 | isFromHistory = false, 89 | ) { 90 | this.logger.debug( 91 | `Applying ${event.constructor.name} with${ 92 | this.autoCommit ? '' : 'out' 93 | } autocommit`, 94 | ); 95 | if (!isFromHistory) { 96 | this.addEvent(event); 97 | } 98 | // eslint-disable-next-line no-unused-expressions 99 | this.autoCommit && (await this.commit()); 100 | const handler = this.getEventHandler(event); 101 | // eslint-disable-next-line no-unused-expressions 102 | handler && (await handler.call(this, event)); 103 | } 104 | 105 | private getEventHandler( 106 | event: T, 107 | ): Function | undefined { 108 | const handler = `on${AggregateRoot.getEventName(event)}`; 109 | return this[handler]; 110 | } 111 | 112 | private static getEventName(event: any): string { 113 | const { constructor } = Object.getPrototypeOf(event); 114 | return constructor.name as string; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/cqrs/default-event-mapper.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from '@nestjs/common'; 2 | import { ReadEventOptionsType, ReadEventBusConfigType } from '../interfaces'; 3 | 4 | export const defaultEventMapper = ( 5 | allEvents: ReadEventBusConfigType['allowedEvents'], 6 | ) => { 7 | const logger = new Logger('Default Event Mapper'); 8 | return ((data, options: ReadEventOptionsType) => { 9 | const className = `${options.eventType}`; 10 | if (allEvents[className]) { 11 | logger.log( 12 | `Build ${className} received from stream ${options.eventStreamId} with id ${options.eventId} and number ${options.eventNumber}`, 13 | ); 14 | return new allEvents[className](data, options); 15 | } 16 | return null; 17 | }) as ReadEventBusConfigType['eventMapper']; 18 | }; 19 | -------------------------------------------------------------------------------- /src/cqrs/event-bus-prepublish.service.ts: -------------------------------------------------------------------------------- 1 | import { ModuleRef } from '@nestjs/core'; 2 | import { Injectable } from '@nestjs/common'; 3 | import { 4 | EventBusPrepublishPrepareCallbackType, 5 | IBaseEvent, 6 | IEventBusPrepublishConfig, 7 | IEventBusPrepublishPrepareProvider, 8 | IEventBusPrepublishValidateProvider, 9 | } from '../interfaces'; 10 | 11 | @Injectable() 12 | export class EventBusPrepublishService< 13 | EventBase extends IBaseEvent = IBaseEvent, 14 | > { 15 | constructor(private readonly moduleRef: ModuleRef) {} 16 | 17 | private async getProvider< 18 | T = 19 | | IEventBusPrepublishPrepareProvider 20 | | IEventBusPrepublishValidateProvider, 21 | >(name): Promise { 22 | try { 23 | return await this.moduleRef.resolve(name); 24 | } catch (e) { 25 | return undefined; 26 | } 27 | } 28 | 29 | async validate( 30 | config: IEventBusPrepublishConfig, 31 | events: T[], 32 | ): Promise { 33 | const { validate } = config; 34 | if (!validate) { 35 | return []; 36 | } 37 | const validator = 38 | (await this.getProvider>( 39 | validate, 40 | )) ?? (validate as IEventBusPrepublishValidateProvider); 41 | const validated = await validator.validate(events); 42 | // validation passed without errors 43 | if (!validated.length) { 44 | return []; 45 | } 46 | // validation failed 47 | if (validator.onValidationFail) { 48 | await validator.onValidationFail(events, validated); 49 | } 50 | return validated; 51 | } 52 | 53 | async prepare( 54 | config: IEventBusPrepublishConfig, 55 | events: T[], 56 | ): Promise { 57 | const { prepare } = config; 58 | if (!prepare) { 59 | return events; 60 | } 61 | const provider = await this.getProvider< 62 | IEventBusPrepublishPrepareProvider 63 | >(prepare); 64 | return provider 65 | ? provider.prepare(events) 66 | : (prepare as EventBusPrepublishPrepareCallbackType)(events); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/cqrs/index.ts: -------------------------------------------------------------------------------- 1 | export * from './aggregate-root'; 2 | export * from './default-event-mapper'; 3 | export * from './event-bus-prepublish.service'; 4 | export * from './read-event-bus'; 5 | export * from './write-event-bus'; 6 | -------------------------------------------------------------------------------- /src/cqrs/read-event-bus.ts: -------------------------------------------------------------------------------- 1 | import { CommandBus, EventBus as Parent } from '@nestjs/cqrs'; 2 | import { Injectable, Logger } from '@nestjs/common'; 3 | import { 4 | ReadEventOptionsType, 5 | IReadEvent, 6 | ReadEventBusConfigType, 7 | } from '../interfaces'; 8 | import { defaultEventMapper } from './default-event-mapper'; 9 | import { Inject } from '@nestjs/common'; 10 | import { READ_EVENT_BUS_CONFIG } from '../constants'; 11 | import { ModuleRef } from '@nestjs/core'; 12 | import { EventBusPrepublishService } from './event-bus-prepublish.service'; 13 | 14 | @Injectable() 15 | export class ReadEventBus< 16 | EventBase extends IReadEvent = IReadEvent 17 | > extends Parent { 18 | private logger = new Logger(this.constructor.name); 19 | constructor( 20 | @Inject(READ_EVENT_BUS_CONFIG) 21 | private readonly config: ReadEventBusConfigType, 22 | private readonly prepublish: EventBusPrepublishService, 23 | commandBus: CommandBus, 24 | moduleRef: ModuleRef, 25 | ) { 26 | super(commandBus, moduleRef); 27 | this.logger.debug('Registering Read EventBus for EventStore...'); 28 | } 29 | async publish(event: T) { 30 | this.logger.debug('Publish in read bus'); 31 | const preparedEvents = await this.prepublish.prepare(this.config, [event]); 32 | if (!(await this.prepublish.validate(this.config, preparedEvents))) { 33 | return; 34 | } 35 | return super.publish(preparedEvents[0]); 36 | } 37 | async publishAll(events: T[]) { 38 | this.logger.debug('Publish all in read bus'); 39 | const preparedEvents = await this.prepublish.prepare(this.config, events); 40 | if (!(await this.prepublish.validate(this.config, preparedEvents))) { 41 | return; 42 | } 43 | return super.publishAll(preparedEvents); 44 | } 45 | map(data: any, options: ReadEventOptionsType): T { 46 | const eventMapper = 47 | this.config.eventMapper || defaultEventMapper(this.config.allowedEvents); 48 | return eventMapper(data, options) as T; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/cqrs/write-event-bus.ts: -------------------------------------------------------------------------------- 1 | import { CommandBus, EventBus as Parent } from '@nestjs/cqrs'; 2 | import { Inject, Injectable, Logger } from '@nestjs/common'; 3 | import { EventStorePublisher } from '../event-store'; 4 | import { 5 | IWriteEvent, 6 | IWriteEventBusConfig, 7 | PublicationContextInterface, 8 | } from '../interfaces'; 9 | import { WRITE_EVENT_BUS_CONFIG } from '../constants'; 10 | import { ModuleRef } from '@nestjs/core'; 11 | import { EventBusPrepublishService } from './event-bus-prepublish.service'; 12 | import { InvalidEventException } from '../exceptions/invalid-event.exception'; 13 | import { 14 | EVENT_STORE_SERVICE, 15 | IEventStoreService, 16 | } from '../event-store/services/event-store.service.interface'; 17 | 18 | // add next, pass onError 19 | 20 | @Injectable() 21 | export class WriteEventBus< 22 | EventBase extends IWriteEvent = IWriteEvent, 23 | > extends Parent { 24 | private logger = new Logger(this.constructor.name); 25 | constructor( 26 | @Inject(EVENT_STORE_SERVICE) 27 | private readonly eventstoreService: IEventStoreService, 28 | @Inject(WRITE_EVENT_BUS_CONFIG) 29 | private readonly config: IWriteEventBusConfig, 30 | private readonly prepublish: EventBusPrepublishService, 31 | commandBus: CommandBus, 32 | moduleRef: ModuleRef, 33 | ) { 34 | super(commandBus, moduleRef); 35 | this.logger.debug('Registering Write EventBus for EventStore...'); 36 | this.publisher = new EventStorePublisher( 37 | this.eventstoreService, 38 | this.config, 39 | ); 40 | } 41 | 42 | async publish( 43 | event: T, 44 | context?: PublicationContextInterface, 45 | ): Promise { 46 | this.logger.debug('Publish in write bus'); 47 | const preparedEvents = await this.prepublish.prepare(this.config, [event]); 48 | const validated = await this.prepublish.validate( 49 | this.config, 50 | preparedEvents, 51 | ); 52 | if (validated.length) { 53 | throw new InvalidEventException(validated); 54 | } 55 | return await this.publisher.publish( 56 | preparedEvents, 57 | // @ts-ignore 58 | context, 59 | ); 60 | } 61 | async publishAll( 62 | events: T[], 63 | context?: PublicationContextInterface, 64 | ): Promise { 65 | this.logger.debug('Publish All in write bus'); 66 | const preparedEvents = await this.prepublish.prepare(this.config, events); 67 | const validated = await this.prepublish.validate( 68 | this.config, 69 | preparedEvents, 70 | ); 71 | if (validated.length) { 72 | throw new InvalidEventException(validated); 73 | } 74 | return await this.publisher.publishAll( 75 | preparedEvents, 76 | // @ts-ignore 77 | context, 78 | ); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/decorators/event-version.decorator.ts: -------------------------------------------------------------------------------- 1 | import { IBaseEvent } from '../interfaces'; 2 | 3 | export const EventVersion = (version: number) => < 4 | T extends { new (...args: any[]): IBaseEvent } 5 | >( 6 | BaseEvent: T, 7 | ) => { 8 | const newClass = class extends BaseEvent implements IBaseEvent { 9 | constructor(...args: any[]) { 10 | super(...args); 11 | this.metadata.version = version; 12 | } 13 | }; 14 | Object.defineProperty(newClass, 'name', { 15 | value: BaseEvent.name, 16 | }); 17 | return newClass; 18 | }; 19 | -------------------------------------------------------------------------------- /src/decorators/index.ts: -------------------------------------------------------------------------------- 1 | export * from './event-version.decorator'; 2 | -------------------------------------------------------------------------------- /src/dto/event-metadata.dto.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Equals, 3 | IsNotEmpty, 4 | IsOptional, 5 | IsPositive, 6 | IsRFC3339, 7 | IsString, 8 | IsUrl, 9 | Matches, 10 | } from 'class-validator'; 11 | 12 | /** 13 | * Event Store event metadata prepared to be transformed into cloudevents 14 | * @see https://github.com/cloudevents/spec/blob/v1.0.1/spec.md#overview 15 | */ 16 | export class EventMetadataDto { 17 | // Cloud Event Metadata 18 | /** 19 | * Specification Version 20 | * @readonly 21 | * @see https://github.com/cloudevents/spec/blob/v1.0.1/spec.md#overview 22 | */ 23 | @Equals(1) 24 | readonly specversion; 25 | 26 | /** 27 | * Timestamp of creation date 28 | * @example 1524379940 29 | */ 30 | @IsRFC3339() 31 | time: string; 32 | 33 | /** 34 | * Typeof event. Note that "-" are not allowed, use "_" instead 35 | * .... 36 | * @example com.api.order.order_created.v2 37 | */ 38 | @Matches(/(\w+\.){1,4}\w+/) 39 | type: string; 40 | 41 | /** 42 | * Identifier of the context in which an event happened, event source 43 | * An absolute URI is RECOMMENDED. 44 | * @example http://api-live.net/order/create 45 | * @example /order/create 46 | */ 47 | @IsUrl({ 48 | require_host: false, 49 | require_protocol: false, 50 | require_tld: false, 51 | allow_protocol_relative_urls: true, 52 | }) 53 | source: string; 54 | 55 | /** 56 | * Identifier in source context sub-structure, if any 57 | */ 58 | @IsOptional() 59 | @IsString() 60 | @IsNotEmpty() 61 | subject?: string; 62 | 63 | /** 64 | * @see RFC 2046 65 | */ 66 | @IsOptional() 67 | @IsString() 68 | @IsNotEmpty() 69 | datacontenttype?: string; 70 | 71 | /** 72 | * Identifies the schema that data adheres to. 73 | * Incompatible changes to the schema SHOULD 74 | * be reflected by a different URI 75 | */ 76 | @IsOptional() 77 | @IsUrl() 78 | dataschema?: string; 79 | 80 | // EventStore Specific (must be inside event-cloud data when transformed) 81 | /** 82 | * Event version 83 | * @example 1 84 | */ 85 | @IsPositive() 86 | version: number; 87 | 88 | /** 89 | * Business process unique id 90 | * @example 15d5f8d5-869e-4107-9961-5035495fe416 91 | */ 92 | @IsString() 93 | @IsNotEmpty() 94 | correlation_id: string; 95 | 96 | /** 97 | * The event creation date (IsoString formatted) 98 | * @example '2022-02-09T17:16:52.305Z' 99 | */ 100 | @IsString() 101 | created_at: string; 102 | } 103 | -------------------------------------------------------------------------------- /src/dto/index.ts: -------------------------------------------------------------------------------- 1 | export * from './event-metadata.dto'; 2 | export * from './write-event.dto'; 3 | -------------------------------------------------------------------------------- /src/dto/write-event.dto.ts: -------------------------------------------------------------------------------- 1 | import { ValidateNested, IsNotEmpty, IsString } from 'class-validator'; 2 | import { Type } from 'class-transformer'; 3 | import { EventMetadataDto } from './event-metadata.dto'; 4 | 5 | export class WriteEventDto { 6 | // TODO Vincent IsUuid ? 7 | @IsNotEmpty() 8 | @IsString() 9 | eventId: string; 10 | 11 | @IsNotEmpty() 12 | @IsString() 13 | eventType: string; 14 | 15 | @ValidateNested() 16 | @Type(() => EventMetadataDto) 17 | metadata: Partial; // we add partial to allow metadata auto-generation 18 | 19 | @ValidateNested() 20 | data: any; 21 | } 22 | -------------------------------------------------------------------------------- /src/event-store/config/connector.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DNSClusterOptions, 3 | GossipClusterOptions, 4 | SingleNodeOptions, 5 | } from '@eventstore/db-client/dist/Client'; 6 | 7 | export default interface Connector { 8 | connectionString?: string; 9 | OptionSettings?: SingleNodeOptions | DNSClusterOptions | GossipClusterOptions; 10 | } 11 | -------------------------------------------------------------------------------- /src/event-store/config/event-store-connection-config.ts: -------------------------------------------------------------------------------- 1 | import { Credentials } from '@eventstore/db-client/dist/types'; 2 | import { ChannelCredentialOptions } from '@eventstore/db-client/dist/Client'; 3 | import Connector from './connector'; 4 | 5 | export interface EventStoreConnectionConfig { 6 | connectionSettings: Connector; 7 | channelCredentials?: ChannelCredentialOptions; 8 | defaultUserCredentials?: Credentials; 9 | } 10 | -------------------------------------------------------------------------------- /src/event-store/config/event-store-service-config.interface.ts: -------------------------------------------------------------------------------- 1 | import { EventStoreProjection } from '../../interfaces'; 2 | import { IPersistentSubscriptionConfig } from '../subscriptions'; 3 | 4 | export interface IEventStoreSubsystems { 5 | projections?: EventStoreProjection[]; 6 | subscriptions?: { 7 | persistent?: IPersistentSubscriptionConfig[]; 8 | }; 9 | onEvent?: (sub, payload) => void; 10 | onConnectionFail?: (err: Error) => void; 11 | } 12 | -------------------------------------------------------------------------------- /src/event-store/config/index.ts: -------------------------------------------------------------------------------- 1 | export * from './connector'; 2 | export * from './event-store-connection-config'; 3 | export * from './event-store-service-config.interface'; 4 | -------------------------------------------------------------------------------- /src/event-store/event-store-aggregate-root.ts: -------------------------------------------------------------------------------- 1 | import { AggregateRoot as Parent } from '../cqrs'; 2 | import { IBaseEvent, PublicationContextInterface } from '../interfaces'; 3 | import * as constants from '@eventstore/db-client/dist/constants'; 4 | import { AppendExpectedRevision } from '@eventstore/db-client/dist/types'; 5 | import { StreamMetadata } from '@eventstore/db-client/dist/utils/streamMetadata'; 6 | 7 | export abstract class EventStoreAggregateRoot< 8 | EventBase extends IBaseEvent = IBaseEvent, 9 | > extends Parent { 10 | private _streamName?: string; 11 | private _streamMetadata?: StreamMetadata; 12 | 13 | set streamName(streamName: string) { 14 | this._streamName = streamName; 15 | } 16 | 17 | set streamMetadata(streamMetadata: StreamMetadata) { 18 | this._streamMetadata = streamMetadata; 19 | } 20 | 21 | set maxAge(maxAge: number) { 22 | this._streamMetadata = { 23 | ...this._streamMetadata, 24 | $maxAge: maxAge, 25 | }; 26 | } 27 | 28 | set maxCount(maxCount: number) { 29 | this._streamMetadata = { 30 | ...this._streamMetadata, 31 | $maxCount: maxCount, 32 | }; 33 | } 34 | 35 | public async commit( 36 | expectedRevision: AppendExpectedRevision = constants.ANY, 37 | expectedMetadataRevision: AppendExpectedRevision = constants.ANY, 38 | ) { 39 | this.logger.debug( 40 | `Aggregate will commit ${this.getUncommittedEvents().length} events in ${ 41 | this.publishers.length 42 | } publishers`, 43 | ); 44 | const context: PublicationContextInterface = { 45 | expectedRevision, 46 | ...(this._streamName ? { streamName: this._streamName } : {}), 47 | ...(this._streamMetadata 48 | ? { streamMetadata: this._streamMetadata, expectedMetadataRevision } 49 | : {}), 50 | }; 51 | for (const publisher of this.publishers) { 52 | await publisher(this.getUncommittedEvents(), context); 53 | } 54 | this.clearEvents(); 55 | return this; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/event-store/event-store.module.ts: -------------------------------------------------------------------------------- 1 | import { DynamicModule, Module, Provider } from '@nestjs/common'; 2 | import { EventStoreService } from './index'; 3 | import { EventStoreHealthIndicator } from './health'; 4 | import { EVENT_STORE_SUBSYSTEMS } from '../constants'; 5 | import { IEventStoreSubsystems } from './config'; 6 | import { EventStoreConnectionConfig } from './config/event-store-connection-config'; 7 | import { EVENT_STORE_SERVICE } from './services/event-store.service.interface'; 8 | import { Client } from '@eventstore/db-client/dist/Client'; 9 | import { EventStoreDBClient } from '@eventstore/db-client'; 10 | import { EVENT_STORE_CONNECTOR } from './services/event-store.constants'; 11 | import { EVENTS_AND_METADATAS_STACKER } from './reliability/interface/events-and-metadatas-stacker'; 12 | import InMemoryEventsAndMetadatasStacker from './reliability/implementations/in-memory/in-memory-events-and-metadatas-stacker'; 13 | 14 | @Module({ 15 | providers: [ 16 | EventStoreHealthIndicator, 17 | { 18 | provide: EVENT_STORE_SERVICE, 19 | useClass: EventStoreService, 20 | }, 21 | ], 22 | exports: [EVENT_STORE_SERVICE, EventStoreHealthIndicator], 23 | }) 24 | export class EventStoreModule { 25 | static async register( 26 | config: EventStoreConnectionConfig, 27 | eventStoreSubsystems: IEventStoreSubsystems = { 28 | onConnectionFail: (e) => console.log('e : ', e), 29 | }, 30 | ): Promise { 31 | return { 32 | module: EventStoreModule, 33 | providers: [ 34 | { 35 | provide: EVENT_STORE_SUBSYSTEMS, 36 | useValue: eventStoreSubsystems, 37 | }, 38 | { 39 | provide: EVENTS_AND_METADATAS_STACKER, 40 | useClass: InMemoryEventsAndMetadatasStacker, 41 | }, 42 | await this.getEventStoreConnector(config), 43 | ], 44 | }; 45 | } 46 | 47 | private static async getEventStoreConnector( 48 | config: EventStoreConnectionConfig, 49 | ): Promise { 50 | const eventStoreConnector: Client = EventStoreDBClient.connectionString( 51 | (config as EventStoreConnectionConfig).connectionSettings 52 | .connectionString, 53 | ); 54 | 55 | return { 56 | provide: EVENT_STORE_CONNECTOR, 57 | useValue: eventStoreConnector, 58 | }; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/event-store/events/event-store-acknowledgeable.event.ts: -------------------------------------------------------------------------------- 1 | import { EventStoreEvent } from './index'; 2 | import { IAcknowledgeableEvent } from '../../interfaces'; 3 | import { PersistentSubscriptionNakEventAction } from '../../interfaces/events/persistent-subscription-nak-event-action.enum'; 4 | 5 | export abstract class EventStoreAcknowledgeableEvent 6 | extends EventStoreEvent 7 | implements IAcknowledgeableEvent 8 | { 9 | ack() { 10 | return Promise.resolve(); 11 | } 12 | nack(action: PersistentSubscriptionNakEventAction, reason: string) { 13 | return Promise.resolve(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/event-store/events/event-store.event.ts: -------------------------------------------------------------------------------- 1 | import { v4 } from 'uuid'; 2 | 3 | import { EventOptionsType, IReadEvent, IWriteEvent } from '../../interfaces'; 4 | import { WriteEventDto } from '../../dto/write-event.dto'; 5 | 6 | export abstract class EventStoreEvent 7 | extends WriteEventDto 8 | implements IWriteEvent, IReadEvent 9 | { 10 | // just for read events 11 | public readonly eventStreamId: IReadEvent['eventStreamId'] | undefined; 12 | public readonly eventNumber: IReadEvent['eventNumber'] | undefined; 13 | public readonly originalEventId: IReadEvent['originalEventId'] | undefined; 14 | 15 | constructor(public data: any, options?: EventOptionsType) { 16 | super(); 17 | // metadata is added automatically in write events, so we cast to any 18 | this.metadata = options?.metadata || {}; 19 | this.eventId = options?.eventId || v4(); 20 | this.eventType = options?.eventType || this.constructor.name; 21 | this.eventStreamId = options?.eventStreamId ?? undefined; 22 | this.eventNumber = options?.eventNumber ?? undefined; 23 | this.originalEventId = options?.originalEventId ?? undefined; 24 | } 25 | 26 | // Notice we force this helpers to return strings 27 | // to keep string typing (!undefined) on our subscriptions 28 | getStream(): string { 29 | return this.eventStreamId || ''; 30 | } 31 | getStreamCategory(): string { 32 | return this.eventStreamId?.split('-')[0] ?? ''; 33 | } 34 | getStreamId(): string { 35 | return this.eventStreamId?.replace(/^[^-]*-/, '') ?? ''; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/event-store/events/index.ts: -------------------------------------------------------------------------------- 1 | export * from './event-store.event'; 2 | export * from './event-store-acknowledgeable.event'; 3 | -------------------------------------------------------------------------------- /src/event-store/health/event-store-health.status.ts: -------------------------------------------------------------------------------- 1 | export default interface EventStoreHealthStatus { 2 | connection?: 'up' | 'down'; 3 | subscriptions?: 'up' | 'down'; 4 | } 5 | -------------------------------------------------------------------------------- /src/event-store/health/event-store.health-indicator.spec.ts: -------------------------------------------------------------------------------- 1 | import { EventStoreHealthIndicator } from './event-store.health-indicator'; 2 | import EventStoreHealthStatus from './event-store-health.status'; 3 | import { HealthIndicatorResult } from '@nestjs/terminus'; 4 | import { Logger as logger } from '@nestjs/common'; 5 | 6 | describe('EventStoreHealthIndicator', () => { 7 | let service: EventStoreHealthIndicator; 8 | 9 | jest.mock('@nestjs/common'); 10 | beforeEach(() => { 11 | service = new EventStoreHealthIndicator(); 12 | jest.spyOn(logger, 'log').mockImplementation(() => null); 13 | jest.spyOn(logger, 'error').mockImplementation(() => null); 14 | jest.spyOn(logger, 'debug').mockImplementation(() => null); 15 | }); 16 | 17 | it('should be created', () => { 18 | expect(service).toBeTruthy(); 19 | }); 20 | 21 | ['up', 'down'].forEach((status: 'up' | 'down') => { 22 | it(`should be notified when connection is ${status}`, () => { 23 | const esHealthStatus: EventStoreHealthStatus = { 24 | connection: status, 25 | }; 26 | service.updateStatus(esHealthStatus); 27 | 28 | const check: HealthIndicatorResult = service.check(); 29 | 30 | expect(check.connection.status).toEqual(status); 31 | }); 32 | }); 33 | 34 | ['up', 'down'].forEach((status: 'up' | 'down') => { 35 | it(`should be notified when subscription's connection is ${status}`, () => { 36 | const esHealthStatus: EventStoreHealthStatus = { 37 | subscriptions: status, 38 | }; 39 | service.updateStatus(esHealthStatus); 40 | 41 | const check: HealthIndicatorResult = service.check(); 42 | 43 | expect(check.subscriptions.status).toEqual(status); 44 | }); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /src/event-store/health/event-store.health-indicator.ts: -------------------------------------------------------------------------------- 1 | import { HealthIndicator, HealthIndicatorResult } from '@nestjs/terminus'; 2 | import { Injectable } from '@nestjs/common'; 3 | import EventStoreHealthStatus from './event-store-health.status'; 4 | 5 | @Injectable() 6 | export class EventStoreHealthIndicator extends HealthIndicator { 7 | private esStatus: EventStoreHealthStatus; 8 | 9 | constructor() { 10 | super(); 11 | } 12 | 13 | public check(): HealthIndicatorResult { 14 | return { 15 | connection: { status: this.esStatus.connection }, 16 | subscriptions: { status: this.esStatus.subscriptions }, 17 | }; 18 | } 19 | 20 | public updateStatus(esHealthStatus: EventStoreHealthStatus): void { 21 | this.esStatus = { ...this.esStatus, ...esHealthStatus }; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/event-store/health/index.ts: -------------------------------------------------------------------------------- 1 | export * from './event-store.health-indicator'; 2 | export * from './event-store-health.status'; 3 | -------------------------------------------------------------------------------- /src/event-store/index.ts: -------------------------------------------------------------------------------- 1 | export * from './config'; 2 | export * from './events'; 3 | export * from './health'; 4 | export * from './publisher'; 5 | export * from './reliability'; 6 | export * from './services'; 7 | export * from './subscriptions'; 8 | export * from './event-store-aggregate-root'; 9 | export * from './event-store.module'; 10 | -------------------------------------------------------------------------------- /src/event-store/publisher/event-store.publisher.spec.ts: -------------------------------------------------------------------------------- 1 | import { EventStorePublisher } from './event-store.publisher'; 2 | import { 3 | IWriteEvent, 4 | IWriteEventBusConfig, 5 | PublicationContextInterface, 6 | } from '../../interfaces'; 7 | import { of } from 'rxjs'; 8 | import { EventStoreService } from '../services/event-store.service'; 9 | import * as constants from '@eventstore/db-client/dist/constants'; 10 | import { AppendExpectedRevision } from '@eventstore/db-client/dist/types'; 11 | import { StreamMetadata } from '@eventstore/db-client/dist/utils/streamMetadata'; 12 | import { Logger as logger } from '@nestjs/common'; 13 | import InMemoryEventsAndMetadatasStacker from '../reliability/implementations/in-memory/in-memory-events-and-metadatas-stacker'; 14 | import { Client } from '@eventstore/db-client/dist/Client'; 15 | import { EventStoreHealthIndicator } from '../health'; 16 | import spyOn = jest.spyOn; 17 | 18 | describe('EventStorePublisher', () => { 19 | let publisher: EventStorePublisher; 20 | 21 | let eventStore: Client; 22 | let eventStoreService: EventStoreService; 23 | let publisherConfig: IWriteEventBusConfig; 24 | 25 | const eventsStackerMock: InMemoryEventsAndMetadatasStacker = { 26 | putEventsInWaitingLine: jest.fn(), 27 | shiftEventsBatchFromWaitingLine: jest.fn(), 28 | getFirstOutFromEventsBatchesWaitingLine: jest.fn(), 29 | getEventBatchesWaitingLineLength: jest.fn(), 30 | putMetadatasInWaitingLine: jest.fn(), 31 | getFirstOutFromMetadatasWaitingLine: jest.fn(), 32 | shiftMetadatasFromWaitingLine: jest.fn(), 33 | getMetadatasWaitingLineLength: jest.fn(), 34 | } as unknown as InMemoryEventsAndMetadatasStacker; 35 | 36 | beforeEach(() => { 37 | jest.resetAllMocks(); 38 | jest.mock('@nestjs/common'); 39 | jest.spyOn(logger, 'log').mockImplementation(() => null); 40 | jest.spyOn(logger, 'error').mockImplementation(() => null); 41 | jest.spyOn(logger, 'debug').mockImplementation(() => null); 42 | 43 | publisherConfig = {}; 44 | eventStore = { 45 | appendToStream: jest.fn(), 46 | setStreamMetadata: jest.fn(), 47 | } as unknown as Client; 48 | 49 | const eventStoreHealthIndicatorMock: EventStoreHealthIndicator = { 50 | updateStatus: jest.fn(), 51 | check: jest.fn(), 52 | } as unknown as EventStoreHealthIndicator; 53 | 54 | eventStoreService = new EventStoreService( 55 | eventStore, 56 | { 57 | onConnectionFail: () => {}, 58 | }, 59 | eventsStackerMock, 60 | eventStoreHealthIndicatorMock, 61 | ); 62 | publisher = new EventStorePublisher( 63 | eventStoreService, 64 | publisherConfig, 65 | ); 66 | }); 67 | 68 | it('should be instanciated properly', () => { 69 | expect(publisher).toBeTruthy(); 70 | }); 71 | 72 | it('should give default context value when write events and no context given', async () => { 73 | spyOn(eventStore, 'appendToStream').mockImplementationOnce(() => { 74 | return null; 75 | }); 76 | spyOn(eventStoreService, 'writeEvents'); 77 | await eventStoreService.onModuleInit(); 78 | await publisher.publish({ 79 | data: undefined, 80 | metadata: { 81 | correlation_id: 'toto', 82 | }, 83 | }); 84 | expect(eventStoreService.writeEvents).toHaveBeenCalledWith( 85 | expect.anything(), 86 | expect.anything(), 87 | { expectedRevision: constants.ANY }, 88 | ); 89 | }); 90 | 91 | it('should write metadatas when metadata stream is given', async () => { 92 | spyOn(eventStore, 'setStreamMetadata'); 93 | spyOn(eventStoreService, 'writeMetadata'); 94 | 95 | const streamName = 'streamName'; 96 | const streamMetadata: StreamMetadata = { truncateBefore: 'start' }; 97 | const expectedRevision: AppendExpectedRevision = constants.STREAM_EXISTS; 98 | const context: PublicationContextInterface = { 99 | streamName: streamName, 100 | expectedRevision: constants.ANY, 101 | streamMetadata, 102 | options: { expectedRevision }, 103 | }; 104 | 105 | await publisher.publish( 106 | { 107 | data: undefined, 108 | metadata: { 109 | correlation_id: 'toto', 110 | }, 111 | }, 112 | context, 113 | ); 114 | 115 | expect(eventStoreService.writeMetadata).toHaveBeenCalledWith( 116 | streamName, 117 | streamMetadata, 118 | context.options, 119 | ); 120 | }); 121 | 122 | it('should publish single event the same way than multiple events when only 1 event is ', async () => { 123 | eventStore.appendToStream = jest.fn().mockReturnValue(of({})); 124 | spyOn(publisher, 'publishAll'); 125 | await publisher.publish({ 126 | data: undefined, 127 | metadata: { 128 | correlation_id: 'toto', 129 | }, 130 | }); 131 | expect(publisher.publishAll).toHaveBeenCalled(); 132 | }); 133 | }); 134 | -------------------------------------------------------------------------------- /src/event-store/publisher/event-store.publisher.ts: -------------------------------------------------------------------------------- 1 | import { hostname } from 'os'; 2 | import { IEventPublisher } from '@nestjs/cqrs'; 3 | import { Inject, Logger } from '@nestjs/common'; 4 | import { basename, extname } from 'path'; 5 | 6 | import { 7 | IWriteEvent, 8 | IWriteEventBusConfig, 9 | PublicationContextInterface, 10 | } from '../../interfaces'; 11 | import { 12 | EVENT_STORE_SERVICE, 13 | IEventStoreService, 14 | } from '../services/event-store.service.interface'; 15 | import { AppendResult } from '@eventstore/db-client/dist/types'; 16 | import { EventData } from '@eventstore/db-client/dist/types/events'; 17 | import { jsonEvent } from '@eventstore/db-client'; 18 | import * as constants from '@eventstore/db-client/dist/constants'; 19 | 20 | export class EventStorePublisher 21 | implements IEventPublisher 22 | { 23 | private logger: Logger = new Logger(this.constructor.name); 24 | 25 | constructor( 26 | @Inject(EVENT_STORE_SERVICE) 27 | private readonly eventStoreService: IEventStoreService, 28 | private readonly config: IWriteEventBusConfig, 29 | ) {} 30 | 31 | private async writeEvents( 32 | events: T[], 33 | context: PublicationContextInterface = {}, 34 | ): Promise { 35 | const { 36 | streamName = context?.streamName || 37 | this.getStreamName(events[0].metadata.correlation_id), 38 | expectedRevision, 39 | streamMetadata, 40 | options, 41 | } = context; 42 | if (streamMetadata) { 43 | await this.eventStoreService.writeMetadata( 44 | streamName, 45 | streamMetadata, 46 | options, 47 | ); 48 | } 49 | const eventCount = events.length; 50 | this.logger.debug( 51 | `Write ${eventCount} events to stream ${streamName} with expectedVersion ${expectedRevision}`, 52 | ); 53 | return this.eventStoreService.writeEvents( 54 | streamName, 55 | events.map((event: T): EventData => { 56 | return jsonEvent({ 57 | id: event.eventId, 58 | type: event.eventType, 59 | metadata: event.metadata, 60 | data: event.data, 61 | }); 62 | }), 63 | { 64 | expectedRevision: expectedRevision ?? constants.ANY, 65 | }, 66 | ); 67 | } 68 | 69 | protected getStreamName( 70 | correlationId: EventBase['metadata']['correlation_id'], 71 | ): string { 72 | const defaultName = process.argv?.[1] 73 | ? basename(process.argv?.[1], extname(process.argv?.[1])) 74 | : `${hostname()}_${process.argv?.[0] || 'unknown'}`; 75 | 76 | return `${this.config.serviceName || defaultName}-${correlationId}`; 77 | } 78 | 79 | async publish( 80 | event: T, 81 | context?: PublicationContextInterface, 82 | ): Promise { 83 | return this.publishAll([event], context); 84 | } 85 | 86 | async publishAll( 87 | events: T[], 88 | context?: PublicationContextInterface, 89 | ): Promise { 90 | return await this.writeEvents(events, context); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/event-store/publisher/index.ts: -------------------------------------------------------------------------------- 1 | export * from './event-store.publisher'; 2 | -------------------------------------------------------------------------------- /src/event-store/reliability/implementations/in-memory/in-memory-events-and-metadatas-stacker.spec.ts: -------------------------------------------------------------------------------- 1 | import InMemoryEventsAndMetadatasStacker from './in-memory-events-and-metadatas-stacker'; 2 | import EventBatch from '../../interface/event-batch'; 3 | import { EventData } from '@eventstore/db-client/dist/types/events'; 4 | import MetadatasContextDatas from '../../interface/metadatas-context-datas'; 5 | import { AppendToStreamOptions } from '@eventstore/db-client/dist/streams'; 6 | import { ANY } from '@eventstore/db-client'; 7 | import { Logger as logger } from '@nestjs/common'; 8 | 9 | describe('InMemoryEventsAndMetadatasStacker', () => { 10 | let service: InMemoryEventsAndMetadatasStacker; 11 | 12 | jest.mock('@nestjs/common'); 13 | beforeEach(() => { 14 | jest.spyOn(logger, 'log').mockImplementation(() => null); 15 | jest.spyOn(logger, 'error').mockImplementation(() => null); 16 | jest.spyOn(logger, 'debug').mockImplementation(() => null); 17 | service = new InMemoryEventsAndMetadatasStacker(); 18 | }); 19 | 20 | describe('when stacking events', () => { 21 | it('should be able to add a new element at the end of the fifo', () => { 22 | let event: EventData = getDumbEvent('1'); 23 | let events: EventData[] = [event]; 24 | let stream = 'poj'; 25 | let expectedVersion: AppendToStreamOptions = { expectedRevision: ANY }; 26 | const batch1: EventBatch = { 27 | events, 28 | stream, 29 | expectedVersion, 30 | }; 31 | event = getDumbEvent('2'); 32 | events = [event]; 33 | stream = 'poj'; 34 | expectedVersion = { expectedRevision: ANY }; 35 | const batch2 = { 36 | events, 37 | stream, 38 | expectedVersion, 39 | }; 40 | 41 | service.putEventsInWaitingLine(batch1); 42 | service.putEventsInWaitingLine(batch2); 43 | 44 | expect( 45 | service.getFirstOutFromEventsBatchesWaitingLine().events[0].id, 46 | ).toEqual('1'); 47 | expect( 48 | service.getFirstOutFromEventsBatchesWaitingLine().events[0].id, 49 | ).not.toEqual('2'); 50 | }); 51 | 52 | it('should not fail when getting first out from waiting line and line is empty', () => { 53 | expect(service.getFirstOutFromEventsBatchesWaitingLine()).toBeNull(); 54 | }); 55 | 56 | it('should be able to give the fifo length ', () => { 57 | const batch1: EventBatch = getDumbBatch('1', 'poj'); 58 | const batch2: EventBatch = getDumbBatch('2', 'oiu'); 59 | 60 | service.putEventsInWaitingLine(batch1); 61 | service.putEventsInWaitingLine(batch2); 62 | 63 | expect(service.getEventBatchesWaitingLineLength()).toEqual(2); 64 | }); 65 | 66 | it('should be able to remove the first element of the waiting line', () => { 67 | const batch1: EventBatch = getDumbBatch('1', 'poj'); 68 | const batch2: EventBatch = getDumbBatch('2', 'oiu'); 69 | 70 | service.putEventsInWaitingLine(batch1); 71 | service.putEventsInWaitingLine(batch2); 72 | 73 | const unstackedBatch1: EventBatch = 74 | service.shiftEventsBatchFromWaitingLine(); 75 | const unstackedBatch2: EventBatch = 76 | service.shiftEventsBatchFromWaitingLine(); 77 | 78 | expect(unstackedBatch1.stream).toEqual('poj'); 79 | expect(unstackedBatch2.stream).toEqual('oiu'); 80 | expect(service.getEventBatchesWaitingLineLength()).toEqual(0); 81 | }); 82 | }); 83 | 84 | describe('when stacking metadatas', () => { 85 | it('should be able to add a new element at the end of the fifo', () => { 86 | const metadatasContextDatas1: MetadatasContextDatas = 87 | getDumbMetadata('1'); 88 | const metadatasContextDatas2: MetadatasContextDatas = 89 | getDumbMetadata('2'); 90 | 91 | service.putMetadatasInWaitingLine(metadatasContextDatas1); 92 | service.putMetadatasInWaitingLine(metadatasContextDatas2); 93 | 94 | expect(service.getFirstOutFromMetadatasWaitingLine().streamName).toEqual( 95 | '1', 96 | ); 97 | expect( 98 | service.getFirstOutFromMetadatasWaitingLine().streamName, 99 | ).not.toEqual('2'); 100 | }); 101 | 102 | it('should not fail when getting first out from waiting line and line is empty', () => { 103 | expect(service.getFirstOutFromMetadatasWaitingLine()).toBeNull(); 104 | }); 105 | 106 | it('should be able to give the fifo length ', () => { 107 | const metadatasContextDatas1: MetadatasContextDatas = 108 | getDumbMetadata('1'); 109 | const metadatasContextDatas2: MetadatasContextDatas = 110 | getDumbMetadata('2'); 111 | 112 | service.putMetadatasInWaitingLine(metadatasContextDatas1); 113 | service.putMetadatasInWaitingLine(metadatasContextDatas2); 114 | 115 | expect(service.getMetadatasWaitingLineLength()).toEqual(2); 116 | }); 117 | 118 | it('should be able to remove the first element of the waiting line', () => { 119 | const metadatasContextDatas1: MetadatasContextDatas = 120 | getDumbMetadata('1'); 121 | const metadatasContextDatas2: MetadatasContextDatas = 122 | getDumbMetadata('2'); 123 | 124 | service.putMetadatasInWaitingLine(metadatasContextDatas1); 125 | service.putMetadatasInWaitingLine(metadatasContextDatas2); 126 | 127 | const metadataGot1 = service.shiftMetadatasFromWaitingLine(); 128 | const metadataGot2 = service.shiftMetadatasFromWaitingLine(); 129 | 130 | expect(metadataGot1.streamName).toEqual('1'); 131 | expect(metadataGot2.streamName).toEqual('2'); 132 | expect(service.getEventBatchesWaitingLineLength()).toEqual(0); 133 | }); 134 | }); 135 | }); 136 | 137 | function getDumbBatch(eventId: string, stream: string): EventBatch { 138 | const events: EventData[] = [getDumbEvent(eventId)]; 139 | const expectedVersion: AppendToStreamOptions = { expectedRevision: ANY }; 140 | return { 141 | events, 142 | stream, 143 | expectedVersion, 144 | }; 145 | } 146 | 147 | const getDumbEvent = (id?: string): EventData => { 148 | return { 149 | contentType: undefined, 150 | data: undefined, 151 | id: id ?? '', 152 | metadata: undefined, 153 | type: '', 154 | }; 155 | }; 156 | const getDumbMetadata = (streamName?: string): MetadatasContextDatas => { 157 | return { 158 | metadata: undefined, 159 | streamName: streamName ?? '', 160 | }; 161 | }; 162 | -------------------------------------------------------------------------------- /src/event-store/reliability/implementations/in-memory/in-memory-events-and-metadatas-stacker.ts: -------------------------------------------------------------------------------- 1 | import IEventsAndMetadatasStacker from '../../interface/events-and-metadatas-stacker'; 2 | import EventBatch from '../../interface/event-batch'; 3 | import MetadatasContextDatas from '../../interface/metadatas-context-datas'; 4 | 5 | export default class InMemoryEventsAndMetadatasStacker 6 | implements IEventsAndMetadatasStacker 7 | { 8 | private eventBatchesFifo: EventBatch[] = []; 9 | 10 | private metadatasFifo: MetadatasContextDatas[] = []; 11 | 12 | public shiftEventsBatchFromWaitingLine(): EventBatch { 13 | return this.eventBatchesFifo.shift(); 14 | } 15 | 16 | public getFirstOutFromEventsBatchesWaitingLine(): EventBatch { 17 | if (this.eventBatchesFifo.length === 0) { 18 | return null; 19 | } 20 | return this.eventBatchesFifo[0]; 21 | } 22 | 23 | public putEventsInWaitingLine(batch: EventBatch): void { 24 | this.eventBatchesFifo.push(batch); 25 | } 26 | 27 | public getEventBatchesWaitingLineLength(): number { 28 | return this.eventBatchesFifo.length; 29 | } 30 | 31 | public shiftMetadatasFromWaitingLine(): MetadatasContextDatas { 32 | return this.metadatasFifo.shift(); 33 | } 34 | 35 | public getFirstOutFromMetadatasWaitingLine(): MetadatasContextDatas { 36 | if (this.metadatasFifo.length === 0) { 37 | return null; 38 | } 39 | return this.metadatasFifo[0]; 40 | } 41 | 42 | public getMetadatasWaitingLineLength(): number { 43 | return this.metadatasFifo.length; 44 | } 45 | 46 | public putMetadatasInWaitingLine(metadata: MetadatasContextDatas): void { 47 | this.metadatasFifo.push(metadata); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/event-store/reliability/index.ts: -------------------------------------------------------------------------------- 1 | export * from './implementations/in-memory/in-memory-events-and-metadatas-stacker'; 2 | export * from './interface/event-batch'; 3 | export * from './interface/events-and-metadatas-stacker'; 4 | export * from './interface/metadatas-context-datas'; 5 | -------------------------------------------------------------------------------- /src/event-store/reliability/interface/event-batch.ts: -------------------------------------------------------------------------------- 1 | import { EventData } from '@eventstore/db-client/dist/types/events'; 2 | import { AppendToStreamOptions } from '@eventstore/db-client/dist/streams'; 3 | 4 | export default interface EventBatch { 5 | stream: string; 6 | events: EventData[]; 7 | expectedVersion: AppendToStreamOptions; 8 | } 9 | -------------------------------------------------------------------------------- /src/event-store/reliability/interface/events-and-metadatas-stacker.ts: -------------------------------------------------------------------------------- 1 | import EventBatch from './event-batch'; 2 | import MetadatasContextDatas from './metadatas-context-datas'; 3 | 4 | export const EVENTS_AND_METADATAS_STACKER = Symbol(); 5 | 6 | export default interface IEventsAndMetadatasStacker { 7 | putEventsInWaitingLine(events: EventBatch): void; 8 | 9 | shiftEventsBatchFromWaitingLine(): EventBatch; 10 | 11 | getFirstOutFromEventsBatchesWaitingLine(): EventBatch; 12 | 13 | getEventBatchesWaitingLineLength(): number; 14 | 15 | putMetadatasInWaitingLine(metadata: MetadatasContextDatas): void; 16 | 17 | getFirstOutFromMetadatasWaitingLine(): MetadatasContextDatas; 18 | 19 | shiftMetadatasFromWaitingLine(): MetadatasContextDatas; 20 | 21 | getMetadatasWaitingLineLength(): number; 22 | } 23 | -------------------------------------------------------------------------------- /src/event-store/reliability/interface/metadatas-context-datas.ts: -------------------------------------------------------------------------------- 1 | import { SetStreamMetadataOptions } from '@eventstore/db-client/dist/streams'; 2 | import { StreamMetadata } from '@eventstore/db-client/dist/utils/streamMetadata'; 3 | 4 | export default interface MetadatasContextDatas { 5 | streamName: string; 6 | metadata: StreamMetadata; 7 | options?: SetStreamMetadataOptions; 8 | } 9 | -------------------------------------------------------------------------------- /src/event-store/services/errors.constant.ts: -------------------------------------------------------------------------------- 1 | export const PERSISTENT_SUBSCRIPTION_ALREADY_EXIST_ERROR_CODE = 6; 2 | export const PROJECTION_ALREADY_EXIST_ERROR_CODE = 2; 3 | export const RECONNECTION_TRY_DELAY_IN_MS = 1000; 4 | -------------------------------------------------------------------------------- /src/event-store/services/event-store.constants.ts: -------------------------------------------------------------------------------- 1 | export const EVENT_STORE_CONNECTOR = Symbol(); 2 | -------------------------------------------------------------------------------- /src/event-store/services/event-store.service.interface.ts: -------------------------------------------------------------------------------- 1 | import { EventStoreProjection } from '../../interfaces'; 2 | import { 3 | CreateContinuousProjectionOptions, 4 | CreateOneTimeProjectionOptions, 5 | CreateTransientProjectionOptions, 6 | GetProjectionStateOptions, 7 | } from '@eventstore/db-client/dist/projections'; 8 | import { DeletePersistentSubscriptionOptions } from '@eventstore/db-client/dist/persistentSubscription'; 9 | import { PersistentSubscriptionSettings } from '@eventstore/db-client/dist/utils'; 10 | import { 11 | AppendResult, 12 | BaseOptions, 13 | Credentials, 14 | StreamingRead, 15 | } from '@eventstore/db-client/dist/types'; 16 | import { 17 | AppendToStreamOptions, 18 | GetStreamMetadataResult, 19 | ReadStreamOptions, 20 | SetStreamMetadataOptions, 21 | } from '@eventstore/db-client/dist/streams'; 22 | import { StreamMetadata } from '@eventstore/db-client/dist/utils/streamMetadata'; 23 | import { ReadableOptions } from 'stream'; 24 | import { PersistentSubscription, ResolvedEvent } from '@eventstore/db-client'; 25 | import { EventData } from '@eventstore/db-client/dist/types/events'; 26 | import { IPersistentSubscriptionConfig } from '../subscriptions'; 27 | 28 | export const EVENT_STORE_SERVICE = Symbol(); 29 | 30 | export interface IEventStoreService { 31 | createProjection( 32 | query: string, 33 | type: 'oneTime' | 'continuous' | 'transient', 34 | projectionName?: string, 35 | options?: 36 | | CreateContinuousProjectionOptions 37 | | CreateTransientProjectionOptions 38 | | CreateOneTimeProjectionOptions, 39 | ): Promise; 40 | 41 | getProjectionState( 42 | streamName: string, 43 | options?: GetProjectionStateOptions, 44 | ): Promise; 45 | 46 | updateProjection( 47 | projection: EventStoreProjection, 48 | content: string, 49 | ): Promise; 50 | 51 | upsertProjections(projections: EventStoreProjection[]): Promise; 52 | 53 | createPersistentSubscription( 54 | streamName: string, 55 | groupName: string, 56 | settings: Partial, 57 | options?: BaseOptions, 58 | ): Promise; 59 | 60 | updatePersistentSubscription( 61 | streamName: string, 62 | group: string, 63 | options: Partial, 64 | credentials?: Credentials, 65 | ): Promise; 66 | 67 | deletePersistentSubscription( 68 | streamName: string, 69 | groupName: string, 70 | options?: DeletePersistentSubscriptionOptions, 71 | ): Promise; 72 | 73 | subscribeToPersistentSubscriptions( 74 | subscriptions: IPersistentSubscriptionConfig[], 75 | ): Promise; 76 | 77 | getPersistentSubscriptions(): PersistentSubscription[]; 78 | 79 | readMetadata(stream: string): Promise; 80 | 81 | writeMetadata( 82 | streamName: string, 83 | metadata: StreamMetadata, 84 | options?: SetStreamMetadataOptions, 85 | ): Promise; 86 | 87 | readFromStream( 88 | stream: string, 89 | options?: ReadStreamOptions, 90 | readableOptions?: ReadableOptions, 91 | ): Promise>; 92 | 93 | writeEvents( 94 | stream: string, 95 | events: EventData[], 96 | expectedVersion: AppendToStreamOptions, 97 | ): Promise; 98 | } 99 | -------------------------------------------------------------------------------- /src/event-store/services/event.handler.helper.spec.ts: -------------------------------------------------------------------------------- 1 | import EventHandlerHelper from './event.handler.helper'; 2 | import { Logger, Logger as logger } from '@nestjs/common'; 3 | 4 | describe('EventHandlerHelper', () => { 5 | jest.mock('@nestjs/common'); 6 | beforeEach(() => { 7 | jest.spyOn(logger, 'log').mockImplementation(() => null); 8 | jest.spyOn(logger, 'error').mockImplementation(() => null); 9 | jest.spyOn(logger, 'debug').mockImplementation(() => null); 10 | }); 11 | 12 | it('should be callable', () => { 13 | const result = EventHandlerHelper.onEvent( 14 | logger as unknown as Logger, 15 | {}, 16 | {}, 17 | ); 18 | expect(result).toBeTruthy(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/event-store/services/event.handler.helper.ts: -------------------------------------------------------------------------------- 1 | import { IAcknowledgeableEvent } from '../../interfaces'; 2 | import { Logger } from '@nestjs/common'; 3 | import { PersistentSubscriptionNakEventAction } from '../../interfaces/events/persistent-subscription-nak-event-action.enum'; 4 | import { ReadEventBus } from '../../cqrs'; 5 | 6 | export default class EventHandlerHelper { 7 | public static async onEvent( 8 | logger: Logger, 9 | subscription: any, 10 | payload: any, 11 | eventBus?: ReadEventBus, 12 | ): Promise { 13 | // do nothing, as we have not defined an event bus 14 | if (!eventBus) { 15 | return; 16 | } 17 | 18 | // use default onEvent 19 | const { event } = payload; 20 | // TODO allow unresolved event 21 | if (!payload.isResolved) { 22 | logger.warn( 23 | `Ignore unresolved event from stream ${payload.originalStreamId} with ID ${payload.originalEvent.eventId}`, 24 | ); 25 | if (!subscription._autoAck && subscription.hasOwnProperty('_autoAck')) { 26 | await subscription.acknowledge([payload]); 27 | } 28 | return; 29 | } 30 | // TODO handle not JSON 31 | if (!event.isJson) { 32 | // TODO add info on error not coded 33 | logger.warn( 34 | `Received event that could not be resolved! stream ${event.eventStreamId} type ${event.eventType} id ${event.eventId} `, 35 | ); 36 | if (!subscription._autoAck && subscription.hasOwnProperty('_autoAck')) { 37 | await subscription.acknowledge([payload]); 38 | } 39 | return; 40 | } 41 | 42 | // TODO throw error 43 | let data = {}; 44 | try { 45 | data = JSON.parse(event.data.toString()); 46 | } catch (e) { 47 | logger.warn( 48 | `Received event of type ${event.eventType} with shitty data acknowledge`, 49 | ); 50 | if (!subscription._autoAck && subscription.hasOwnProperty('_autoAck')) { 51 | await subscription.acknowledge([payload]); 52 | } 53 | return; 54 | } 55 | 56 | // we do not add default metadata as 57 | // we do not want to modify 58 | // read models 59 | let metadata = {}; 60 | if (event.metadata.toString()) { 61 | metadata = { ...metadata, ...JSON.parse(event.metadata.toString()) }; 62 | } 63 | 64 | const finalEvent = eventBus.map(data, { 65 | metadata, 66 | eventStreamId: event.eventStreamId, 67 | eventId: event.eventId, 68 | eventNumber: event.eventNumber.low, 69 | eventType: event.eventType, 70 | originalEventId: payload.originalEvent.eventId || event.eventId, 71 | }); 72 | 73 | if (!finalEvent) { 74 | logger.warn( 75 | `Received event of type ${event.eventType} with no declared handler acknowledge`, 76 | ); 77 | if (!subscription._autoAck && subscription.hasOwnProperty('_autoAck')) { 78 | await subscription.acknowledge([payload]); 79 | } 80 | return; 81 | } 82 | // If event wants to handle ack/nack 83 | // only for persistent 84 | if (subscription.hasOwnProperty('_autoAck')) { 85 | if ( 86 | typeof finalEvent.ack == 'function' && 87 | typeof finalEvent.nack == 'function' 88 | ) { 89 | const ack = async () => { 90 | logger.debug( 91 | `Acknowledge event ${event.eventType} with id ${event.eventId}`, 92 | ); 93 | return subscription.acknowledge([payload]); 94 | }; 95 | const nack = async ( 96 | action: PersistentSubscriptionNakEventAction, 97 | reason: string, 98 | ) => { 99 | logger.debug( 100 | `Nak and ${ 101 | Object.keys(PersistentSubscriptionNakEventAction)[action] 102 | } for event ${event.eventType} with id ${ 103 | event.eventId 104 | } : reason ${reason}`, 105 | ); 106 | return subscription.fail([payload], action, reason); 107 | }; 108 | 109 | finalEvent.ack = ack; 110 | finalEvent.nack = nack; 111 | } else { 112 | // Otherwise manage here 113 | logger.debug( 114 | `Auto acknowledge event ${event.eventType} with id ${event.eventId}`, 115 | ); 116 | subscription.acknowledge([payload]); 117 | } 118 | } 119 | 120 | // Dispatch to event handlers and sagas 121 | await eventBus.publish(finalEvent); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/event-store/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './errors.constant'; 2 | export * from './event-store.constants'; 3 | export * from './event-store.service.interface'; 4 | export * from './event-store.service'; 5 | export * from './event.handler.helper'; 6 | -------------------------------------------------------------------------------- /src/event-store/subscriptions/index.ts: -------------------------------------------------------------------------------- 1 | export * from './persistent-subscription-config.interface'; 2 | -------------------------------------------------------------------------------- /src/event-store/subscriptions/persistent-subscription-config.interface.ts: -------------------------------------------------------------------------------- 1 | import { DuplexOptions } from 'stream'; 2 | import { PersistentSubscriptionSettings } from '@eventstore/db-client/dist/utils'; 3 | import { ConnectToPersistentSubscriptionOptions } from '@eventstore/db-client/dist/persistentSubscription'; 4 | import { BaseOptions } from '@eventstore/db-client/dist/types'; 5 | 6 | export interface IPersistentSubscriptionConfig { 7 | stream: string; 8 | group: string; 9 | optionsForConnection?: { 10 | subscriptionConnectionOptions?: Partial; 11 | duplexOptions?: Partial; 12 | }; 13 | settingsForCreation?: { 14 | subscriptionSettings?: Partial; 15 | baseOptions?: Partial; 16 | }; 17 | 18 | onSubscriptionStart?: () => void | undefined; 19 | onSubscriptionDropped?: (reason: string, error: string) => void | undefined; 20 | onError?: (error: Error) => void | undefined; 21 | } 22 | -------------------------------------------------------------------------------- /src/exceptions/index.ts: -------------------------------------------------------------------------------- 1 | export * from './invalid-event.exception'; 2 | export * from './invalid-publisher.exception'; 3 | -------------------------------------------------------------------------------- /src/exceptions/invalid-event.exception.ts: -------------------------------------------------------------------------------- 1 | import { HttpException, HttpStatus } from '@nestjs/common'; 2 | 3 | export class InvalidEventException extends HttpException { 4 | constructor(errors: Error[]) { 5 | super(errors, HttpStatus.INTERNAL_SERVER_ERROR); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/exceptions/invalid-publisher.exception.ts: -------------------------------------------------------------------------------- 1 | import { HttpException, HttpStatus } from '@nestjs/common'; 2 | 3 | export class InvalidPublisherException< 4 | T extends object = Function 5 | > extends HttpException { 6 | constructor(publisher: T, method: keyof T) { 7 | super( 8 | `Invalid publisher: expected ${ 9 | publisher.constructor.name + '::' + method 10 | } to be a function`, 11 | HttpStatus.INTERNAL_SERVER_ERROR, 12 | ); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from '@eventstore/db-client/dist/constants'; 2 | export * from './cloudevents'; 3 | export * from './cqrs'; 4 | export * from './decorators'; 5 | export * from './dto'; 6 | export * from './event-store'; 7 | export * from './exceptions'; 8 | export * from './interfaces'; 9 | export * from './tools'; 10 | 11 | export * from './cqrs-event-store.module'; 12 | export * from './constants'; 13 | -------------------------------------------------------------------------------- /src/interfaces/config/event-bus-config.type.ts: -------------------------------------------------------------------------------- 1 | import { ReadEventBusConfigType } from './read-event-bus-config.type'; 2 | import { IWriteEventBusConfig } from './write-event-bus-config.interface'; 3 | 4 | export type EventBusConfigType = { 5 | read?: ReadEventBusConfigType; 6 | write?: IWriteEventBusConfig; 7 | }; 8 | -------------------------------------------------------------------------------- /src/interfaces/config/event-bus-prepublish-config.interface.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common'; 2 | import { IBaseEvent } from '../events'; 3 | import { IEventBusPrepublishValidateProvider } from './event-bus-prepublish-validate-provider.interface'; 4 | import { IEventBusPrepublishPrepareProvider } from './event-bus-prepublish-prepare-provider.interface'; 5 | import { EventBusPrepublishPrepareCallbackType } from './event-bus-prepublish-prepare-callback.type'; 6 | 7 | export interface IEventBusPrepublishConfig { 8 | validate?: 9 | | Type> 10 | | IEventBusPrepublishValidateProvider; 11 | prepare?: 12 | | Type> 13 | | EventBusPrepublishPrepareCallbackType; 14 | } 15 | -------------------------------------------------------------------------------- /src/interfaces/config/event-bus-prepublish-prepare-callback.type.ts: -------------------------------------------------------------------------------- 1 | import { IBaseEvent } from '../events'; 2 | 3 | export type EventBusPrepublishPrepareCallbackType< 4 | T extends IBaseEvent, 5 | K extends IBaseEvent = T 6 | > = (events: T[]) => Promise; 7 | -------------------------------------------------------------------------------- /src/interfaces/config/event-bus-prepublish-prepare-provider.interface.ts: -------------------------------------------------------------------------------- 1 | import { IBaseEvent } from '../events'; 2 | import { EventBusPrepublishPrepareCallbackType } from './event-bus-prepublish-prepare-callback.type'; 3 | 4 | export interface IEventBusPrepublishPrepareProvider< 5 | T extends IBaseEvent, 6 | K extends IBaseEvent = T 7 | > { 8 | prepare: EventBusPrepublishPrepareCallbackType; 9 | } 10 | -------------------------------------------------------------------------------- /src/interfaces/config/event-bus-prepublish-validate-provider.interface.ts: -------------------------------------------------------------------------------- 1 | import { IBaseEvent } from '../events'; 2 | 3 | export interface IEventBusPrepublishValidateProvider { 4 | validate: (events: T[]) => Promise; 5 | onValidationFail: (events: T[], errors: any[]) => void; 6 | } 7 | -------------------------------------------------------------------------------- /src/interfaces/config/index.ts: -------------------------------------------------------------------------------- 1 | export * from './event-bus-config.type'; 2 | export * from './event-bus-prepublish-config.interface'; 3 | export * from './read-event-bus-config.type'; 4 | export * from './write-event-bus-config.interface'; 5 | export * from './event-bus-prepublish-validate-provider.interface'; 6 | export * from './event-bus-prepublish-prepare-provider.interface'; 7 | export * from './event-bus-prepublish-prepare-callback.type'; 8 | -------------------------------------------------------------------------------- /src/interfaces/config/read-event-bus-config.type.ts: -------------------------------------------------------------------------------- 1 | import { ReadEventOptionsType, IReadEvent } from '../events'; 2 | import { IEventBusPrepublishConfig } from './event-bus-prepublish-config.interface'; 3 | import { EventStoreEvent } from '../../event-store'; 4 | 5 | type EventMapperType = ( 6 | data: any, 7 | options: ReadEventOptionsType, 8 | ) => IReadEvent | null; 9 | 10 | type EventConstructorType = new ( 11 | ...args: any[] 12 | ) => T; 13 | 14 | export type ReadEventBusConfigType = 15 | IEventBusPrepublishConfig & 16 | ( 17 | | { 18 | eventMapper: EventMapperType; 19 | allowedEvents?: never; 20 | } 21 | | { 22 | eventMapper?: never; 23 | allowedEvents: { [key: string]: EventConstructorType }; 24 | } 25 | ); 26 | -------------------------------------------------------------------------------- /src/interfaces/config/write-event-bus-config.interface.ts: -------------------------------------------------------------------------------- 1 | import { IEvent } from '@nestjs/cqrs'; 2 | import { Observable } from 'rxjs'; 3 | import { ContextName } from 'nestjs-context'; 4 | import { EventStorePublisher } from '../../event-store'; 5 | import { IEventBusPrepublishConfig } from './event-bus-prepublish-config.interface'; 6 | import { IWriteEvent } from '../events'; 7 | 8 | export interface IWriteEventBusConfig 9 | extends IEventBusPrepublishConfig { 10 | context?: ContextName; 11 | serviceName?: string; 12 | // Handle publish error default do nothing 13 | onPublishFail?: ( 14 | error: Error, 15 | events: IEvent[], 16 | eventStore: EventStorePublisher, 17 | ) => Observable; 18 | } 19 | -------------------------------------------------------------------------------- /src/interfaces/events/acknowledgeable-event.interface.ts: -------------------------------------------------------------------------------- 1 | import { IReadEvent } from './read-event.interface'; 2 | import { PersistentSubscriptionNakEventAction } from './persistent-subscription-nak-event-action.enum'; 3 | 4 | export interface IAcknowledgeableEvent extends IReadEvent { 5 | ack: () => Promise; 6 | nack: ( 7 | action: PersistentSubscriptionNakEventAction, 8 | reason: string, 9 | ) => Promise; 10 | } 11 | -------------------------------------------------------------------------------- /src/interfaces/events/base-event.interface.ts: -------------------------------------------------------------------------------- 1 | import { EventMetadataDto } from '../../dto'; 2 | 3 | export interface IBaseEvent { 4 | data: any; 5 | metadata?: Partial; 6 | eventId?: string; 7 | eventType?: string; 8 | } 9 | -------------------------------------------------------------------------------- /src/interfaces/events/event-options.type.ts: -------------------------------------------------------------------------------- 1 | import { IWriteEvent } from './write-event.interface'; 2 | import { ReadEventOptionsType } from './read-event-options.type'; 3 | 4 | type WriteEventOptionsType = Omit & { 5 | eventStreamId?: never; 6 | eventNumber?: never; 7 | originalEventId?: never; 8 | }; 9 | 10 | export type EventOptionsType = ReadEventOptionsType | WriteEventOptionsType; 11 | -------------------------------------------------------------------------------- /src/interfaces/events/index.ts: -------------------------------------------------------------------------------- 1 | export * from './acknowledgeable-event.interface'; 2 | export * from './base-event.interface'; 3 | export * from './event-options.type'; 4 | export * from './publication-context.interface'; 5 | export * from './read-event.interface'; 6 | export * from './read-event-options.type'; 7 | export * from './write-event.interface'; 8 | -------------------------------------------------------------------------------- /src/interfaces/events/persistent-subscription-nak-event-action.enum.ts: -------------------------------------------------------------------------------- 1 | export enum PersistentSubscriptionNakEventAction { 2 | Unknown = 0, 3 | Park = 1, 4 | Retry = 2, 5 | Skip = 3, 6 | Stop = 4, 7 | } 8 | -------------------------------------------------------------------------------- /src/interfaces/events/publication-context.interface.ts: -------------------------------------------------------------------------------- 1 | import { StreamMetadata } from '@eventstore/db-client/dist/utils/streamMetadata'; 2 | import { SetStreamMetadataOptions } from '@eventstore/db-client/dist/streams'; 3 | import { AppendExpectedRevision } from '@eventstore/db-client/dist/types'; 4 | 5 | export interface PublicationContextInterface { 6 | streamName?: string; 7 | expectedRevision?: AppendExpectedRevision; 8 | streamMetadata?: StreamMetadata; 9 | options?: SetStreamMetadataOptions; 10 | } 11 | -------------------------------------------------------------------------------- /src/interfaces/events/read-event-options.type.ts: -------------------------------------------------------------------------------- 1 | import { IReadEvent } from './read-event.interface'; 2 | 3 | export type ReadEventOptionsType = Omit< 4 | IReadEvent, 5 | 'data' | 'getStream' | 'getStreamCategory' | 'getStreamId' 6 | >; 7 | -------------------------------------------------------------------------------- /src/interfaces/events/read-event.interface.ts: -------------------------------------------------------------------------------- 1 | import { IBaseEvent } from './base-event.interface'; 2 | 3 | export interface IReadEvent extends IBaseEvent { 4 | eventStreamId: string; 5 | eventNumber: number; 6 | originalEventId: string; 7 | getStream(): string; 8 | getStreamCategory(): string; 9 | getStreamId(): string; 10 | } 11 | -------------------------------------------------------------------------------- /src/interfaces/events/write-event.interface.ts: -------------------------------------------------------------------------------- 1 | import { IBaseEvent } from './base-event.interface'; 2 | 3 | export interface IWriteEvent extends IBaseEvent {} 4 | -------------------------------------------------------------------------------- /src/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './config'; 2 | export * from './events'; 3 | 4 | export * from './projection.type'; 5 | export * from './write-event-bus.interface'; 6 | -------------------------------------------------------------------------------- /src/interfaces/projection.type.ts: -------------------------------------------------------------------------------- 1 | export type EventStoreProjection = { 2 | name: string; 3 | content?: string; 4 | file?: string; 5 | 6 | mode?: 'oneTime' | 'continuous' | 'transient'; 7 | trackEmittedStreams?: boolean; 8 | enabled?: boolean; 9 | checkPointsEnabled?: boolean; 10 | emitEnabled?: boolean; 11 | }; 12 | -------------------------------------------------------------------------------- /src/interfaces/write-event-bus.interface.ts: -------------------------------------------------------------------------------- 1 | import { WriteEventBus } from '../cqrs'; 2 | 3 | export interface IWriteEventBus extends WriteEventBus {} 4 | -------------------------------------------------------------------------------- /src/tools/create-event-default-metadata.ts: -------------------------------------------------------------------------------- 1 | import { EventMetadataDto } from '../dto'; 2 | 3 | export const createEventDefaultMetadata = () => 4 | ({ 5 | time: new Date().toISOString(), 6 | version: 1, 7 | } as Partial); 8 | -------------------------------------------------------------------------------- /src/tools/index.ts: -------------------------------------------------------------------------------- 1 | export * from './create-event-default-metadata'; 2 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "target": "ES2020", 9 | "sourceMap": false, 10 | "outDir": "./dist", 11 | "rootDir": "./src", 12 | "baseUrl": "./", 13 | "noLib": false 14 | }, 15 | "include": ["src/**/*.ts"], 16 | "exclude": ["node_modules", "./dist"] 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "declaration": true, 6 | "removeComments": true, 7 | "emitDecoratorMetadata": true, 8 | "experimentalDecorators": true, 9 | "target": "ES2020", 10 | "sourceMap": true, 11 | "rootDir": "./src", 12 | "baseUrl": "./", 13 | "noLib": false 14 | }, 15 | "include": ["src/**/*.spec.ts"], 16 | "exclude": ["node_modules"] 17 | } 18 | -------------------------------------------------------------------------------- /upgrade.md: -------------------------------------------------------------------------------- 1 | # Updating connector, what's new 2 | 3 | ## Version 5.0.0 4 | 5 | The connector is updated, and the deprecated version of EventStore client is not maintained anymore. 6 | 7 | 1. EventStore Client 8 | 9 | The usage is switching to the official 10 | client [EventStore client](https://developers.eventstore.com/clients/grpc/getting-started/) 11 | 12 | 2. App startup 13 | 14 | Connecting to the module is now done only by providing a conf that you can find here : 15 | 16 | [src/event-store/config/event-store-connection-config.ts](./src/event-store/config/event-store-connection-config.ts) 17 | 18 | The conf and all the objects are now (or willing to be) strongly typed, so it's easy to know what option is needed. 19 | 20 | 3. EventStore configuration 21 | 22 | You can give a strongly typed conf representing the persistent subscriptions and the projections, at the startup : 23 | 24 | [src/event-store/config/event-store-service-config.interface.ts](./src/event-store/config/event-store-service-config.interface.ts) 25 | 26 | The main diff is that we have now to fill it with a creation conf and a connection conf. They are not the same anymore ( 27 | in order to match the es client's process). 28 | 29 | 4. Methods signature 30 | 31 | Some EventStoreService methods have see there signature changed to suit at best the interfaces of the new client : 32 | 33 | [src/event-store/services/event-store.service.interface.ts](./src/event-store/services/event-store.service.interface.ts) 34 | 35 | 6. Exemple : how to connect 36 | 37 | First step : prepare your eventstore connection configuration, like this : 38 | 39 | ```typescript 40 | const eventStoreConnectionConfig: EventStoreConnectionConfig = { 41 | connectionSettings: { 42 | connectionString: 43 | process.env.CONNECTION_STRING || 'esdb://localhost:20113?tls=false', 44 | }, 45 | defaultUserCredentials: { 46 | username: process.env.EVENTSTORE_CREDENTIALS_USERNAME || 'admin', 47 | password: process.env.EVENTSTORE_CREDENTIALS_PASSWORD || 'changeit', 48 | }, 49 | }; 50 | ``` 51 | 52 | Then, you must provide the list of subsystems you want to configure (projections/persistent subscriptions) : 53 | 54 | ```typescript 55 | const eventStoreSubsystems: IEventStoreSubsystems = { 56 | subscriptions: { 57 | persistent: [ 58 | { 59 | stream: '$ce-hero', 60 | group: 'data', 61 | settingsForCreation: { 62 | subscriptionSettings: { 63 | resolveLinkTos: true, 64 | minCheckpointCount: 1, 65 | }, 66 | }, 67 | onError: (err: Error) => 68 | console.log(`An error occured : ${err.name}, ${err.message}`), 69 | }, 70 | ], 71 | }, 72 | projections: [ 73 | { 74 | name: 'hero-dragon', 75 | file: resolve(`${__dirname}/projections/hero-dragon.js`), 76 | mode: 'continuous', 77 | enabled: true, 78 | checkPointsEnabled: true, 79 | emitEnabled: true, 80 | }, 81 | ], 82 | onConnectionFail: (err: Error) => 83 | console.log(`Connection to Event store hooked : ${err}`), 84 | }; 85 | ``` 86 | 87 | **Note on error callbacks** 88 | 89 | - the onError callback will be triggered when the subscription will face a unexpected issue (for example : EventStore 90 | connection is closed). This allows you to stack your events/do anything else. 91 | - Same, you have now a `onConnectionFail` that you can give to the conf. This will be triggered if the connection to EventStore is failing, while you try to write event(s). 92 | 93 | Again, all of these configurations are strongly typed, you can check the interfaces to know what options are needed or 94 | not. 95 | 96 | Note: for creation options, even if you do not provide all options needed, the system will fill it with default values, 97 | given by calling the `persistentSubscriptionSettingsFromDefaults` method provided by the client lib. 98 | 99 | Then, one last step, you have to provide the readBus and writeBus options, like previously. 100 | 101 | In your module, you then have to import the `CqrsEventStoreModule` like this way : 102 | 103 | ```typescript 104 | @Module({ 105 | controllers: [ 106 | // ... OtherControllers 107 | ], 108 | providers: [ 109 | // ... OtherProviders 110 | ], 111 | imports: [ 112 | OtherModules, 113 | CqrsEventStoreModule.register( 114 | eventStoreConnectionConfig, 115 | eventStoreSubsystems, 116 | eventBusConfig, 117 | ), 118 | ], 119 | }) 120 | export class YourCoolFeatureModule {} 121 | ``` 122 | 123 | Because the connection is at module init, once the app is started, all the projections and persistent subscriptions 124 | provided are asserted. You can get the subscriptions by this way : 125 | 126 | ```typescript 127 | EventStoreService.getPersistentSubscriptions(); 128 | ``` 129 | 130 | ### Update with command handlers and the aggregate 131 | 132 | The main difference with the command handlers concerns the commit parameter. Now, the signature is this one : 133 | 134 | ```typescript 135 | interface ExampleAggregate { 136 | commit( 137 | expectedRevision: AppendExpectedRevision = constants.ANY, 138 | expectedMetadataRevision: AppendExpectedRevision = constants.ANY, 139 | ); 140 | } 141 | ``` 142 | 143 | The constants are findable here : 144 | 145 | [@eventstore/db-client/dist/constants.d.ts](./node_modules/@eventstore/db-client/dist/constants.d.ts) 146 | 147 | The possibilities are : 148 | 149 | `constants.NO_STREAM | constants.STREAM_EXISTS | constants.ANY | bigint;` 150 | 151 | Note that if you want to declare a bigint at this place, you have to do like this : 152 | 153 | ```typescript 154 | const veryBigInt: bigint = BigInt(1234); 155 | ``` 156 | 157 | In the command handler, when you want to commit, you should then provide the right value for expected versions. 158 | 159 | ### Failing strategy 160 | 161 | By default, the failing strategy keeps in memory the event batches, while the connection to the store is down. Each time you try to write events, the system will try to rewrite all the events stacked in the right order with the correct stream and expected revision. 162 | 163 | You may want to override this mechanism. To do so, the only thing you have to provide is `EVENT_STACKER` service, like this in your module : 164 | 165 | ```typescript 166 | { 167 | provide: EVENT_AND_METADATAS_STACKER, 168 | useClass: InMemoryEventsAndMetadatasStacker 169 | } 170 | ``` 171 | 172 | `InMemoryEventsAndMetadatasStacker` is the devault value. you juste have to add a service that matches the interface [IEventsAndMetadatasStacker](src/event-store/reliability/interface/events-and-metadatas-stacker.ts) 173 | 174 | Note that it works the same way for the metadatas. 175 | --------------------------------------------------------------------------------