├── .commitlintrc.js ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── commit-lint.yml │ ├── publish-next.yml │ └── test.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .istanbul.yml ├── LICENSE ├── README.md ├── SECURITY.md ├── assets ├── cert.pem ├── img │ ├── flow.png │ ├── logo.png │ └── logo.svg ├── index.html ├── key.pem └── media │ └── audio.mp3 ├── benchmarks ├── Makefile ├── middlewares.js ├── routing.js └── run ├── codecov.yml ├── docker-compose.yml ├── jest.config.js ├── lerna.json ├── package.json ├── packages ├── @integration │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── cqrs │ │ │ ├── __mock__ │ │ │ │ └── __mock__dependencies.ts │ │ │ ├── domain │ │ │ │ ├── Offer.command.ts │ │ │ │ └── Offer.event.ts │ │ │ ├── effects │ │ │ │ ├── eventbus.effects.ts │ │ │ │ └── http.effects.ts │ │ │ └── index.ts │ │ ├── http │ │ │ ├── effects │ │ │ │ ├── api.effects.ts │ │ │ │ ├── static.effects.ts │ │ │ │ └── user.effects.ts │ │ │ ├── fakes │ │ │ │ ├── auth.fake.ts │ │ │ │ ├── dao.fake.ts │ │ │ │ └── random.ts │ │ │ ├── index.ts │ │ │ └── middlewares │ │ │ │ ├── auth.middleware.ts │ │ │ │ └── cors.middleware.ts │ │ ├── messaging │ │ │ ├── client.ts │ │ │ └── server.ts │ │ └── websockets │ │ │ ├── http.server.ts │ │ │ └── websockets.server.ts │ ├── test │ │ ├── http.integration.spec.ts │ │ └── messaging.integration.spec.ts │ └── tsconfig.json ├── README.md ├── core │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── +internal │ │ │ ├── files │ │ │ │ ├── fileReader.helper.spec.ts │ │ │ │ ├── fileReader.helper.ts │ │ │ │ └── index.ts │ │ │ ├── fp │ │ │ │ ├── IxBuilder.spec.ts │ │ │ │ ├── IxBuilder.ts │ │ │ │ └── index.ts │ │ │ ├── observable │ │ │ │ ├── fromReadableStream.spec.ts │ │ │ │ ├── fromReadableStream.ts │ │ │ │ └── index.ts │ │ │ ├── testing │ │ │ │ ├── index.ts │ │ │ │ └── marbles.helper.ts │ │ │ └── utils │ │ │ │ ├── any.util.ts │ │ │ │ ├── array.util.ts │ │ │ │ ├── env.util.ts │ │ │ │ ├── error.util.ts │ │ │ │ ├── index.ts │ │ │ │ ├── spec │ │ │ │ ├── any.util.spec.ts │ │ │ │ ├── array.util.spec.ts │ │ │ │ ├── env.util.spec.ts │ │ │ │ ├── error.util.spec.ts │ │ │ │ └── string.util.spec.ts │ │ │ │ ├── stream.util.ts │ │ │ │ ├── string.util.ts │ │ │ │ └── type.util.ts │ │ ├── context │ │ │ ├── context.helper.ts │ │ │ ├── context.hook.ts │ │ │ ├── context.logger.ts │ │ │ ├── context.reader.factory.ts │ │ │ ├── context.token.factory.ts │ │ │ ├── context.ts │ │ │ └── specs │ │ │ │ ├── context.helper.spec.ts │ │ │ │ ├── context.hook.spec.ts │ │ │ │ └── context.spec.ts │ │ ├── effects │ │ │ ├── effects.combiner.spec.ts │ │ │ ├── effects.combiner.ts │ │ │ ├── effects.interface.ts │ │ │ └── effectsContext.factory.ts │ │ ├── error │ │ │ ├── error.factory.ts │ │ │ ├── error.model.ts │ │ │ └── specs │ │ │ │ ├── error.factory.spec.ts │ │ │ │ └── error.model.spec.ts │ │ ├── event │ │ │ ├── event.factory.ts │ │ │ ├── event.interface.ts │ │ │ ├── event.spec.ts │ │ │ └── event.ts │ │ ├── index.spec.ts │ │ ├── index.ts │ │ ├── listener │ │ │ ├── listener.factory.ts │ │ │ └── listener.interface.ts │ │ ├── logger │ │ │ ├── index.ts │ │ │ ├── logger.interface.ts │ │ │ ├── logger.token.ts │ │ │ └── logger.ts │ │ └── operators │ │ │ ├── act │ │ │ ├── act.operator.spec.ts │ │ │ └── act.operator.ts │ │ │ ├── index.ts │ │ │ ├── matchEvent │ │ │ ├── matchEvent.operator.spec.ts │ │ │ └── matchEvent.operator.ts │ │ │ └── use │ │ │ └── use.operator.ts │ └── tsconfig.json ├── http │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── +internal │ │ │ ├── contentType.util.spec.ts │ │ │ ├── contentType.util.ts │ │ │ ├── header.util.spec.ts │ │ │ ├── header.util.ts │ │ │ ├── metadata.util.ts │ │ │ ├── server.util.spec.ts │ │ │ ├── server.util.ts │ │ │ ├── testing.util.ts │ │ │ └── urlEncoded.util.ts │ │ ├── effects │ │ │ ├── http.effects.interface.ts │ │ │ └── http.effects.ts │ │ ├── error │ │ │ ├── http.error.effect.spec.ts │ │ │ ├── http.error.effect.ts │ │ │ ├── http.error.model.spec.ts │ │ │ └── http.error.model.ts │ │ ├── http.config.ts │ │ ├── http.interface.ts │ │ ├── index.ts │ │ ├── response │ │ │ ├── http.responseBody.factory.ts │ │ │ ├── http.responseHandler.ts │ │ │ ├── http.responseHeaders.factory.ts │ │ │ └── specs │ │ │ │ ├── http.responseBody.factory.spec.ts │ │ │ │ ├── http.responseHandler.spec.ts │ │ │ │ └── http.responseHeaders.factory.spec.ts │ │ ├── router │ │ │ ├── http.router.combiner.ts │ │ │ ├── http.router.effects.ts │ │ │ ├── http.router.factory.ts │ │ │ ├── http.router.helpers.ts │ │ │ ├── http.router.interface.ts │ │ │ ├── http.router.ixbuilder.ts │ │ │ ├── http.router.matcher.ts │ │ │ ├── http.router.params.factory.ts │ │ │ ├── http.router.query.factory.ts │ │ │ ├── http.router.resolver.ts │ │ │ └── specs │ │ │ │ ├── http.router.combiner.spec.ts │ │ │ │ ├── http.router.factory.spec.ts │ │ │ │ ├── http.router.helper.spec.ts │ │ │ │ ├── http.router.ixbuilder.spec.ts │ │ │ │ ├── http.router.params.factory.spec.ts │ │ │ │ ├── http.router.query.factory.spec.ts │ │ │ │ └── http.router.resolver.spec.ts │ │ └── server │ │ │ ├── http.server.event.ts │ │ │ ├── http.server.interface.ts │ │ │ ├── http.server.listener.ts │ │ │ ├── http.server.spec.ts │ │ │ ├── http.server.ts │ │ │ └── internal-dependencies │ │ │ ├── httpRequestBus.reader.ts │ │ │ ├── httpRequestMetadataStorage.reader.spec.ts │ │ │ ├── httpRequestMetadataStorage.reader.ts │ │ │ ├── httpServerClient.reader.ts │ │ │ ├── httpServerEventStream.reader.spec.ts │ │ │ └── httpServerEventStream.reader.ts │ └── tsconfig.json ├── messaging │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── +internal │ │ │ └── testing │ │ │ │ ├── index.ts │ │ │ │ └── messaging.testBed.ts │ │ ├── ack │ │ │ ├── ack.spec.ts │ │ │ └── ack.ts │ │ ├── client │ │ │ └── messaging.client.ts │ │ ├── effects │ │ │ └── messaging.effects.interface.ts │ │ ├── eventStore │ │ │ ├── eventTimerStore.spec.ts │ │ │ └── eventTimerStore.ts │ │ ├── eventbus │ │ │ ├── messaging.eventBus.reader.spec.ts │ │ │ ├── messaging.eventBus.reader.ts │ │ │ └── messaging.eventBusClient.reader.ts │ │ ├── index.spec.ts │ │ ├── index.ts │ │ ├── middlewares │ │ │ ├── messaging.ack.middleware.ts │ │ │ ├── messaging.eventInput.middleware.ts │ │ │ ├── messaging.eventLogger.middleware.spec.ts │ │ │ ├── messaging.eventLogger.middleware.ts │ │ │ ├── messaging.eventOutput.middleware.spec.ts │ │ │ ├── messaging.eventOutput.middleware.ts │ │ │ └── messaging.statusLogger.middleware.ts │ │ ├── reply │ │ │ ├── reply.spec.ts │ │ │ └── reply.ts │ │ ├── server │ │ │ ├── messaging.server.events.ts │ │ │ ├── messaging.server.interface.ts │ │ │ ├── messaging.server.listener.ts │ │ │ ├── messaging.server.tokens.ts │ │ │ ├── messaging.server.ts │ │ │ └── specs │ │ │ │ ├── messaging.server.amqp.spec.ts │ │ │ │ └── messaging.server.redis.spec.ts │ │ ├── transport │ │ │ ├── strategies │ │ │ │ ├── amqp.strategy.interface.ts │ │ │ │ ├── amqp.strategy.ts │ │ │ │ ├── local.strategy.interface.ts │ │ │ │ ├── local.strategy.ts │ │ │ │ ├── redis.strategy.helper.ts │ │ │ │ ├── redis.strategy.interface.ts │ │ │ │ ├── redis.strategy.ts │ │ │ │ ├── tcp.strategy.interface.ts │ │ │ │ └── tcp.strategy.ts │ │ │ ├── transport.error.ts │ │ │ ├── transport.interface.ts │ │ │ ├── transport.provider.spec.ts │ │ │ ├── transport.provider.ts │ │ │ ├── transport.transformer.spec.ts │ │ │ └── transport.transformer.ts │ │ └── util │ │ │ └── messaging.test.util.ts │ └── tsconfig.json ├── middleware-body │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── body.middleware.ts │ │ ├── body.model.ts │ │ ├── body.util.ts │ │ ├── index.ts │ │ ├── parsers │ │ │ ├── default.body.parser.ts │ │ │ ├── index.ts │ │ │ ├── json.body.parser.ts │ │ │ ├── raw.body.parser.ts │ │ │ ├── specs │ │ │ │ └── default.body.parser.spec.ts │ │ │ ├── text.body.parser.ts │ │ │ └── url.body.parser.ts │ │ └── specs │ │ │ ├── body.middleware.spec.ts │ │ │ ├── body.util.spec.ts │ │ │ └── index.spec.ts │ ├── test │ │ ├── bodyParser.integration.spec.ts │ │ └── bodyParser.integration.ts │ └── tsconfig.json ├── middleware-cors │ ├── LICENSE │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── applyHeaders.ts │ │ ├── checkArrayOrigin.ts │ │ ├── checkOrigin.ts │ │ ├── checkRegexpOrigin.ts │ │ ├── checkStringOrigin.ts │ │ ├── configurePreflightResponse.ts │ │ ├── configureResponse.ts │ │ ├── index.ts │ │ ├── middleware.ts │ │ ├── spec │ │ │ ├── applyHeaders.spec.ts │ │ │ ├── checkArrayOrigin.spec.ts │ │ │ ├── checkOrigin.spec.ts │ │ │ ├── checkRegexpOrigin.spec.ts │ │ │ ├── checkStringOrigin.spec.ts │ │ │ ├── configurePreflightResponse.spec.ts │ │ │ ├── configureResponse.spec.ts │ │ │ ├── index.spec.ts │ │ │ ├── middleware.spec.ts │ │ │ └── util.spec.ts │ │ └── util.ts │ └── tsconfig.json ├── middleware-io │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── io.error.ts │ │ ├── io.event.middleware.ts │ │ ├── io.json-schema.ts │ │ ├── io.middleware.ts │ │ ├── io.reporter.ts │ │ ├── io.request.middleware.ts │ │ └── specs │ │ │ ├── index.spec.ts │ │ │ ├── io.error.spec.ts │ │ │ ├── io.event.middleware.spec.ts │ │ │ ├── io.json-schema.spec.ts │ │ │ ├── io.middleware.spec.ts │ │ │ ├── io.reporter.spec.ts │ │ │ └── io.request.middleware.spec.ts │ ├── test │ │ ├── io-http.integration.spec.ts │ │ ├── io-http.integration.ts │ │ ├── io-ws.integration.spec.ts │ │ └── io-ws.integration.ts │ └── tsconfig.json ├── middleware-logger │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── logger.factory.ts │ │ ├── logger.handler.ts │ │ ├── logger.middleware.ts │ │ ├── logger.model.ts │ │ ├── logger.util.ts │ │ └── spec │ │ │ ├── index.spec.ts │ │ │ ├── logger.factory.spec.ts │ │ │ ├── logger.middleware.spec.ts │ │ │ └── logger.util.spec.ts │ └── tsconfig.json ├── middleware-multipart │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── multipart.interface.ts │ │ ├── multipart.middleware.ts │ │ ├── multipart.parser.field.ts │ │ ├── multipart.parser.file.ts │ │ ├── multipart.parser.ts │ │ ├── multipart.util.ts │ │ └── specs │ │ │ ├── index.spec.ts │ │ │ ├── multipart.middleware.spec.ts │ │ │ └── multipart.util.spec.ts │ └── tsconfig.json ├── testing │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── index.spec.ts │ │ ├── index.ts │ │ └── testBed │ │ │ ├── http │ │ │ ├── http.testBed.interface.ts │ │ │ ├── http.testBed.request.interface.ts │ │ │ ├── http.testBed.request.spec.ts │ │ │ ├── http.testBed.request.ts │ │ │ ├── http.testBed.response.interface.ts │ │ │ ├── http.testBed.response.ts │ │ │ ├── http.testBed.spec.ts │ │ │ └── http.testBed.ts │ │ │ ├── testBed.interface.ts │ │ │ ├── testBedContainer.interface.ts │ │ │ ├── testBedContainer.spec.ts │ │ │ ├── testBedContainer.ts │ │ │ ├── testBedSetup.interface.ts │ │ │ ├── testBedSetup.spec.ts │ │ │ └── testBedSetup.ts │ └── tsconfig.json └── websockets │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ ├── +internal │ │ ├── index.ts │ │ └── websocket.test.util.ts │ ├── effects │ │ └── websocket.effects.interface.ts │ ├── error │ │ ├── specs │ │ │ ├── websocket.error.effect.spec.ts │ │ │ └── websocket.error.model.spec.ts │ │ ├── websocket.error.effect.ts │ │ └── websocket.error.model.ts │ ├── index.spec.ts │ ├── index.ts │ ├── middlewares │ │ ├── websockets.eventLogger.middleware.ts │ │ └── websockets.statusLogger.middleware.ts │ ├── operators │ │ ├── broadcast │ │ │ ├── websocket.broadcast.operator.spec.ts │ │ │ └── websocket.broadcast.operator.ts │ │ ├── index.ts │ │ └── mapToServer │ │ │ ├── websocket.mapToServer.operator.spec.ts │ │ │ └── websocket.mapToServer.operator.ts │ ├── server │ │ ├── specs │ │ │ ├── websocket.server.event.spec.ts │ │ │ ├── websocket.server.helper.spec.ts │ │ │ └── websocket.server.spec.ts │ │ ├── websocket.server.event.subscriber.ts │ │ ├── websocket.server.event.ts │ │ ├── websocket.server.helper.ts │ │ ├── websocket.server.interface.ts │ │ ├── websocket.server.listener.ts │ │ ├── websocket.server.response.ts │ │ └── websocket.server.ts │ ├── transformer │ │ ├── websocket.json.transformer.spec.ts │ │ ├── websocket.json.transformer.ts │ │ └── websocket.transformer.interface.ts │ └── websocket.interface.ts │ └── tsconfig.json ├── scripts ├── run-tests.sh ├── test-helpers.js ├── wait-rabbitmq.js └── wait-redis.js ├── tsconfig.json ├── tsconfig.test.json └── yarn.lock /.commitlintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | '@commitlint/config-conventional', 4 | ], 5 | rules: { 6 | 'header-max-length': [0, 'always', 120], 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | quote_type = single 11 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | *.d.ts 2 | *.js 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | plugins: [ 4 | '@typescript-eslint', 5 | 'deprecation', 6 | ], 7 | extends: [ 8 | 'plugin:@typescript-eslint/recommended', 9 | 'plugin:import/errors', 10 | 'plugin:import/warnings', 11 | 'plugin:import/typescript', 12 | ], 13 | parserOptions: { 14 | ecmaVersion: 2020, 15 | sourceType: 'module', 16 | project: './tsconfig.json', 17 | }, 18 | rules: { 19 | '@typescript-eslint/indent': 0, 20 | '@typescript-eslint/no-explicit-any': 0, 21 | '@typescript-eslint/no-object-literal-type-assertion': 0, 22 | '@typescript-eslint/no-parameter-properties': 0, 23 | '@typescript-eslint/no-empty-interface': 0, 24 | '@typescript-eslint/no-unused-vars': ['error', { 'argsIgnorePattern': '^_' }], 25 | '@typescript-eslint/no-use-before-define': 0, 26 | '@typescript-eslint/prefer-interface': 0, 27 | '@typescript-eslint/explicit-function-return-type': 0, 28 | '@typescript-eslint/explicit-member-accessibility': 0, 29 | '@typescript-eslint/explicit-module-boundary-types': 0, 30 | '@typescript-eslint/no-var-requires': 0, 31 | 'deprecation/deprecation': 'warn', 32 | 'quotes': ['error', 'single', { allowTemplateLiterals: true }], 33 | 'semi': ['error'], 34 | 'import/order': ['error', { 35 | 'groups': [ 36 | 'builtin', 37 | 'external', 38 | 'internal', 39 | 'parent', 40 | 'sibling', 41 | 'index', 42 | ], 43 | }], 44 | }, 45 | }; 46 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior. 12 | 13 | **Expected behavior** 14 | A clear and concise description of what you expected to happen. 15 | 16 | **Desktop (please complete the following information):** 17 | - OS 18 | - Package + Version 19 | - Node version 20 | 21 | **Additional context** 22 | Add any other context about the problem here. 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## PR Type 2 | What kind of change does this PR introduce? 3 | 4 | 5 | ``` 6 | [ ] Bugfix 7 | [ ] Feature 8 | [ ] Code style update (formatting, local variables) 9 | [ ] Refactoring (no functional changes, no api changes) 10 | [ ] Build related changes 11 | [ ] CI related changes 12 | [ ] Documentation content changes 13 | [ ] Other... Please describe: 14 | ``` 15 | 16 | ## What is the current behavior? 17 | 18 | 19 | Issue Number: N/A 20 | 21 | 22 | ## What is the new behavior? 23 | 24 | 25 | ## Does this PR introduce a breaking change? 26 | ``` 27 | [ ] Yes 28 | [ ] No 29 | ``` 30 | 31 | 32 | 33 | 34 | ## Other information 35 | -------------------------------------------------------------------------------- /.github/workflows/commit-lint.yml: -------------------------------------------------------------------------------- 1 | name: Commitlint 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | env: 9 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 10 | steps: 11 | - uses: actions/checkout@v2 12 | with: 13 | fetch-depth: 0 14 | - uses: wagoid/commitlint-github-action@v1 15 | with: 16 | configFile: .commitlintrc.js 17 | -------------------------------------------------------------------------------- /.github/workflows/publish-next.yml: -------------------------------------------------------------------------------- 1 | name: Publish canary "next" build 2 | 3 | on: 4 | push: 5 | branches: 6 | - next 7 | 8 | jobs: 9 | publish: 10 | name: Build and publish 11 | runs-on: ubuntu-18.04 12 | steps: 13 | - uses: actions/checkout@v1 14 | - name: Use Node.js 12 15 | uses: actions/setup-node@v1 16 | with: 17 | node-version: 12 18 | - name: Build packages & publish to npm registry 19 | run: | 20 | yarn 21 | npm config set //registry.npmjs.org/:_authToken=$NPM_TOKEN 22 | yarn clean 23 | yarn build 24 | yarn test 25 | npm run publish:canary:major:github 26 | env: 27 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 28 | 29 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test suite 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | - next 8 | 9 | jobs: 10 | test: 11 | name: Test 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | matrix: 15 | node_version: [8, 10, 12] 16 | os: [ubuntu-latest] 17 | 18 | steps: 19 | - uses: actions/checkout@v1 20 | - name: Use Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v1 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | - name: Install dependencies 25 | run: yarn 26 | - name: Build packages 27 | run: yarn build 28 | - name: Lint code 29 | run: yarn lint 30 | - name: Run unit tests 31 | run: yarn test:unit 32 | - name: Run integration tests 33 | run: yarn test:integration 34 | - name: Upload coverage to Codecov 35 | uses: codecov/codecov-action@v1 36 | with: 37 | token: ${{secrets.CODECOV_TOKEN}} 38 | fail_ci_if_error: false 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # -------------------------------------------------- 2 | # IDE specific 3 | # -------------------------------------------------- 4 | .vscode 5 | .idea 6 | 7 | # -------------------------------------------------- 8 | # OS specific 9 | # -------------------------------------------------- 10 | .DS_Store 11 | 12 | # -------------------------------------------------- 13 | # Logs 14 | # -------------------------------------------------- 15 | logs 16 | *.log 17 | npm-debug.log* 18 | yarn-debug.log* 19 | yarn-error.log* 20 | 21 | # -------------------------------------------------- 22 | # Build artifacts 23 | # -------------------------------------------------- 24 | build 25 | dist 26 | doc 27 | coverage 28 | tmp 29 | package-lock.json 30 | tsconfig.tsbuildinfo 31 | *.tgz 32 | 33 | # -------------------------------------------------- 34 | # remove from npm package 35 | # -------------------------------------------------- 36 | packages/**/*.spec.js 37 | 38 | # -------------------------------------------------- 39 | # Dependency directories 40 | # -------------------------------------------------- 41 | node_modules 42 | 43 | # -------------------------------------------------- 44 | # Typescript v1 declaration files 45 | # -------------------------------------------------- 46 | typings/ 47 | 48 | # -------------------------------------------------- 49 | # Optional cache 50 | # -------------------------------------------------- 51 | .npm 52 | .eslintcache 53 | 54 | # -------------------------------------------------- 55 | # Optional REPL history 56 | # -------------------------------------------------- 57 | .node_repl_history 58 | 59 | # -------------------------------------------------- 60 | # dotenv environment variables file 61 | # -------------------------------------------------- 62 | .env 63 | env.ts 64 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn commitlint --edit "$1" 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn lint 5 | -------------------------------------------------------------------------------- /.istanbul.yml: -------------------------------------------------------------------------------- 1 | reporting: 2 | watermarks: 3 | statements: [99, 100] 4 | lines: [99, 100] 5 | functions: [99, 100] 6 | branches: [99, 100] 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Jozef Flakus 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 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | 3.x | :white_check_mark: | 8 | | 2.3.x | :white_check_mark: | 9 | | < 2.0 | :x: | 10 | 11 | ## Reporting a Vulnerability 12 | 13 | You can report a potential vulnerability by creating a [new issue](https://github.com/marblejs/marble/issues). 14 | -------------------------------------------------------------------------------- /assets/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDlDCCAnwCCQD2/UZrTGhrsDANBgkqhkiG9w0BAQUFADCBizELMAkGA1UEBhMC 3 | UEwxEDAOBgNVBAgMB1NpbGVzaWExDTALBgNVBAcMBGNpdHkxDzANBgNVBAoMBk1h 4 | cmJsZTEQMA4GA1UECwwHc2VjdGlvbjEPMA0GA1UEAwwGbWFyYmxlMScwJQYJKoZI 5 | hvcNAQkBFhhqb3plZi5mbGFrdXNAamZsYWt1cy5jb20wHhcNMTkwMjE3MTYxOTE5 6 | WhcNMTkwMzE5MTYxOTE5WjCBizELMAkGA1UEBhMCUEwxEDAOBgNVBAgMB1NpbGVz 7 | aWExDTALBgNVBAcMBGNpdHkxDzANBgNVBAoMBk1hcmJsZTEQMA4GA1UECwwHc2Vj 8 | dGlvbjEPMA0GA1UEAwwGbWFyYmxlMScwJQYJKoZIhvcNAQkBFhhqb3plZi5mbGFr 9 | dXNAamZsYWt1cy5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCi 10 | vRsY4oDz1DX9l0cuy2Dgn/mk27ku4afaqhp7oAlHjoeVsbFAdPLqHRq735BwwXqa 11 | 5I59Pbq7EBSS+x144eLqm7yVB5QqqSTQUYdl90fOcFI8AarNPXADlaMh23RERGVK 12 | 9yHPK9KoqglgiBcdOsb9FX1nauidje6O1WHkmOxLV10lIgH8xwODxySmN73UFH+3 13 | xEYPfSq0bm1ExFNqj+QECEyWetwaTNHFXhDm9oVkuOZiSuxgogKbN074H5zzO4I8 14 | HKwlaPbYxeEhP4Jtv5AJQpRJ+XApWQ21ZthiJUzDomVayrl1OMKNsr4vLqVlOK9X 15 | nq4MkDAKgtojsm3/AOZPAgMBAAEwDQYJKoZIhvcNAQEFBQADggEBAJXWhbfOR6OV 16 | mcpFW7YQnMYkzKvFPBSNO9TCyFptrG3EDLBV/FL/aARq9exjNk3+UG0YIoJggcvm 17 | GpqQofBK13K5ADrXNj/ldSVVagrKGqDzJ8IySGA8UTW3QyFgH2cHy5bYwvZdYMQg 18 | XVL7FI8Q1oz/SYEYn+4ZCmukC5rLqO1qQM7BTv5/hPKTe6mo1tk5g7FgMrNUyXeZ 19 | 14sXn/IyNQz9eoPliGE3ANxjUxPs6TLYWY9F4IgZpg+qQlT/VjrckYVYer/gAkOy 20 | xEqxvzdjtn3kqDM5Mjf0vyg0vu5ZRowAG5piKwIY+b+LfPz8YTfjXI/dbdCMe5J2 21 | u6V+XUnmrD0= 22 | -----END CERTIFICATE----- 23 | -------------------------------------------------------------------------------- /assets/img/flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marblejs/marble/a1b236888246cd2f3aac3d6c060cbf123787a315/assets/img/flow.png -------------------------------------------------------------------------------- /assets/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marblejs/marble/a1b236888246cd2f3aac3d6c060cbf123787a315/assets/img/logo.png -------------------------------------------------------------------------------- /assets/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Test HTML 6 | 7 | 8 |

Test

9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /assets/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEAor0bGOKA89Q1/ZdHLstg4J/5pNu5LuGn2qoae6AJR46HlbGx 3 | QHTy6h0au9+QcMF6muSOfT26uxAUkvsdeOHi6pu8lQeUKqkk0FGHZfdHznBSPAGq 4 | zT1wA5WjIdt0RERlSvchzyvSqKoJYIgXHTrG/RV9Z2ronY3ujtVh5JjsS1ddJSIB 5 | /McDg8ckpje91BR/t8RGD30qtG5tRMRTao/kBAhMlnrcGkzRxV4Q5vaFZLjmYkrs 6 | YKICmzdO+B+c8zuCPBysJWj22MXhIT+Cbb+QCUKUSflwKVkNtWbYYiVMw6JlWsq5 7 | dTjCjbK+Ly6lZTivV56uDJAwCoLaI7Jt/wDmTwIDAQABAoIBAQCEQB/kQjY/cKaL 8 | tkOf0KGjCf6rrfA5HCL3vaMV8kF4SGkBCs2cLy41eX9/TDiqmWFUvQba2q6EVJYT 9 | uEUENrXcqMFBb05GuPx02ryo7aMKLhd2MnimoBYGo7VVz20WCWsz19A+90MT1FMY 10 | gqAeHeUWST+HKsdtOqmyo9ARIQFuW34xs8tg9949bQoUQ8wKXEmv4XInILfG1sEl 11 | gFEazXnQtkz90PyzGiESqT8S8J/0sSFZrzF6HNAKv5FuY0lZXudmTtAlUGTIx+M/ 12 | JuH2XBjAsGq6NC6Eptz9t4460XkUA9wQl7+liwqGW4sf8tAL8ardkOZ05zgQZkIy 13 | HbTmEybJAoGBANICI8qfJbdPq3G9MUyX1wuMvuU3FaoeqBs4br+qFPRJzLAchtK8 14 | JasrnZnSpvVWQqeUEYS9eLB9Ky8u6IjTOef3Mgjbx3sJjqoh+lG5ngwKr4iLo4d5 15 | JRcTI0M7D8l5hhOeZ0bv1ZB1HkV8yQQldxdEdr4DhxZ+glXbAL5m6B9dAoGBAMZg 16 | 2bptJRirZQvythUb/sFFgVo/boVBJnMi4pVlK1vKcMPgV0N8M0+WnIvEN9yD3+4W 17 | addWuVB3SliVCPRtLnDLxbSutVIGIkCv8JxrrKjOHz7ktnPsvJmxirecL6+SPrwg 18 | DSzxUst+vvwCO1S5BpwLRi5ocMLf0xJDL6LC4/2bAoGBAKNM+KnpgPl/IRCrjjdB 19 | 5v1bL2GrqNQFTLEF+9BcIDkpXdogBJK/rQbiPoXLcPpbXi9TCyBN+Rg77KWe80Da 20 | BzAiXyzWQdKhxubyzuRX0tcIRCCIfNuuTzIXNpdjyM3hCmodBa/6dPYErEpaUzE/ 21 | NNDJ8w+kTJooO48pYfWsrZkdAoGAK9lAhxqU9oz7+tYdNTFI8EOCwgX/UekCrLRr 22 | TQZdvR5UKpet8jbDyLXLyIEIr+9eUewWXQjIUWbswgO/RPfVKg2Al91+KrE8ZPv+ 23 | vTR3p6BX+7jmM12Cmp5JwSf+yloTD9Yt019MI7rSUDXWE3YKZfd//cYMFqcOqpy7 24 | LMjTSD0CgYB5sq4Pvdg66hz2fBnS3+l3b8FhaMprzA7thmCLzYh4ZFk+8xMv12ST 25 | UrqxYgmmFlluFNRE7M7P9Q2B0NaBC8BzwNLjwz06RWzI5JqZ34pzDw+vTmCgugN7 26 | qdjqZNt8KfNXTDtV9wsm6LfITkg/DWh0Uz9wD0JvTyQZpoB+qnjLtA== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /assets/media/audio.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marblejs/marble/a1b236888246cd2f3aac3d6c060cbf123787a315/assets/media/audio.mp3 -------------------------------------------------------------------------------- /benchmarks/Makefile: -------------------------------------------------------------------------------- 1 | all: routing middlewares 2 | 3 | routing: 4 | @./run 5 $@ 5 | @./run 15 $@ 6 | @./run 25 $@ 7 | @./run 50 $@ 8 | @./run 75 $@ 9 | @./run 100 $@ 10 | @./run 150 $@ 11 | @./run 200 $@ 12 | @echo 13 | 14 | middlewares: 15 | @./run 5 $@ 16 | @./run 15 $@ 17 | @./run 25 $@ 18 | @./run 50 $@ 19 | @./run 75 $@ 20 | @./run 100 $@ 21 | @./run 150 $@ 22 | @./run 200 $@ 23 | @echo 24 | 25 | .PHONY: all 26 | -------------------------------------------------------------------------------- /benchmarks/middlewares.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const { mapTo, filter } = require('rxjs/operators'); 3 | const { bindTo, createContext, createServer, httpListener, r, registerAll, ServerClientToken } = require('../packages/core/dist'); 4 | 5 | const n = parseInt(process.env.MW || '1', 10); 6 | 7 | const root$ = r.pipe( 8 | r.matchPath('/'), 9 | r.matchType('GET'), 10 | r.useEffect(req$ => req$.pipe( 11 | mapTo({ body: 'Hello World' }) 12 | )), 13 | ); 14 | 15 | const middlewares = []; 16 | const effects = [root$]; 17 | 18 | for (let i = 0; i < n; i++) { 19 | middlewares.push(req$ => req$); 20 | } 21 | 22 | const server = createServer({ 23 | port: 1337, 24 | listener: httpListener({ effects, middlewares }), 25 | }); 26 | 27 | const bootstrap = async () => { 28 | const app = await server; 29 | await app(); 30 | console.log(` ${n} middlewares - Marble.js`); 31 | }; 32 | 33 | bootstrap(); 34 | -------------------------------------------------------------------------------- /benchmarks/routing.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const { mapTo } = require('rxjs/operators'); 3 | const { bindTo, createContext, createServer, httpListener, r, registerAll, ServerClientToken } = require('../packages/core/dist'); 4 | 5 | const n = parseInt(process.env.MW || '1', 10); 6 | 7 | const middlewares = []; 8 | const effects = []; 9 | 10 | for (let i = 0; i < n-1; i++) { 11 | effects.push( 12 | r.pipe( 13 | r.matchPath(`/test-${i}`), 14 | r.matchType('GET'), 15 | r.useEffect(r$ => r$.pipe( 16 | mapTo({ body: 'Hello World' }), 17 | )), 18 | ), 19 | ); 20 | } 21 | 22 | effects.push( 23 | r.pipe( 24 | r.matchPath(`/`), 25 | r.matchType('GET'), 26 | r.useEffect(r$ => r$.pipe( 27 | mapTo({ body: 'Hello World' }), 28 | )), 29 | ), 30 | ); 31 | 32 | const server = createServer({ 33 | port: 1337, 34 | listener: httpListener({ middlewares, effects }), 35 | }); 36 | 37 | const bootstrap = async () => { 38 | const app = await server; 39 | await app(); 40 | console.log(` ${n} endpoints - Marble.js`); 41 | }; 42 | 43 | bootstrap(); 44 | -------------------------------------------------------------------------------- /benchmarks/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo 4 | MW=$1 node $2 & 5 | pid=$! 6 | 7 | sleep 2 8 | 9 | wrk 'http://localhost:1337' \ 10 | -d 3 \ 11 | -c 50 \ 12 | -t 8 \ 13 | | grep 'Requests/sec' \ 14 | | awk '{ print " " $2 }' 15 | 16 | kill $pid 17 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | target: 90% 6 | threshold: 5% 7 | patch: off 8 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | rabbit: 5 | container_name: marble-rabbit 6 | hostname: rabbit 7 | image: "rabbitmq:management" 8 | ports: 9 | - "15672:15672" 10 | - "5672:5672" 11 | tty: true 12 | redis: 13 | container_name: marble-redis 14 | image: redis 15 | ports: 16 | - "6379:6379" 17 | restart: always 18 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const SCOPE = process.env.SCOPE; 4 | 5 | const config = { 6 | transform: { 7 | '^.+\\.tsx?$': 'ts-jest' 8 | }, 9 | testEnvironment: 'node', 10 | testRegex: 'spec\.ts$', 11 | coverageDirectory: './coverage/', 12 | setupFiles: ['./scripts/test-helpers.js'], 13 | coveragePathIgnorePatterns: [ 14 | 'dist', 15 | '\\+internal/testing', 16 | '@integration', 17 | '\\.spec-(util|setup)\\.ts$', 18 | '\\.spec\\.ts$', 19 | 'integration\\.ts$' 20 | ], 21 | collectCoverageFrom : ['packages/**/*.ts'], 22 | moduleFileExtensions: [ 23 | 'ts', 24 | 'js', 25 | 'json' 26 | ], 27 | globals: { 28 | 'ts-jest': { 29 | tsconfig: path.join(path.dirname(__filename), './tsconfig.test.json'), 30 | diagnostics: { 31 | ignoreCodes: [2300], 32 | }, 33 | }, 34 | }, 35 | }; 36 | 37 | if (SCOPE === 'integration') { 38 | config.testRegex = 'integration\.spec\.ts$'; 39 | console.info('RUNNING INTEGRATION TESTS'); 40 | } 41 | 42 | if (SCOPE === 'unit') { 43 | config.testRegex = '^((?!integration).)*\.spec\.ts$'; 44 | console.info('RUNNING UNIT TESTS'); 45 | } 46 | 47 | module.exports = config; 48 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "4.1.0", 3 | "npmClient": "yarn", 4 | "useWorkspaces": true, 5 | "packages": [ 6 | "packages/*" 7 | ], 8 | "command": { 9 | "publish": { 10 | "message": "chore: publish %s", 11 | "allowBranch": [ 12 | "master", 13 | "feat/*" 14 | ] 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/@integration/README.md: -------------------------------------------------------------------------------- 1 | Marble.js (integration examples) 2 | ======= 3 | 4 | Here all located all integration examples for all [Marble.js](https://github.com/marblejs/marble) packages. 5 | 6 | ## Available examples 7 | 8 | - `./src/http` 9 | 10 | A typical HTTP-based server. Demonstrates basic usage of: 11 | - REST API, 12 | - middlewares 13 | - request validation, 14 | - file serving, 15 | - file upload, 16 | 17 | - `./src/cqrs` 18 | 19 | A simple `EventBus` integration example. Demonstates basc usage of: 20 | - event creation (DDD-like: commands/events) 21 | - dependency injection 22 | - basic usage of `@marblejs/messaging` module based on `EventBus` (local transport layer) 23 | 24 | - `./src/messaging` 25 | 26 | A simple `RabbitMQ (AMQP)` and `Redis Pub/Sub` integration example. Demonstates basc usage of: 27 | - `Redis` transport layer 28 | - `AMQP` transport layer 29 | - dependency injection 30 | - microservices integration (client/consumer), 31 | 32 | - `./src/websockets` 33 | 34 | A typical WebSocket-based server. Demonstrates basic usage of: 35 | - WebSocker server creation 36 | - integration with HTTP and WebSocket server 37 | 38 | - `./test` 39 | 40 | Example usage of `@marblejs/testing` module. 41 | 42 | ## License 43 | MIT 44 | -------------------------------------------------------------------------------- /packages/@integration/jest.config.js: -------------------------------------------------------------------------------- 1 | const config = require('../../jest.config'); 2 | module.exports = config; 3 | -------------------------------------------------------------------------------- /packages/@integration/src/cqrs/__mock__/__mock__dependencies.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { useContext, createContextToken, createReader } from '@marblejs/core'; 3 | import { EventBusClientToken } from '@marblejs/messaging'; 4 | 5 | export const SomeDependencyToken = createContextToken('SomeDependency'); 6 | 7 | /** 8 | * @description Some random dependency used for checking if EventBusClient can be properly resolved 9 | */ 10 | export const SomeDependency = createReader(ask => { 11 | const eventBusClient = useContext(EventBusClientToken)(ask); 12 | 13 | assert.strictEqual(typeof eventBusClient.emit, 'function'); 14 | 15 | return 'nothing special'; 16 | }); 17 | -------------------------------------------------------------------------------- /packages/@integration/src/cqrs/domain/Offer.command.ts: -------------------------------------------------------------------------------- 1 | import * as t from 'io-ts'; 2 | import { event } from '@marblejs/core'; 3 | 4 | export enum OfferCommandType { 5 | GENERATE_OFFER_DOCUMENT = 'GENERATE_OFFER_DOCUMENT', 6 | } 7 | 8 | export const GenerateOfferDocumentCommand = 9 | event(OfferCommandType.GENERATE_OFFER_DOCUMENT)(t.type({ offerId: t.string })); 10 | -------------------------------------------------------------------------------- /packages/@integration/src/cqrs/domain/Offer.event.ts: -------------------------------------------------------------------------------- 1 | import * as t from 'io-ts'; 2 | import { event } from '@marblejs/core'; 3 | 4 | export enum OfferEventType { 5 | OFFER_DOCUMENT_SAVED = 'OFFER_DOCUMENT_SAVED', 6 | OFFER_DOCUMENT_CREATED = 'OFFER_DOCUMENT_CREATED', 7 | } 8 | 9 | export const OfferDocumentCreatedEvent = 10 | event(OfferEventType.OFFER_DOCUMENT_CREATED)(t.type({ offerId: t.string })); 11 | 12 | export const OfferDocumentSavedEvent = 13 | event(OfferEventType.OFFER_DOCUMENT_SAVED)(); 14 | -------------------------------------------------------------------------------- /packages/@integration/src/cqrs/effects/eventbus.effects.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | 3 | import { of } from 'rxjs'; 4 | import { map, tap, delay } from 'rxjs/operators'; 5 | import { pipe } from 'fp-ts/lib/function'; 6 | import { act, matchEvent, useContext, LoggerToken, LoggerTag } from '@marblejs/core'; 7 | import { MsgEffect, EventBusClientToken } from '@marblejs/messaging'; 8 | import { GenerateOfferDocumentCommand } from '../domain/Offer.command'; 9 | import { OfferDocumentSavedEvent, OfferDocumentCreatedEvent } from '../domain/Offer.event'; 10 | import { SomeDependencyToken } from '../__mock__/__mock__dependencies'; 11 | 12 | const tag = LoggerTag.EVENT_BUS; 13 | 14 | export const generateOfferDocument$: MsgEffect = (event$, ctx) => { 15 | const logger = useContext(LoggerToken)(ctx.ask); 16 | const _1 = useContext(SomeDependencyToken)(ctx.ask); 17 | const _2 = useContext(EventBusClientToken)(ctx.ask); 18 | 19 | return event$.pipe( 20 | matchEvent(GenerateOfferDocumentCommand), 21 | act(event => pipe( 22 | of(event.payload.offerId), 23 | tap(logger({ tag, type: 'generateOfferDocument$', message: 'Generating offer document...'})), 24 | delay(5 * 1000), 25 | map(offerId => OfferDocumentCreatedEvent.create({ offerId })), 26 | )), 27 | ); 28 | }; 29 | 30 | export const offerDocumentCreated$: MsgEffect = (event$, ctx) => { 31 | const logger = useContext(LoggerToken)(ctx.ask); 32 | 33 | return event$.pipe( 34 | matchEvent(OfferDocumentCreatedEvent), 35 | act(event => pipe( 36 | of(event.payload.offerId), 37 | tap(logger({ tag, type: 'saveOfferDocument$', message: 'Saving offer document...'})), 38 | delay(5 * 1000), 39 | map(OfferDocumentSavedEvent.create), 40 | )), 41 | ); 42 | }; 43 | -------------------------------------------------------------------------------- /packages/@integration/src/cqrs/effects/http.effects.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from '@marblejs/core'; 2 | import { r, HttpStatus } from '@marblejs/http'; 3 | import { EventBusClientToken } from '@marblejs/messaging'; 4 | import { requestValidator$, t } from '@marblejs/middleware-io'; 5 | import { tap, map } from 'rxjs/operators'; 6 | import { GenerateOfferDocumentCommand } from '../domain/Offer.command'; 7 | 8 | const generateDocumentValidator$ = requestValidator$({ 9 | params: t.type({ 10 | id: t.string, 11 | }), 12 | }); 13 | 14 | export const postDocumentsGenerate$ = r.pipe( 15 | r.matchPath('/documents/:id/generate'), 16 | r.matchType('POST'), 17 | r.useEffect((req$, ctx) => { 18 | const eventBusClient = useContext(EventBusClientToken)(ctx.ask); 19 | 20 | return req$.pipe( 21 | generateDocumentValidator$, 22 | map(req => GenerateOfferDocumentCommand.create({ offerId: req.params.id })), 23 | tap(eventBusClient.emit), 24 | map(() => ({ status: HttpStatus.ACCEPTED })), 25 | ); 26 | })); 27 | -------------------------------------------------------------------------------- /packages/@integration/src/cqrs/index.ts: -------------------------------------------------------------------------------- 1 | import * as T from 'fp-ts/lib/Task'; 2 | import { pipe } from 'fp-ts/lib/function'; 3 | import { isTestEnv, getPortEnv } from '@marblejs/core/dist/+internal/utils'; 4 | import { bindEagerlyTo, bindTo } from '@marblejs/core'; 5 | import { httpListener, createServer } from '@marblejs/http'; 6 | import { messagingListener, EventBusClientToken, EventBusClient, EventBusToken, EventBus } from '@marblejs/messaging'; 7 | import { bodyParser$ } from '@marblejs/middleware-body'; 8 | import { logger$ } from '@marblejs/middleware-logger'; 9 | import { postDocumentsGenerate$ } from './effects/http.effects'; 10 | import { generateOfferDocument$, offerDocumentCreated$ } from './effects/eventbus.effects'; 11 | import { SomeDependencyToken, SomeDependency } from './__mock__/__mock__dependencies'; 12 | 13 | const eventBusListener = messagingListener({ 14 | effects: [ 15 | generateOfferDocument$, 16 | offerDocumentCreated$, 17 | ], 18 | }); 19 | 20 | const listener = httpListener({ 21 | middlewares: [ 22 | logger$({ silent: isTestEnv() }), 23 | bodyParser$(), 24 | ], 25 | effects: [ 26 | postDocumentsGenerate$, 27 | ], 28 | }); 29 | 30 | export const server = () => createServer({ 31 | port: getPortEnv(), 32 | listener, 33 | dependencies: [ 34 | bindEagerlyTo(EventBusToken)(EventBus({ listener: eventBusListener })), 35 | bindEagerlyTo(EventBusClientToken)(EventBusClient), 36 | bindTo(SomeDependencyToken)(SomeDependency), 37 | ], 38 | }); 39 | 40 | export const main = !isTestEnv() 41 | ? pipe(server, T.map(run => run())) 42 | : T.of(undefined); 43 | 44 | main(); 45 | -------------------------------------------------------------------------------- /packages/@integration/src/http/effects/api.effects.ts: -------------------------------------------------------------------------------- 1 | import { r, HttpError, HttpStatus, combineRoutes } from '@marblejs/http'; 2 | import { requestValidator$, t } from '@marblejs/middleware-io'; 3 | import { throwError } from 'rxjs'; 4 | import { map, mergeMap } from 'rxjs/operators'; 5 | import { user$ } from './user.effects'; 6 | import { static$ } from './static.effects'; 7 | 8 | const rootValiadtor$ = requestValidator$({ 9 | params: t.type({ 10 | version: t.union([ 11 | t.literal('v1'), 12 | t.literal('v2'), 13 | ]), 14 | }), 15 | }); 16 | 17 | const root$ = r.pipe( 18 | r.matchPath('/'), 19 | r.matchType('GET'), 20 | r.useEffect(req$ => req$.pipe( 21 | rootValiadtor$, 22 | map(req => req.params.version), 23 | map(version => `API version: ${version}`), 24 | map(message => ({ body: message })), 25 | ))); 26 | 27 | const notImplemented$ = r.pipe( 28 | r.matchPath('/error'), 29 | r.matchType('GET'), 30 | r.useEffect(req$ => req$.pipe( 31 | mergeMap(() => throwError( 32 | () => new HttpError('Route not implemented', HttpStatus.NOT_IMPLEMENTED, { reason: 'Not implemented' }) 33 | )), 34 | ))); 35 | 36 | const notFound$ = r.pipe( 37 | r.matchPath('*'), 38 | r.matchType('*'), 39 | r.useEffect(req$ => req$.pipe( 40 | mergeMap(() => throwError( 41 | () => new HttpError('Route not found', HttpStatus.NOT_FOUND) 42 | )), 43 | ))); 44 | 45 | export const api$ = combineRoutes( 46 | '/api/:version', 47 | [ root$, user$, static$, notImplemented$, notFound$ ], 48 | ); 49 | -------------------------------------------------------------------------------- /packages/@integration/src/http/fakes/auth.fake.ts: -------------------------------------------------------------------------------- 1 | import { HttpRequest } from '@marblejs/http'; 2 | 3 | export const isAuthorized = (request: HttpRequest) => 4 | request.headers.authorization || request.url.includes('?token') 5 | ? request.headers.authorization === 'Bearer FAKE' || request.url.includes('?token=FAKE') 6 | : false; 7 | -------------------------------------------------------------------------------- /packages/@integration/src/http/fakes/dao.fake.ts: -------------------------------------------------------------------------------- 1 | import { of, throwError } from 'rxjs'; 2 | 3 | export const Dao = { 4 | getUsers: () => of([ 5 | { id: '1', firstName: 'Bob', lastName: 'Collins' }, 6 | { id: '2', firstName: 'Sara', lastName: 'Rodriguez' }, 7 | { id: '3', firstName: 'Adam', lastName: 'Wayne' }, 8 | ]), 9 | 10 | getUserById: (id: number | string) => 11 | String(id) !== String(0) 12 | ? of({ id, firstName: 'Bob', lastName: 'Collins' }) 13 | : throwError(() => new Error()), 14 | 15 | postUser: (data: T) => of({ 16 | data, 17 | }), 18 | }; 19 | -------------------------------------------------------------------------------- /packages/@integration/src/http/fakes/random.ts: -------------------------------------------------------------------------------- 1 | import { delay, tap } from 'rxjs/operators'; 2 | import { isTestEnv } from '@marblejs/core/dist/+internal/utils'; 3 | 4 | const getRandomFrom1To10 = () => Math.floor(Math.random() * 10); 5 | 6 | export const simulateRandomDelay = 7 | delay(isTestEnv() ? 0 : getRandomFrom1To10() * 1000); 8 | 9 | export const simulateRandomFailure = tap(() => { 10 | const test = getRandomFrom1To10(); 11 | if (isTestEnv()) return; 12 | if (test > 6) throw new Error('Some random error'); 13 | }); 14 | -------------------------------------------------------------------------------- /packages/@integration/src/http/index.ts: -------------------------------------------------------------------------------- 1 | import * as T from 'fp-ts/lib/Task'; 2 | import { pipe } from 'fp-ts/lib/function'; 3 | import { createServer, httpListener } from '@marblejs/http'; 4 | import { isTestEnv, getPortEnv } from '@marblejs/core/dist/+internal/utils'; 5 | import { logger$ } from '@marblejs/middleware-logger'; 6 | import { bodyParser$ } from '@marblejs/middleware-body'; 7 | import { api$ } from './effects/api.effects'; 8 | import { cors$ } from './middlewares/cors.middleware'; 9 | 10 | export const listener = httpListener({ 11 | middlewares: [ 12 | logger$({ silent: isTestEnv() }), 13 | bodyParser$(), 14 | cors$, 15 | ], 16 | effects: [ 17 | api$, 18 | ], 19 | }); 20 | 21 | export const server = () => createServer({ 22 | port: getPortEnv(), 23 | listener, 24 | }); 25 | 26 | export const main = !isTestEnv() 27 | ? pipe(server, T.map(run => run())) 28 | : T.of(undefined); 29 | 30 | main(); 31 | -------------------------------------------------------------------------------- /packages/@integration/src/http/middlewares/auth.middleware.ts: -------------------------------------------------------------------------------- 1 | import { HttpError, HttpStatus, HttpMiddlewareEffect } from '@marblejs/http'; 2 | import { iif, of, throwError } from 'rxjs'; 3 | import { switchMap } from 'rxjs/operators'; 4 | import { isAuthorized } from '../fakes/auth.fake'; 5 | 6 | export const authorize$: HttpMiddlewareEffect = req$ => 7 | req$.pipe( 8 | switchMap(req => iif( 9 | () => !isAuthorized(req), 10 | throwError(() => new HttpError('Unauthorized', HttpStatus.UNAUTHORIZED)), 11 | of(req), 12 | )), 13 | ); 14 | -------------------------------------------------------------------------------- /packages/@integration/src/http/middlewares/cors.middleware.ts: -------------------------------------------------------------------------------- 1 | import { cors$ as corsMiddleware$ } from '@marblejs/middleware-cors'; 2 | 3 | export const cors$ = corsMiddleware$({ 4 | origin: '*', 5 | methods: ['HEAD', 'GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], 6 | optionsSuccessStatus: 204, 7 | allowHeaders: ['Authorization', 'X-Header'], 8 | withCredentials: true, 9 | maxAge: 36000, 10 | }); 11 | -------------------------------------------------------------------------------- /packages/@integration/src/websockets/websockets.server.ts: -------------------------------------------------------------------------------- 1 | import { WsEffect, webSocketListener, createWebSocketServer } from '@marblejs/websockets'; 2 | import { matchEvent, act } from '@marblejs/core'; 3 | import { eventValidator$, t } from '@marblejs/middleware-io'; 4 | import { buffer, map } from 'rxjs/operators'; 5 | import { flow } from 'fp-ts/lib/function'; 6 | 7 | const sum$: WsEffect = event$ => 8 | event$.pipe( 9 | matchEvent('SUM') 10 | ); 11 | 12 | const add$: WsEffect = (event$, ctx) => 13 | event$.pipe( 14 | matchEvent('ADD'), 15 | act(flow( 16 | eventValidator$(t.number), 17 | buffer(sum$(event$, ctx)), 18 | map(addEvents => addEvents.reduce((a, e) => e.payload + a, 0)), 19 | map(payload => ({ type: 'SUM_RESULT', payload })), 20 | )) 21 | ); 22 | 23 | export const webSocketServer = createWebSocketServer({ 24 | listener: webSocketListener({ 25 | middlewares: [], 26 | effects: [add$], 27 | }), 28 | }); 29 | -------------------------------------------------------------------------------- /packages/@integration/test/messaging.integration.spec.ts: -------------------------------------------------------------------------------- 1 | import { pipe } from 'fp-ts/lib/function'; 2 | import { createTestBedSetup, createHttpTestBed } from '@marblejs/testing'; 3 | import { createMicroserviceTestBed } from '@marblejs/messaging/dist/+internal/testing'; 4 | import { dependencies, listener } from '../src/messaging/client'; 5 | import { amqpMicroservice, redisMicroservice } from '../src/messaging/server'; 6 | 7 | const useHttpTestBedSetup = createTestBedSetup({ 8 | dependencies, 9 | testBed: createHttpTestBed({ listener }), 10 | }); 11 | 12 | describe('messaging integration', () => { 13 | const httpTestBedSetup = useHttpTestBedSetup(); 14 | 15 | afterEach(httpTestBedSetup.cleanup); 16 | 17 | createMicroserviceTestBed(redisMicroservice); 18 | createMicroserviceTestBed(amqpMicroservice); 19 | 20 | test.each([ 21 | 'redis', 22 | 'amqp', 23 | ])('GET /%s/fib returns 10, 11, 12, 13, 14 th fibonacci number', async type => { 24 | const { request } = await httpTestBedSetup.useTestBed(); 25 | 26 | const response = await pipe( 27 | request('GET'), 28 | request.withPath(`/${type}/fib/10`), 29 | request.send, 30 | ); 31 | 32 | expect(response.statusCode).toEqual(200); 33 | expect(response.body).toEqual([ 34 | { type: 'FIB_RESULT', payload: 55 }, 35 | { type: 'FIB_RESULT', payload: 89 }, 36 | { type: 'FIB_RESULT', payload: 144 }, 37 | { type: 'FIB_RESULT', payload: 233 }, 38 | { type: 'FIB_RESULT', payload: 377 }, 39 | ]); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /packages/@integration/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "emitDecoratorMetadata": true, 4 | "experimentalDecorators": true, 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "noImplicitAny": false, 8 | "noEmitOnError": true, 9 | "pretty": true, 10 | "strict": true, 11 | "target": "es6", 12 | "outDir": "./dist/", 13 | "rootDir": "./src", 14 | "typeRoots": [ 15 | "node_modules/@types", 16 | "../node_modules/@types" 17 | ], 18 | "lib": [ 19 | "es2018" 20 | ] 21 | }, 22 | "inputs": [ 23 | "src" 24 | ], 25 | "include": [ 26 | "node_modules/@types", 27 | "src/**/*.ts" 28 | ], 29 | "references": [ 30 | { "path": "../core" }, 31 | { "path": "../http" }, 32 | { "path": "../messaging" }, 33 | { "path": "../websockets" }, 34 | { "path": "../middleware-body" }, 35 | { "path": "../middleware-cors" }, 36 | { "path": "../middleware-io" }, 37 | { "path": "../middleware-logger" }, 38 | { "path": "../middleware-multipart" }, 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /packages/README.md: -------------------------------------------------------------------------------- 1 | Marble.js 2 | ======= 3 | 4 | Here all located all source files for all [Marble.js](https://github.com/marblejs/marble) packages. 5 | 6 | License: MIT 7 | -------------------------------------------------------------------------------- /packages/core/jest.config.js: -------------------------------------------------------------------------------- 1 | const config = require('../../jest.config'); 2 | module.exports = config; 3 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@marblejs/core", 3 | "version": "4.1.0", 4 | "description": "Reactive Node APIs made easy", 5 | "main": "./dist/index.js", 6 | "types": "./dist/index.d.ts", 7 | "scripts": { 8 | "start": "yarn watch", 9 | "watch": "tsc -w", 10 | "build": "tsc", 11 | "clean": "rimraf dist", 12 | "test": "jest --config ./jest.config.js", 13 | "prepack": "cp ../../README.md ./" 14 | }, 15 | "files": [ 16 | "dist/" 17 | ], 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/marblejs/marble.git" 21 | }, 22 | "engines": { 23 | "node": ">= 8.0.0", 24 | "yarn": ">= 1.7.0", 25 | "npm": ">= 5.0.0" 26 | }, 27 | "author": "marblejs", 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/marblejs/marble/issues" 31 | }, 32 | "homepage": "https://github.com/marblejs/marble#readme", 33 | "peerDependencies": { 34 | "fp-ts": "^2.13.1", 35 | "rxjs": "^7.5.7" 36 | }, 37 | "dependencies": { 38 | "@types/uuid": "^7.0.0", 39 | "chalk": "^3.0.0", 40 | "io-ts": "^2.2.19", 41 | "uuid": "^7.0.1" 42 | }, 43 | "publishConfig": { 44 | "access": "public" 45 | }, 46 | "gitHead": "208643c17d76a01d0a9cb304611d4474c4a3bff6" 47 | } 48 | -------------------------------------------------------------------------------- /packages/core/src/+internal/files/fileReader.helper.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | import { Observable } from 'rxjs'; 4 | 5 | export const readFile = (basePath: string) => (dir: string) => 6 | new Observable(subscriber => { 7 | const pathname = path.resolve(basePath, dir); 8 | 9 | fs.readFile(pathname, (err, file) => { 10 | if (err && err.code === 'ENOENT') { 11 | subscriber.error(err); 12 | } 13 | subscriber.next(file); 14 | subscriber.complete(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /packages/core/src/+internal/files/index.ts: -------------------------------------------------------------------------------- 1 | export * from './fileReader.helper'; 2 | -------------------------------------------------------------------------------- /packages/core/src/+internal/fp/IxBuilder.spec.ts: -------------------------------------------------------------------------------- 1 | import { iof, ichain, map } from './IxBuilder'; 2 | 3 | describe('IxBuilder', () => { 4 | test('maps values', () => { 5 | expect(iof('a') 6 | .map(x => x + 'b') 7 | .map(x => x + 'c') 8 | .run()).toBe('abc'); 9 | }); 10 | 11 | test('chains builders', () => { 12 | expect(iof('a') 13 | .ichain(x => iof(x + 'b')) 14 | .ichain(x => iof(x + 'c')) 15 | .run()).toBe('abc'); 16 | }); 17 | }); 18 | 19 | test('static #iof creates builder', () => { 20 | expect( 21 | iof('a').run() 22 | ).toBe('a'); 23 | }); 24 | 25 | test('static #ichan chains builders', () => { 26 | expect( 27 | ichain(iof('a'), x => iof(x + 'b')).run() 28 | ).toBe('ab'); 29 | }); 30 | 31 | test('static #map maps values', () => { 32 | expect( 33 | map(iof('a'), x => x + 'b').run() 34 | ).toBe('ab'); 35 | }); 36 | -------------------------------------------------------------------------------- /packages/core/src/+internal/fp/IxBuilder.ts: -------------------------------------------------------------------------------- 1 | export class IxBuilder { 2 | readonly _A: A | undefined; 3 | readonly _L: O | undefined; 4 | readonly _U: I | undefined; 5 | 6 | constructor(readonly spec: A) {} 7 | 8 | run(): A { 9 | return this.spec; 10 | } 11 | 12 | map(f: (a: A) => B): IxBuilder { 13 | return new IxBuilder(f(this.spec)); 14 | } 15 | 16 | ichain(f: (a: A) => IxBuilder): IxBuilder { 17 | return new IxBuilder(f(this.spec).run()); 18 | } 19 | } 20 | 21 | export const map = 22 | (fa: IxBuilder, f: (a: A) => B): IxBuilder => 23 | fa.map(f); 24 | 25 | export const iof = 26 | (a: A): IxBuilder => 27 | new IxBuilder(a); 28 | 29 | export const ichain = 30 | (fa: IxBuilder, f: (a: A) => IxBuilder): IxBuilder => 31 | fa.ichain(f); 32 | 33 | export const ichainCurry = 34 | (f: (a: A) => IxBuilder) => 35 | (fa: IxBuilder): IxBuilder => 36 | fa.ichain(f); 37 | -------------------------------------------------------------------------------- /packages/core/src/+internal/fp/index.ts: -------------------------------------------------------------------------------- 1 | export * from './IxBuilder'; 2 | -------------------------------------------------------------------------------- /packages/core/src/+internal/observable/fromReadableStream.spec.ts: -------------------------------------------------------------------------------- 1 | import { Transform } from 'stream'; 2 | import { fromReadableStream } from './fromReadableStream'; 3 | 4 | class DuplexStream extends Transform { 5 | _transform(chunk, _, callback) { 6 | this.push(chunk); 7 | callback(); 8 | } 9 | } 10 | 11 | describe('#fromReadableStream', () => { 12 | test('reads data to buffer', done => { 13 | const stream = new DuplexStream(); 14 | const data = '111'; 15 | 16 | fromReadableStream(stream).subscribe({ 17 | next: (buffer: Buffer) => expect(buffer.toString()).toEqual(data), 18 | error: (err: Error) => fail(`Error shouldn't be thrown: ${err}`), 19 | complete: done, 20 | }); 21 | 22 | stream.write(data); 23 | stream.end(); 24 | }); 25 | 26 | test('handles stream error', done => { 27 | const stream = new DuplexStream(); 28 | const errorToThrow = new Error('test-error'); 29 | 30 | fromReadableStream(stream).subscribe({ 31 | next: () => fail(`Error should be thrown`), 32 | error: (error: Error) => { 33 | expect(error.message).toBe(errorToThrow.message); 34 | done(); 35 | }, 36 | }); 37 | 38 | stream.emit('error', errorToThrow); 39 | stream.end(); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /packages/core/src/+internal/observable/fromReadableStream.ts: -------------------------------------------------------------------------------- 1 | import { Readable } from 'stream'; 2 | import { Observable } from 'rxjs'; 3 | 4 | export const fromReadableStream = (stream: Readable): Observable => { 5 | stream.pause(); 6 | return new Observable(observer => { 7 | const next = chunk => observer.next(chunk); 8 | const complete = () => observer.complete(); 9 | const error = err => observer.error(err); 10 | 11 | stream 12 | .on('data', next) 13 | .on('error', error) 14 | .on('end', complete) 15 | .resume(); 16 | 17 | return () => { 18 | stream.removeListener('data', next); 19 | stream.removeListener('error', error); 20 | stream.removeListener('end', complete); 21 | }; 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /packages/core/src/+internal/observable/index.ts: -------------------------------------------------------------------------------- 1 | export * from './fromReadableStream'; 2 | -------------------------------------------------------------------------------- /packages/core/src/+internal/testing/index.ts: -------------------------------------------------------------------------------- 1 | export * from './marbles.helper'; 2 | -------------------------------------------------------------------------------- /packages/core/src/+internal/testing/marbles.helper.ts: -------------------------------------------------------------------------------- 1 | import { TestScheduler } from 'rxjs/testing'; 2 | import { EffectContext, EffectLike } from '../../effects/effects.interface'; 3 | 4 | type MarbleFlow = 5 | | [string, { [marble: string]: any } | undefined] 6 | | [string, { [marble: string]: any } | undefined, any] 7 | ; 8 | type MarbleDependencies = { ctx?: Partial> }; 9 | 10 | export const Marbles = { 11 | deepEquals: (actual: any, expected: any) => expect(actual).toEqual(expected), 12 | 13 | createTestScheduler: () => new TestScheduler(Marbles.deepEquals), 14 | 15 | assertEffect: ( 16 | effect: EffectLike, 17 | marbleflow: [MarbleFlow, MarbleFlow], 18 | dependencies: MarbleDependencies = {}, 19 | ) => { 20 | const [initStream, initValues, initError] = marbleflow[0]; 21 | const [expectedStream, expectedValues, expectedError] = marbleflow[1]; 22 | 23 | const scheduler = Marbles.createTestScheduler(); 24 | const stream$ = scheduler.createColdObservable(initStream, initValues, initError); 25 | 26 | scheduler 27 | .expectObservable(effect(stream$, dependencies.ctx)) 28 | .toBe(expectedStream, expectedValues, expectedError); 29 | 30 | scheduler.flush(); 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /packages/core/src/+internal/utils/array.util.ts: -------------------------------------------------------------------------------- 1 | import { fromNullable } from 'fp-ts/lib/Option'; 2 | 3 | export const getArrayFromEnum = (E: Record) => 4 | Object.keys(E).filter(key => typeof E[key as any] === 'number'); 5 | 6 | export const getHead = (array: T[]) => 7 | fromNullable(array[0]); 8 | 9 | export const getLast = (array: T[]) => 10 | fromNullable(array[array.length - 1]); 11 | 12 | export const filterArray = (f: (v: T) => boolean) => (array: T[]) => 13 | array.filter(f); 14 | 15 | export const mapArray = (f: (v: T) => R) => (array: T[]) => 16 | array.map(f); 17 | 18 | export const insertIf = (condition: boolean) => (...elements: T[]) => 19 | condition ? elements as NonNullable[] : []; 20 | 21 | export const insertIfElse = (condition: boolean) => (...elements: T[]) => (...elseElements: U[]) => 22 | condition ? elements as NonNullable[] : elseElements as NonNullable[]; 23 | -------------------------------------------------------------------------------- /packages/core/src/+internal/utils/env.util.ts: -------------------------------------------------------------------------------- 1 | import * as IO from 'fp-ts/IO'; 2 | import { Option, fromNullable, getOrElse } from 'fp-ts/Option'; 3 | import { pipe } from 'fp-ts/function'; 4 | 5 | function memoize(ma: IO.IO): IO.IO { 6 | let cache: A; 7 | let isMemoized = false; 8 | 9 | return () => { 10 | if (!isMemoized) { 11 | cache = ma(); 12 | isMemoized = true; 13 | } 14 | 15 | return cache; 16 | }; 17 | } 18 | 19 | /** 20 | * Read env config (but only once, value is cached) 21 | * 22 | * @param key - env variable to read. 23 | * @see getEnvConfigOrElse 24 | */ 25 | export const getEnvConfig = (envKey: string): IO.IO> => 26 | memoize(() => fromNullable(process.env[envKey])); 27 | 28 | /** 29 | * Read env config with fallback value in case it is not defined. 30 | * 31 | * @param envKey 32 | * @param onNone 33 | * @see getEnvConfig 34 | */ 35 | export const getEnvConfigOrElse = (envKey: string, onNone: string): IO.IO => 36 | pipe( 37 | getEnvConfig(envKey), 38 | IO.map(getOrElse(() => onNone)), 39 | ); 40 | 41 | /** 42 | * Read env config using #getEnvConfigOrElse and converts the value to boolean 43 | * defined value must be "true" (case insensitive) to be true. 44 | * 45 | * @param envKey 46 | * @param onNone 47 | * @see getEnvConfigOrElse 48 | */ 49 | export const getEnvConfigOrElseAsBoolean = (envKey: string, onNone: 'false' | 'true' | true | false): IO.IO => 50 | pipe( 51 | getEnvConfigOrElse(envKey, String(onNone)), 52 | IO.map(val => val.toLowerCase() === 'true'), 53 | ); 54 | -------------------------------------------------------------------------------- /packages/core/src/+internal/utils/error.util.ts: -------------------------------------------------------------------------------- 1 | import { isString } from './string.util'; 2 | 3 | export class NamedError extends Error { 4 | constructor(public name: string, message: string) { 5 | super(message); 6 | } 7 | } 8 | 9 | export const isNamedError = (data: any): data is NamedError => 10 | !!data?.name && !!data?.message; 11 | 12 | export const isError = (data: any): data is Error => 13 | !!data?.stack && !!data?.name; 14 | 15 | export const encodeError = (error: any) => 16 | !isError(error) ? error : ['name', ...Object.getOwnPropertyNames(error)] 17 | .filter(key => !['stack'].includes(key)) 18 | .reduce((acc, key) => { 19 | acc[key] = error[key]; 20 | return acc; 21 | }, Object.create(null)); 22 | 23 | export const throwException = (error: any) => { throw error; }; 24 | 25 | export const getErrorMessage = (error: unknown): string => 26 | error instanceof Error 27 | ? error.message 28 | : isString(error) 29 | ? error 30 | : JSON.stringify(error); 31 | -------------------------------------------------------------------------------- /packages/core/src/+internal/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './array.util'; 2 | export * from './string.util'; 3 | export * from './error.util'; 4 | export * from './any.util'; 5 | export * from './stream.util'; 6 | export * from './type.util'; 7 | export * from './env.util'; 8 | -------------------------------------------------------------------------------- /packages/core/src/+internal/utils/spec/any.util.spec.ts: -------------------------------------------------------------------------------- 1 | import { isNonNullable, isNullable, getPortEnv } from '../any.util'; 2 | 3 | test('#isNonNullable', () => { 4 | expect(isNonNullable({})).toBe(true); 5 | expect(isNonNullable(2)).toBe(true); 6 | expect(isNonNullable('test')).toBe(true); 7 | expect(isNonNullable('')).toBe(true); 8 | expect(isNonNullable(false)).toBe(true); 9 | expect(isNonNullable(null)).toBe(false); 10 | expect(isNonNullable(undefined)).toBe(false); 11 | }); 12 | 13 | test('#isNullable', () => { 14 | expect(isNullable({})).toBe(false); 15 | expect(isNullable(2)).toBe(false); 16 | expect(isNullable('test')).toBe(false); 17 | expect(isNullable('')).toBe(false); 18 | expect(isNullable(false)).toBe(false); 19 | expect(isNullable(null)).toBe(true); 20 | expect(isNullable(undefined)).toBe(true); 21 | }); 22 | 23 | test('#getPortEnv', () => { 24 | process.env.PORT = '8000'; 25 | expect(getPortEnv()).toEqual(8000); 26 | }); 27 | -------------------------------------------------------------------------------- /packages/core/src/+internal/utils/spec/error.util.spec.ts: -------------------------------------------------------------------------------- 1 | import { isError } from '../error.util'; 2 | 3 | describe('#isError', () => { 4 | const cases: [any, boolean][] = [ 5 | [{}, false], 6 | [new (class Foo { stack = [] }), false], 7 | [new Error(), true], 8 | [new Error('test'), true], 9 | ]; 10 | 11 | test.each(cases)('given %p as argument, returns %p', (data, expected) => 12 | expect(isError(data)).toEqual(expected)); 13 | }); 14 | -------------------------------------------------------------------------------- /packages/core/src/+internal/utils/stream.util.ts: -------------------------------------------------------------------------------- 1 | import { Stream } from 'stream'; 2 | 3 | export const isStream = (stream: any): stream is Stream => 4 | stream !== null && 5 | typeof stream === 'object' && 6 | typeof stream.pipe === 'function'; 7 | -------------------------------------------------------------------------------- /packages/core/src/+internal/utils/string.util.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuid } from 'uuid'; 2 | import { flow } from 'fp-ts/lib/function'; 3 | import { fromNullable, getOrElse } from 'fp-ts/lib/Option'; 4 | 5 | export const isString = (value: any): value is string => 6 | typeof value === 'string' || value instanceof String; 7 | 8 | export const trim = (strings: TemplateStringsArray, ...values: any[]) => { 9 | const notNilValues = values.map(flow(fromNullable, getOrElse(() => ''))); 10 | const interpolation = strings.reduce( 11 | (prev, current) => prev + current + (notNilValues.length ? notNilValues.shift() : ''), '', 12 | ); 13 | 14 | return interpolation.trim(); 15 | }; 16 | 17 | export const trunc = (n: number) => (input: string) => 18 | (input.length > n) 19 | ? input.substr(0, n) + '…' 20 | : input; 21 | 22 | export const stringify = (value: any): string => 23 | typeof value === 'function' 24 | ? (value.displayName || value.name) 25 | : JSON.stringify(value); 26 | 27 | export const createUuid = () => uuid(); 28 | 29 | export const maskUriComponent = (type: 'authorization') => (uri: string): string => { 30 | switch (type) { 31 | case 'authorization': 32 | return uri.replace(/\/\/(.*)\@/, '//[user]:[pass]@'); 33 | default: 34 | return uri; 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /packages/core/src/+internal/utils/type.util.ts: -------------------------------------------------------------------------------- 1 | export type PromiseArg = T extends PromiseLike ? U : T; 2 | -------------------------------------------------------------------------------- /packages/core/src/context/context.helper.ts: -------------------------------------------------------------------------------- 1 | import * as T from 'fp-ts/lib/Task'; 2 | import * as O from 'fp-ts/lib/Option'; 3 | import * as IO from 'fp-ts/lib/IO'; 4 | import { constant, pipe } from 'fp-ts/lib/function'; 5 | import { logger, LoggerToken, mockLogger } from '../logger'; 6 | import { isTestEnv } from '../+internal/utils'; 7 | import { registerAll, BoundDependency, createContext, bindTo, resolve, Context, lookup, DerivedContextToken } from './context'; 8 | import { ContextToken } from './context.token.factory'; 9 | 10 | /** 11 | * `INTERNAL` - unregisters redundant token if available in DerivedContext 12 | * @since v3.4.0 13 | */ 14 | const unregisterRedundantToken = (token: ContextToken) => (context: Context): IO.IO => { 15 | const deleteToken = pipe( 16 | () => context.delete(token), 17 | IO.map(constant(context)), 18 | ); 19 | 20 | return pipe( 21 | lookup(context)(DerivedContextToken), 22 | O.chain(derivedContext => lookup(derivedContext)(token)), 23 | O.fold(() => IO.of(context), () => deleteToken), 24 | ); 25 | }; 26 | 27 | /** 28 | * Constructs and resolves a new or derived context based on provided dependencies 29 | * @since v3.4.0 30 | */ 31 | export const constructContext = (context?: Context) => (...dependencies: BoundDependency[]): Promise => 32 | pipe( 33 | context ?? createContext(), 34 | registerAll([ 35 | bindTo(LoggerToken)(isTestEnv() ? mockLogger : logger), 36 | ...dependencies, 37 | ]), 38 | context => () => resolve(context), 39 | T.chain(context => T.fromIO(unregisterRedundantToken(LoggerToken)(context))), 40 | )(); 41 | 42 | /** 43 | * Constructs and resolves a new context based on provided dependencies 44 | * @since v3.2.0 45 | */ 46 | export const contextFactory = constructContext(); 47 | -------------------------------------------------------------------------------- /packages/core/src/context/context.hook.ts: -------------------------------------------------------------------------------- 1 | import { identity, pipe } from 'fp-ts/lib/function'; 2 | import * as O from 'fp-ts/lib/Option'; 3 | import { ContextError } from '../error/error.model'; 4 | import { coreErrorFactory, CoreErrorOptions } from '../error/error.factory'; 5 | import { ContextToken } from './context.token.factory'; 6 | import { ContextProvider } from './context'; 7 | 8 | const coreErrorOptions: CoreErrorOptions = { contextMethod: 'useContext', offset: 2 }; 9 | 10 | export const useContext = (token: ContextToken) => (ask: ContextProvider) => pipe( 11 | ask(token), 12 | O.fold( 13 | () => { 14 | const message = `Cannot resolve "${token.name || token._id}" context token.`; 15 | const detail = `You've probably forgot to register the bound token in the app context.`; 16 | const error = new ContextError(`${message} ${detail}`); 17 | const coreError = coreErrorFactory(error.message, coreErrorOptions); 18 | 19 | console.error(coreError.stack); 20 | 21 | throw error; 22 | }, 23 | identity, 24 | ), 25 | ); 26 | -------------------------------------------------------------------------------- /packages/core/src/context/context.logger.ts: -------------------------------------------------------------------------------- 1 | import * as M from 'fp-ts/lib/Map'; 2 | import * as A from 'fp-ts/lib/Array'; 3 | import * as IO from 'fp-ts/lib/IO'; 4 | import { pipe } from 'fp-ts/lib/function'; 5 | import { LoggerToken } from '../logger'; 6 | import { Context, lookup, ordContextToken } from './context'; 7 | import { useContext } from './context.hook'; 8 | import { ContextToken } from './context.token.factory'; 9 | 10 | export const logContext = (tag: string) => (context: Context): Context => { 11 | const logger = pipe( 12 | lookup(context), 13 | useContext(LoggerToken), 14 | ); 15 | 16 | const log = (token: ContextToken) => logger({ 17 | tag, 18 | type: 'Context', 19 | message: token.name 20 | ? `Registered: "${token.name}"` 21 | : `Registered unnamed token: ${token._id}`, 22 | }); 23 | 24 | const logDependencies = IO.sequenceArray( 25 | pipe( 26 | context, 27 | M.keys(ordContextToken), 28 | A.map(log), 29 | ) 30 | ); 31 | 32 | logDependencies(); 33 | 34 | return context; 35 | }; 36 | -------------------------------------------------------------------------------- /packages/core/src/context/context.reader.factory.ts: -------------------------------------------------------------------------------- 1 | import { pipe } from 'fp-ts/lib/function'; 2 | import * as R from 'fp-ts/lib/Reader'; 3 | import { ContextProvider, reader } from './context'; 4 | 5 | export type ReaderHandler = (ask: ContextProvider) => T; 6 | 7 | export const createReader = (handler: ReaderHandler) => 8 | pipe(reader, R.map(handler)); 9 | -------------------------------------------------------------------------------- /packages/core/src/context/context.token.factory.ts: -------------------------------------------------------------------------------- 1 | import { createUuid } from '../+internal/utils'; 2 | 3 | export class ContextToken { 4 | _id = createUuid(); 5 | _T!: T; 6 | constructor(public name?: string) {} 7 | } 8 | 9 | export const createContextToken = (name?: string) => 10 | new class extends ContextToken { 11 | constructor() { super(name); } 12 | }; 13 | -------------------------------------------------------------------------------- /packages/core/src/effects/effects.combiner.ts: -------------------------------------------------------------------------------- 1 | import { Observable, merge } from 'rxjs'; 2 | import { share } from 'rxjs/operators'; 3 | import { Effect, EffectContext } from './effects.interface'; 4 | 5 | export const combineMiddlewares = 6 | (...effects: (Effect | undefined)[]) => 7 | (input$: Observable, ctx: EffectContext): Observable => 8 | effects.reduce((i$, effect) => effect ? effect(i$, ctx) : i$, input$); 9 | 10 | export const combineEffects = 11 | (...effects: Effect[]) => 12 | (input$: Observable, ctx: EffectContext): Observable => 13 | merge(...effects.map(effect => effect(input$, ctx))).pipe(share()); 14 | -------------------------------------------------------------------------------- /packages/core/src/effects/effects.interface.ts: -------------------------------------------------------------------------------- 1 | import { Observable, SchedulerLike } from 'rxjs'; 2 | import { ContextProvider } from '../context/context'; 3 | 4 | export interface EffectLike { 5 | (input$: Observable, ...args: any[]): Observable; 6 | } 7 | 8 | export interface EffectMiddlewareLike { 9 | (i$: Observable, ...args: any[]): Observable; 10 | } 11 | 12 | export interface Effect { 13 | (input$: Observable, ctx: EffectContext): Observable; 14 | } 15 | 16 | export interface EffectContext { 17 | ask: ContextProvider; 18 | scheduler: U; 19 | client: T; 20 | } 21 | -------------------------------------------------------------------------------- /packages/core/src/effects/effectsContext.factory.ts: -------------------------------------------------------------------------------- 1 | import { SchedulerLike, asyncScheduler } from 'rxjs'; 2 | import { ContextProvider } from '../context/context'; 3 | import { EffectContext } from './effects.interface'; 4 | 5 | export const createEffectContext = ( 6 | data: { 7 | ask: ContextProvider; 8 | client: Client; 9 | scheduler?: Scheduler; 10 | }, 11 | ): EffectContext => ({ 12 | ask: data.ask, 13 | client: data.client, 14 | scheduler: data.scheduler || typeof asyncScheduler as any, 15 | }); 16 | -------------------------------------------------------------------------------- /packages/core/src/error/error.factory.ts: -------------------------------------------------------------------------------- 1 | import * as chalk from 'chalk'; 2 | import { CoreError } from './error.model'; 3 | 4 | export type CoreErrorOptions = { 5 | contextMethod?: string; 6 | contextPackage?: string; 7 | offset?: number; 8 | printStacktrace?: boolean; 9 | }; 10 | 11 | export const stringifyStackTrace = (stackTrace: NodeJS.CallSite[]) => 12 | stackTrace 13 | .map(s => chalk.gray(` @ ${s}`)) 14 | .join('\n '); 15 | 16 | export const coreErrorStackTraceFactory = (opts: CoreErrorOptions) => (message: string, stack: NodeJS.CallSite[]) => { 17 | const methodPointer = opts.offset ?? 0; 18 | const printStacktrace = opts.printStacktrace ?? true; 19 | const method = stack[methodPointer]; 20 | 21 | const filePointer = methodPointer + 1; 22 | const file = stack[filePointer]; 23 | 24 | const restStack = stack.slice(filePointer, stack.length); 25 | const [line, col] = [file.getLineNumber() ?? 0, file.getColumnNumber() ?? 0]; 26 | const packageName = opts.contextPackage ?? '@marblejs/core'; 27 | const methodName = opts.contextMethod + ' : ' + (method.getMethodName() ?? '-'); 28 | const fileName = file.getFileName() ?? ''; 29 | 30 | return ` 31 | ${chalk.red(`${packageName} error:`)} 32 | 33 | 🚨 ${message} 34 | ${printStacktrace ? ` 35 | 👉 ${chalk.yellow.bold(methodName)} 36 | @ file: ${chalk.underline(fileName)} 37 | @ line: [${line.toString()}:${col.toString()}] 38 | 39 | ${stringifyStackTrace(restStack)}\n` : ''} 40 | `; 41 | }; 42 | 43 | export const coreErrorFactory = (message: string, opts: CoreErrorOptions) => 44 | new CoreError( 45 | message || 'Something is not right...', 46 | { 47 | stackTraceFactory: coreErrorStackTraceFactory(opts), 48 | context: coreErrorFactory, 49 | } 50 | ); 51 | -------------------------------------------------------------------------------- /packages/core/src/error/error.model.ts: -------------------------------------------------------------------------------- 1 | import { Event } from '../event/event.interface'; 2 | import { NamedError } from '../+internal/utils'; 3 | 4 | export enum ErrorType { 5 | CORE_ERROR = 'CoreError', 6 | CONTEXT_ERROR = 'ContextError', 7 | EVENT_ERROR = 'EventError', 8 | } 9 | 10 | export class CoreError extends NamedError { 11 | constructor( 12 | public readonly message: string, 13 | options: { 14 | stackTraceFactory: (message: string, stack: NodeJS.CallSite[]) => string; 15 | context: any; 16 | } 17 | ) { 18 | super(ErrorType.CORE_ERROR, message); 19 | Error.prepareStackTrace = (_, stack) => options.stackTraceFactory(message, stack); 20 | Error.captureStackTrace(this, options.context); 21 | } 22 | } 23 | 24 | export class ContextError extends NamedError { 25 | constructor( 26 | public readonly message: string, 27 | ) { 28 | super(ErrorType.CONTEXT_ERROR, message); 29 | } 30 | } 31 | 32 | export class EventError extends NamedError { 33 | constructor( 34 | public readonly event: Event, 35 | public readonly message: string, 36 | public readonly data?: Record | Array, 37 | ) { 38 | super(ErrorType.EVENT_ERROR, message); 39 | } 40 | } 41 | 42 | export const isCoreError = (error: Error | undefined): error is CoreError => 43 | error?.name === ErrorType.CORE_ERROR; 44 | 45 | export const isEventError = (error: Error | undefined): error is EventError => 46 | error?.name === ErrorType.EVENT_ERROR; 47 | -------------------------------------------------------------------------------- /packages/core/src/error/specs/error.model.spec.ts: -------------------------------------------------------------------------------- 1 | import { CoreError, isCoreError, EventError, isEventError } from '../error.model'; 2 | 3 | describe('Error model', () => { 4 | 5 | beforeEach(() => { 6 | jest.spyOn(Error, 'captureStackTrace'); 7 | }); 8 | 9 | afterEach(() => { 10 | jest.clearAllMocks(); 11 | }); 12 | 13 | test('#CoreError creates error object', () => { 14 | // given 15 | const stackTraceFactory = jest.fn(); 16 | const error = new CoreError('test-message', { 17 | context: {}, 18 | stackTraceFactory, 19 | }); 20 | 21 | // when 22 | if (!Error.prepareStackTrace) { 23 | return fail('Error.prepareStackTrace is not defined'); 24 | } 25 | 26 | Error.prepareStackTrace(error, []); 27 | 28 | // then 29 | expect(error.name).toBe('CoreError'); 30 | expect(error.message).toBe('test-message'); 31 | expect(Error.captureStackTrace).toHaveBeenCalled(); 32 | expect(stackTraceFactory).toHaveBeenCalledWith('test-message', []); 33 | 34 | return; 35 | }); 36 | 37 | test('#isCoreError detects CoreError type', () => { 38 | const coreError = new CoreError('test-message', { 39 | context: {}, 40 | stackTraceFactory: jest.fn(), 41 | }); 42 | const otherError = new Error(); 43 | 44 | expect(isCoreError(coreError)).toBe(true); 45 | expect(isCoreError(otherError)).toBe(false); 46 | }); 47 | 48 | test('#isEventError detects EventError type', () => { 49 | const eventError = new EventError({ type: 'TEST', }, 'test-message', {}); 50 | const otherError = new Error(); 51 | 52 | expect(isEventError(eventError)).toBe(true); 53 | expect(isEventError(otherError)).toBe(false); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /packages/core/src/event/event.interface.ts: -------------------------------------------------------------------------------- 1 | export type EventType = string; 2 | 3 | export interface EventMetadata extends Record { 4 | correlationId?: string; 5 | replyTo?: string; 6 | raw?: any; 7 | } 8 | 9 | export interface Event

{ 10 | type: T; 11 | payload?: P; 12 | error?: E; 13 | metadata?: EventMetadata; 14 | } 15 | 16 | export interface EventWithoutPayload { 17 | type: T; 18 | metadata?: EventMetadata; 19 | } 20 | 21 | export interface EventWithPayload

{ 22 | type: T; 23 | payload: P; 24 | metadata?: EventMetadata; 25 | } 26 | 27 | export type ValidatedEvent

2 | 3 | Marble.js logo 4 | 5 |

6 | 7 | # @marblejs/http 8 | 9 | A HTTP module for [Marble.js](https://github.com/marblejs/marble). 10 | 11 | ## Installation 12 | 13 | ``` 14 | $ npm i @marblejs/http 15 | ``` 16 | Requires `@marblejs/core`, `rxjs` and `fp-ts` to be installed. 17 | 18 | ## Documentation 19 | 20 | For the latest updates, documentation, change log, and release information visit [docs.marblejs.com](https://docs.marblejs.com) and follow [@marble_js](https://twitter.com/marble_js) on Twitter. 21 | 22 | License: MIT 23 | -------------------------------------------------------------------------------- /packages/http/jest.config.js: -------------------------------------------------------------------------------- 1 | const config = require('../../jest.config'); 2 | module.exports = config; 3 | -------------------------------------------------------------------------------- /packages/http/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@marblejs/http", 3 | "version": "4.1.0", 4 | "description": "HTTP module for Marble.js", 5 | "main": "./dist/index.js", 6 | "types": "./dist/index.d.ts", 7 | "scripts": { 8 | "start": "yarn watch", 9 | "watch": "tsc -w", 10 | "build": "tsc", 11 | "clean": "rimraf dist", 12 | "test": "jest --config ./jest.config.js" 13 | }, 14 | "files": [ 15 | "dist/" 16 | ], 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/marblejs/marble.git" 20 | }, 21 | "engines": { 22 | "node": ">= 8.0.0", 23 | "yarn": ">= 1.7.0", 24 | "npm": ">= 5.0.0" 25 | }, 26 | "author": "marblejs", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/marblejs/marble/issues" 30 | }, 31 | "homepage": "https://github.com/marblejs/marble#readme", 32 | "peerDependencies": { 33 | "@marblejs/core": "^4.0.0", 34 | "fp-ts": "^2.13.1", 35 | "rxjs": "^7.5.7" 36 | }, 37 | "dependencies": { 38 | "@types/json-schema": "^7.0.3", 39 | "@types/qs": "^6.9.0", 40 | "@types/uuid": "^7.0.0", 41 | "chalk": "^3.0.0", 42 | "file-type": "^8.0.0", 43 | "mime": "^2.4.4", 44 | "path-to-regexp": "^6.1.0", 45 | "qs": "^6.9.1", 46 | "uuid": "^7.0.1" 47 | }, 48 | "devDependencies": { 49 | "@marblejs/core": "^4.1.0", 50 | "@types/file-type": "^5.2.1", 51 | "@types/mime": "^2.0.1" 52 | }, 53 | "publishConfig": { 54 | "access": "public" 55 | }, 56 | "gitHead": "208643c17d76a01d0a9cb304611d4474c4a3bff6" 57 | } 58 | -------------------------------------------------------------------------------- /packages/http/src/+internal/header.util.spec.ts: -------------------------------------------------------------------------------- 1 | import { createServer, Server } from 'http'; 2 | import * as O from 'fp-ts/lib/Option'; 3 | import { getHeaderValue, normalizeHeaders } from './header.util'; 4 | import { closeServer, getServerAddress } from './server.util'; 5 | import { createHttpRequest } from './testing.util'; 6 | 7 | test('#getHeaderValue', () => { 8 | const req = createHttpRequest({ 9 | headers: { 10 | 'x-test-1': 'a', 11 | 'x-test-2': ['b', 'c'], 12 | } 13 | }); 14 | 15 | expect(getHeaderValue('x-test-1')(req.headers)).toEqual(O.some('a')); 16 | expect(getHeaderValue('x-test-2')(req.headers)).toEqual(O.some('b')); 17 | expect(getHeaderValue('x-test-3')(req.headers)).toEqual(O.none); 18 | }); 19 | 20 | test('#getServerAddress', async () => { 21 | const server = await new Promise(res => { 22 | const server = createServer(); 23 | server.listen(() => res(server)); 24 | }); 25 | 26 | expect(getServerAddress(server)).toEqual({ 27 | port: expect.any(Number), 28 | host: '127.0.0.1', 29 | }); 30 | 31 | await closeServer(server)(); 32 | }); 33 | 34 | test('#normalizeHeaders', () => { 35 | expect(normalizeHeaders({ 36 | 'Content-Type': 'application/json', 37 | 'Authorization': 'Bearer ABC123', 38 | 'x-test-1': 'test-123', 39 | })).toEqual({ 40 | 'content-type': 'application/json', 41 | 'authorization': 'Bearer ABC123', 42 | 'x-test-1': 'test-123', 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /packages/http/src/+internal/header.util.ts: -------------------------------------------------------------------------------- 1 | import * as O from 'fp-ts/lib/Option'; 2 | import * as A from 'fp-ts/lib/Array'; 3 | import { pipe } from 'fp-ts/lib/function'; 4 | import { HttpHeaders } from '../http.interface'; 5 | 6 | /** 7 | * Get header value for given key from provided headers object 8 | * 9 | * @param key header key 10 | * @since 4.0.0 11 | */ 12 | export const getHeaderValue = (key: string) => (headers: HttpHeaders): O.Option => 13 | pipe( 14 | O.fromNullable(headers[key] ?? headers[key.toLowerCase()]), 15 | O.chain(value => Array.isArray(value) 16 | ? A.head(value) as O.Option 17 | : O.some(String(value)) as O.Option), 18 | ); 19 | 20 | /** 21 | * Normalize HTTP headers (transform keys to lowercase) 22 | * 23 | * @param headers 24 | * @since 4.0.0 25 | */ 26 | export const normalizeHeaders = (headers: HttpHeaders): HttpHeaders => 27 | pipe( 28 | Object.entries(headers).map(([key, value]) => [key.toLowerCase(), value]), 29 | Object.fromEntries, 30 | ); 31 | -------------------------------------------------------------------------------- /packages/http/src/+internal/metadata.util.ts: -------------------------------------------------------------------------------- 1 | import { JSONSchema7 } from 'json-schema'; 2 | import { IO } from 'fp-ts/lib/IO'; 3 | import { createUuid } from '@marblejs/core/dist/+internal/utils'; 4 | import { HttpHeaders } from '../http.interface'; 5 | import { MARBLE_HTTP_REQUEST_METADATA_ENV_KEY } from '../http.config'; 6 | import { getHeaderValue } from './header.util'; 7 | 8 | export interface RequestMetadata { 9 | path?: string; 10 | body?: JSONSchema7; 11 | headers?: JSONSchema7; 12 | params?: JSONSchema7; 13 | query?: JSONSchema7; 14 | } 15 | 16 | export const HTTP_REQUEST_METADATA_ID_HEADER_KEY = 'X-Request-Metadata-Id'; 17 | 18 | /** 19 | * Creates random request metadata header entry 20 | * 21 | * @returns headers `HttpHeaders` 22 | */ 23 | export const createRequestMetadataHeader: IO = (): HttpHeaders => ({ 24 | [HTTP_REQUEST_METADATA_ID_HEADER_KEY]: createUuid(), 25 | }); 26 | 27 | /** 28 | * Get custom request metadata header value 29 | * 30 | * @param headers `HttpHeaders` 31 | * @returns optional header value 32 | */ 33 | export const getHttpRequestMetadataIdHeader = 34 | getHeaderValue(HTTP_REQUEST_METADATA_ID_HEADER_KEY); 35 | 36 | /** 37 | * Activates `MARBLE_HTTP_REQUEST_METADATA_ENV_KEY` environment variable 38 | * 39 | * @returns `void` 40 | */ 41 | export const enableHttpRequestMetadata: IO = () => 42 | process.env[MARBLE_HTTP_REQUEST_METADATA_ENV_KEY] = 'true'; 43 | -------------------------------------------------------------------------------- /packages/http/src/+internal/server.util.spec.ts: -------------------------------------------------------------------------------- 1 | import { createServer, Server } from 'http'; 2 | import { closeServer, getServerAddress } from './server.util'; 3 | 4 | test('#getServerAddress', async () => { 5 | const server = await new Promise(res => { 6 | const server = createServer(); 7 | server.listen(() => res(server)); 8 | }); 9 | 10 | expect(getServerAddress(server)).toEqual({ 11 | port: expect.any(Number), 12 | host: '127.0.0.1', 13 | }); 14 | 15 | await closeServer(server)(); 16 | }); 17 | -------------------------------------------------------------------------------- /packages/http/src/+internal/server.util.ts: -------------------------------------------------------------------------------- 1 | import { AddressInfo } from 'net'; 2 | import { Task } from 'fp-ts/lib/Task'; 3 | import { HttpServer } from '../http.interface'; 4 | 5 | export const closeServer = (server: HttpServer): Task => () => 6 | new Promise((res, rej) => server.close(err => err ? rej(err) : res(undefined))); 7 | 8 | export const getServerAddress = (server: HttpServer): { host: string; port: number } => { 9 | const serverAddressInfo = server.address() as AddressInfo; 10 | const host = serverAddressInfo.address === '::' ? '127.0.0.1' : serverAddressInfo.address; 11 | const port = serverAddressInfo.port; 12 | 13 | return { host, port }; 14 | }; 15 | -------------------------------------------------------------------------------- /packages/http/src/+internal/urlEncoded.util.ts: -------------------------------------------------------------------------------- 1 | import * as qs from 'qs'; 2 | import { isString } from '@marblejs/core/dist/+internal/utils'; 3 | 4 | export const transformUrlEncoded = (data: any): string => 5 | !isString(data) ? qs.stringify(data) : data; 6 | -------------------------------------------------------------------------------- /packages/http/src/effects/http.effects.interface.ts: -------------------------------------------------------------------------------- 1 | import { Event, Effect } from '@marblejs/core'; 2 | import { HttpRequest, HttpStatus, HttpHeaders, HttpServer, WithHttpRequest } from '../http.interface'; 3 | 4 | export interface HttpEffectResponse { 5 | request?: HttpRequest; 6 | status?: HttpStatus; 7 | headers?: HttpHeaders; 8 | body?: T; 9 | } 10 | 11 | export interface HttpMiddlewareEffect< 12 | I extends HttpRequest = HttpRequest, 13 | O extends HttpRequest = HttpRequest, 14 | > extends HttpEffect {} 15 | 16 | export interface HttpErrorEffect< 17 | Err extends Error = Error, 18 | Req extends HttpRequest = HttpRequest, 19 | > extends HttpEffect< 20 | WithHttpRequest<{ error: Err }, Req>, 21 | WithHttpRequest 22 | > {} 23 | 24 | export interface HttpServerEffect< 25 | Ev extends Event = Event 26 | > extends HttpEffect {} 27 | 28 | export interface HttpOutputEffect< 29 | Req extends HttpRequest = HttpRequest, 30 | > extends HttpEffect< 31 | WithHttpRequest, 32 | WithHttpRequest 33 | > {} 34 | 35 | export interface HttpEffect< 36 | I = HttpRequest, 37 | O = HttpEffectResponse, 38 | > extends Effect {} 39 | -------------------------------------------------------------------------------- /packages/http/src/effects/http.effects.ts: -------------------------------------------------------------------------------- 1 | import { matchEvent, useContext, LoggerToken, LoggerTag, LoggerLevel } from '@marblejs/core'; 2 | import { tap, map } from 'rxjs/operators'; 3 | import { ServerEvent } from '../server/http.server.event'; 4 | import { HttpServerEffect } from './http.effects.interface'; 5 | 6 | export const listening$: HttpServerEffect = (event$, ctx) => { 7 | const logger = useContext(LoggerToken)(ctx.ask); 8 | 9 | return event$.pipe( 10 | matchEvent(ServerEvent.listening), 11 | map(event => event.payload), 12 | tap(({ host, port }) => { 13 | const message = `Server running @ http://${host}:${port}/ 🚀`; 14 | const log = logger({ tag: LoggerTag.HTTP, level: LoggerLevel.INFO, type: 'Server', message }); 15 | 16 | log(); 17 | }), 18 | ); 19 | }; 20 | 21 | export const error$: HttpServerEffect = (event$, ctx) => { 22 | const logger = useContext(LoggerToken)(ctx.ask); 23 | 24 | return event$.pipe( 25 | matchEvent(ServerEvent.error), 26 | map(event => event.payload), 27 | tap(({ error }) => { 28 | const message = `Unexpected server error occured: "${error.name}", "${error.message}"`; 29 | const log = logger({ tag: LoggerTag.HTTP, level: LoggerLevel.ERROR, type: 'Server', message }); 30 | 31 | log(); 32 | }), 33 | ); 34 | }; 35 | 36 | export const close$: HttpServerEffect = (event$, ctx) => { 37 | const logger = useContext(LoggerToken)(ctx.ask); 38 | 39 | return event$.pipe( 40 | matchEvent(ServerEvent.close), 41 | map(event => event.payload), 42 | tap(() => { 43 | const message = `Server connection was closed`; 44 | const log = logger({ tag: LoggerTag.HTTP, level: LoggerLevel.INFO, type: 'Server', message }); 45 | 46 | log(); 47 | }), 48 | ); 49 | }; 50 | -------------------------------------------------------------------------------- /packages/http/src/error/http.error.effect.ts: -------------------------------------------------------------------------------- 1 | import { map } from 'rxjs/operators'; 2 | import { HttpErrorEffect } from '../effects/http.effects.interface'; 3 | import { HttpStatus } from '../http.interface'; 4 | import { HttpError, isHttpError } from './http.error.model'; 5 | 6 | interface HttpErrorResponse { 7 | error: { 8 | status: HttpStatus; 9 | message: string; 10 | data?: any; 11 | context?: string; 12 | }; 13 | } 14 | 15 | const defaultHttpError = new HttpError( 16 | 'Internal server error', 17 | HttpStatus.INTERNAL_SERVER_ERROR, 18 | ); 19 | 20 | const getStatusCode = (error: Error): HttpStatus => 21 | isHttpError(error) 22 | ? error.status 23 | : HttpStatus.INTERNAL_SERVER_ERROR; 24 | 25 | const errorFactory = (status: HttpStatus) => (error: Error): HttpErrorResponse => ({ 26 | error: isHttpError(error) 27 | ? { status, message: error.message, data: error.data, context: error.context } 28 | : { status, message: error.message }, 29 | }); 30 | 31 | export const defaultError$: HttpErrorEffect = req$ => 32 | req$.pipe( 33 | map(({ request, error = defaultHttpError }) => { 34 | const status = getStatusCode(error); 35 | const body = errorFactory(status)(error); 36 | return ({ status, body, request }); 37 | }), 38 | ); 39 | -------------------------------------------------------------------------------- /packages/http/src/error/http.error.model.spec.ts: -------------------------------------------------------------------------------- 1 | import { HttpError, isHttpError } from './http.error.model'; 2 | 3 | describe('Http error model', () => { 4 | 5 | test('#HttpError creates error object', () => { 6 | const error = new HttpError('test-message', 200); 7 | 8 | expect(error.name).toBe('HttpError'); 9 | expect(error.status).toBe(200); 10 | expect(error.message).toBe('test-message'); 11 | }); 12 | 13 | test('#isHttpError detects HttpError type', () => { 14 | const httpError = new HttpError('test-message', 200); 15 | const otherError = new Error(); 16 | 17 | expect(isHttpError(httpError)).toBe(true); 18 | expect(isHttpError(otherError)).toBe(false); 19 | }); 20 | 21 | }); 22 | -------------------------------------------------------------------------------- /packages/http/src/http.config.ts: -------------------------------------------------------------------------------- 1 | import { getEnvConfigOrElseAsBoolean } from '@marblejs/core/dist/+internal/utils'; 2 | import { IO } from 'fp-ts/lib/IO'; 3 | 4 | /** 5 | * Flag to indicate whether we should prevent converting (normalizing) all headers to lower case. 6 | * This flag was introduced to prevent breaking changes, for more details see: 7 | * https://github.com/marblejs/marble/issues/311 8 | * 9 | * Flag will be removed in the next major version, 10 | * where all headers are normalized by default. 11 | * 12 | * @since 4.0.0 13 | */ 14 | export const MARBLE_HTTP_HEADERS_NORMALIZATION_ENV_KEY = 'MARBLE_HTTP_HEADERS_NORMALIZATION'; 15 | 16 | /** 17 | * If enabled applies request metadata to every outgoing HTTP response 18 | * 19 | * @since 2.0.0 20 | */ 21 | export const MARBLE_HTTP_REQUEST_METADATA_ENV_KEY = 'MARBLE_HTTP_REQUEST_METADATA'; 22 | 23 | type HttpModuleConfiguration = Readonly<{ 24 | useHttpHeadersNormalization: IO; 25 | useHttpRequestMetadata: IO; 26 | }>; 27 | 28 | /** 29 | * Initialize and provide environment configuration 30 | * 31 | * @since 4.0.0 32 | */ 33 | export const provideConfig: IO = () => ({ 34 | useHttpHeadersNormalization: getEnvConfigOrElseAsBoolean(MARBLE_HTTP_HEADERS_NORMALIZATION_ENV_KEY, true), 35 | useHttpRequestMetadata: getEnvConfigOrElseAsBoolean(MARBLE_HTTP_REQUEST_METADATA_ENV_KEY, false), 36 | }); 37 | -------------------------------------------------------------------------------- /packages/http/src/index.ts: -------------------------------------------------------------------------------- 1 | // config 2 | export { 3 | provideConfig, 4 | MARBLE_HTTP_HEADERS_NORMALIZATION_ENV_KEY, 5 | MARBLE_HTTP_REQUEST_METADATA_ENV_KEY, 6 | } from './http.config'; 7 | 8 | // http 9 | export { defaultError$ } from './error/http.error.effect'; 10 | export { HttpError, HttpRequestError, isHttpError, isHttpRequestError } from './error/http.error.model'; 11 | export { createServer } from './server/http.server'; 12 | export { combineRoutes } from './router/http.router.combiner'; 13 | export { r } from './router/http.router.ixbuilder'; 14 | export * from './router/http.router.interface'; 15 | export * from './effects/http.effects.interface'; 16 | export * from './server/http.server.event'; 17 | export * from './server/http.server.interface'; 18 | export * from './server/http.server.listener'; 19 | export * from './http.interface'; 20 | 21 | // http - server - internal dependencies 22 | export * from './server/internal-dependencies/httpRequestMetadataStorage.reader'; 23 | export * from './server/internal-dependencies/httpServerClient.reader'; 24 | export * from './server/internal-dependencies/httpServerEventStream.reader'; 25 | export * from './server/internal-dependencies/httpRequestBus.reader'; 26 | -------------------------------------------------------------------------------- /packages/http/src/response/http.responseBody.factory.ts: -------------------------------------------------------------------------------- 1 | import { Stream } from 'stream'; 2 | import { pipe } from 'fp-ts/lib/function'; 3 | import { bufferFrom, isStream, stringifyJson } from '@marblejs/core/dist/+internal/utils'; 4 | import { HttpHeaders } from '../http.interface'; 5 | import { ContentType, getContentTypeUnsafe, isJsonContentType } from '../+internal/contentType.util'; 6 | import { transformUrlEncoded } from '../+internal/urlEncoded.util'; 7 | 8 | export type ResponseBodyFactory = (data: { headers: HttpHeaders, body: any }) => string | Stream | Buffer; 9 | 10 | export const factorizeBody: ResponseBodyFactory = ({ headers, body }) => { 11 | const contentType = getContentTypeUnsafe(headers); 12 | 13 | if (isStream(body)) 14 | return body; 15 | 16 | if (isJsonContentType(contentType)) 17 | return stringifyJson(body); 18 | 19 | switch (contentType) { 20 | case ContentType.APPLICATION_X_WWW_FORM_URLENCODED: 21 | return transformUrlEncoded(body); 22 | case ContentType.APPLICATION_OCTET_STREAM: 23 | return !Buffer.isBuffer(body) 24 | ? pipe(body, stringifyJson, bufferFrom) 25 | : body; 26 | case ContentType.TEXT_PLAIN: 27 | return String(body); 28 | default: 29 | return body; 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /packages/http/src/router/http.router.combiner.ts: -------------------------------------------------------------------------------- 1 | import { HttpMiddlewareEffect } from '../effects/http.effects.interface'; 2 | import { RouteCombinerConfig, RouteEffectGroup, RouteEffect, ErrorSubject } from './http.router.interface'; 3 | import { isRouteCombinerConfig, decorateMiddleware } from './http.router.helpers'; 4 | 5 | export function combineRoutes(path: string, config: RouteCombinerConfig): RouteEffectGroup; 6 | export function combineRoutes(path: string, effects: (RouteEffect | RouteEffectGroup)[]): RouteEffectGroup; 7 | export function combineRoutes( 8 | path: string, 9 | configOrEffects: RouteCombinerConfig | (RouteEffect | RouteEffectGroup)[] 10 | ): RouteEffectGroup { 11 | return { 12 | path, 13 | effects: isRouteCombinerConfig(configOrEffects) 14 | ? configOrEffects.effects 15 | : configOrEffects, 16 | middlewares: isRouteCombinerConfig(configOrEffects) 17 | ? (configOrEffects.middlewares || []) 18 | : [], 19 | }; 20 | } 21 | 22 | export const combineRouteMiddlewares = 23 | (decorate: boolean, errorSubject: ErrorSubject) => (...middlewares: HttpMiddlewareEffect[]): HttpMiddlewareEffect => (input$, ctx) => 24 | middlewares.reduce( 25 | (i$, middleware) => middleware(decorate ? decorateMiddleware(i$, errorSubject) : i$, ctx), 26 | decorate ? decorateMiddleware(input$, errorSubject) : input$, 27 | ); 28 | -------------------------------------------------------------------------------- /packages/http/src/router/http.router.effects.ts: -------------------------------------------------------------------------------- 1 | import { throwError } from 'rxjs'; 2 | import { mergeMap } from 'rxjs/operators'; 3 | import { HttpError } from '../error/http.error.model'; 4 | import { HttpStatus } from '../http.interface'; 5 | import { r } from './http.router.ixbuilder'; 6 | 7 | export const ROUTE_NOT_FOUND_ERROR = new HttpError('Route not found', HttpStatus.NOT_FOUND); 8 | 9 | export const notFound$ = r.pipe( 10 | r.matchPath('*'), 11 | r.matchType('*'), 12 | r.useEffect(req$ => req$.pipe(mergeMap(() => throwError(() => ROUTE_NOT_FOUND_ERROR)))), 13 | r.applyMeta({ overridable: true })); 14 | -------------------------------------------------------------------------------- /packages/http/src/router/http.router.matcher.ts: -------------------------------------------------------------------------------- 1 | import { HttpMethod } from '../http.interface'; 2 | import { BootstrappedRouting, RouteMatched } from './http.router.interface'; 3 | 4 | export const matchRoute = (routing: BootstrappedRouting) => (url: string, method: HttpMethod): RouteMatched | undefined => { 5 | for (let i = 0; i < routing.length; ++i) { 6 | const { regExp, methods, path } = routing[i]; 7 | const match = url.match(regExp); 8 | 9 | if (!match) { continue; } 10 | 11 | const matchedMethod = methods[method] || methods['*']; 12 | 13 | if (!matchedMethod) { continue; } 14 | 15 | const params = {}; 16 | 17 | if (matchedMethod.parameters) { 18 | for (let p = 0; p < matchedMethod.parameters.length; p++) { 19 | params[matchedMethod.parameters[p]] = decodeURIComponent(match[p + 1]); 20 | } 21 | } 22 | 23 | return { 24 | subject: matchedMethod.subject, 25 | params, 26 | path, 27 | }; 28 | } 29 | 30 | return undefined; 31 | }; 32 | -------------------------------------------------------------------------------- /packages/http/src/router/http.router.params.factory.ts: -------------------------------------------------------------------------------- 1 | import { pathToRegexp, Key } from 'path-to-regexp'; 2 | import { ParametricRegExp } from './http.router.interface'; 3 | 4 | export const factorizeRegExpWithParams = (path: string): ParametricRegExp => { 5 | const keys: Key[] = []; 6 | const preparedPath = path 7 | .replace(/\/\*/g, '/(.*)') /* Transfer wildcards */ 8 | .replace(/\/\/+/g, '/') /* Remove repeated backslashes */ 9 | .replace(/\/$/, ''); /* Remove trailing backslash */ 10 | 11 | const regExp = pathToRegexp(preparedPath, keys, { strict: false }); 12 | const regExpParameters = keys 13 | .filter(key => key.name !== 0) /* Filter wildcard groups */ 14 | .map(key => String(key.name)); 15 | 16 | return { 17 | regExp, 18 | parameters: regExpParameters.length > 0 ? regExpParameters : undefined, 19 | path: preparedPath, 20 | }; 21 | }; 22 | -------------------------------------------------------------------------------- /packages/http/src/router/http.router.query.factory.ts: -------------------------------------------------------------------------------- 1 | import * as qs from 'qs'; 2 | import { pipe } from 'fp-ts/lib/function'; 3 | import { fromNullable, map, getOrElse } from 'fp-ts/lib/Option'; 4 | import { QueryParameters } from '../http.interface'; 5 | 6 | export const queryParamsFactory = (queryParams: string | undefined | null): QueryParameters => pipe( 7 | fromNullable(queryParams), 8 | map(qs.parse), 9 | getOrElse(() => ({})), 10 | ); 11 | -------------------------------------------------------------------------------- /packages/http/src/router/specs/http.router.helper.spec.ts: -------------------------------------------------------------------------------- 1 | import { isRouteEffectGroup, isRouteCombinerConfig } from '../http.router.helpers'; 2 | 3 | describe('Router helper', () => { 4 | 5 | test('#isRouteGroup checks if provided argument is typeof RouteGroup', () => { 6 | expect(isRouteEffectGroup({ path: '/', effects: [], middlewares: [] })).toEqual(true); 7 | expect(isRouteEffectGroup({ path: '/', effects: [] })).toEqual(false); 8 | expect(isRouteEffectGroup({ path: '/', method: 'GET', effect: req$ => req$ })).toEqual(false); 9 | }); 10 | 11 | test('#isRouteCombinerConfig checks if provided argument is typeof RouteCombinerConfig', () => { 12 | expect(isRouteCombinerConfig({ effects: [], middlewares: [] })).toEqual(true); 13 | expect(isRouteCombinerConfig([req$ => req$])).toEqual(false); 14 | }); 15 | 16 | }); 17 | -------------------------------------------------------------------------------- /packages/http/src/server/http.server.interface.ts: -------------------------------------------------------------------------------- 1 | import * as https from 'https'; 2 | import { ServerConfig } from '@marblejs/core'; 3 | import { HttpServerEffect } from '../effects/http.effects.interface'; 4 | import { httpListener } from './http.server.listener'; 5 | 6 | export const DEFAULT_HOSTNAME = '127.0.0.1'; 7 | 8 | type HttpListenerFn = ReturnType; 9 | 10 | export interface CreateServerConfig extends ServerConfig { 11 | port?: number; 12 | hostname?: string; 13 | options?: ServerOptions; 14 | } 15 | 16 | export interface ServerOptions { 17 | httpsOptions?: https.ServerOptions; 18 | } 19 | -------------------------------------------------------------------------------- /packages/http/src/server/internal-dependencies/httpRequestBus.reader.ts: -------------------------------------------------------------------------------- 1 | import { Subject } from 'rxjs'; 2 | import { createContextToken, createReader } from '@marblejs/core'; 3 | import { HttpRequest } from '../../http.interface'; 4 | 5 | export type HttpRequestBus = ReturnType; 6 | 7 | export const HttpRequestBusToken = createContextToken('HttpRequestBus'); 8 | 9 | export const HttpRequestBus = createReader(_ => new Subject()); 10 | -------------------------------------------------------------------------------- /packages/http/src/server/internal-dependencies/httpServerClient.reader.ts: -------------------------------------------------------------------------------- 1 | import { createContextToken, createReader } from '@marblejs/core'; 2 | import { HttpServer } from '../../http.interface'; 3 | import { HttpRequestBus } from './httpRequestBus.reader'; 4 | 5 | export type HttpServerClient = ReturnType; 6 | 7 | export const HttpServerClientToken = createContextToken('HttpServerClient'); 8 | 9 | export const HttpServerClient = (httpServer: HttpServer) => createReader(_ => httpServer); 10 | -------------------------------------------------------------------------------- /packages/http/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist/", 5 | "rootDir": "./src" 6 | }, 7 | "include": ["src/**/*.ts"], 8 | "references": [ 9 | { "path": "../core" } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /packages/messaging/README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Marble.js logo 4 | 5 |

6 | 7 | # @marblejs/messaging 8 | 9 | A messaging module for [Marble.js](https://github.com/marblejs/marble). 10 | 11 | ## Installation 12 | 13 | ``` 14 | $ npm i @marblejs/messaging 15 | ``` 16 | Requires `@marblejs/core`, `rxjs` and `fp-ts` to be installed. 17 | 18 | ## Documentation 19 | 20 | For the latest updates, documentation, change log, and release information visit [docs.marblejs.com](https://docs.marblejs.com) and follow [@marble_js](https://twitter.com/marble_js) on Twitter. 21 | 22 | 23 | License: MIT 24 | -------------------------------------------------------------------------------- /packages/messaging/jest.config.js: -------------------------------------------------------------------------------- 1 | const config = require('../../jest.config'); 2 | module.exports = config; 3 | -------------------------------------------------------------------------------- /packages/messaging/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@marblejs/messaging", 3 | "version": "4.1.0", 4 | "description": "Messaging module for Marble.js", 5 | "main": "./dist/index.js", 6 | "types": "./dist/index.d.ts", 7 | "scripts": { 8 | "start": "yarn watch", 9 | "watch": "tsc -w", 10 | "build": "tsc", 11 | "clean": "rimraf dist", 12 | "test": "jest --config ./jest.config.js" 13 | }, 14 | "files": [ 15 | "dist/" 16 | ], 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/marblejs/marble.git" 20 | }, 21 | "engines": { 22 | "node": ">= 8.0.0", 23 | "yarn": ">= 1.7.0", 24 | "npm": ">= 5.0.0" 25 | }, 26 | "author": "Józef Flakus ", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/marblejs/marble/issues" 30 | }, 31 | "homepage": "https://github.com/marblejs/marble#readme", 32 | "peerDependencies": { 33 | "@marblejs/core": "^4.0.0", 34 | "fp-ts": "^2.13.1", 35 | "rxjs": "^7.5.7" 36 | }, 37 | "dependencies": { 38 | "chalk": "~2.4.1" 39 | }, 40 | "devDependencies": { 41 | "@marblejs/core": "^4.1.0", 42 | "@marblejs/middleware-io": "^4.1.0", 43 | "@types/amqp-connection-manager": "^2.0.4", 44 | "@types/amqplib": "^0.5.11", 45 | "@types/redis": "^2.8.14", 46 | "amqp-connection-manager": "^3.2.0", 47 | "amqplib": "^0.5.3", 48 | "redis": "^3.1.1" 49 | }, 50 | "publishConfig": { 51 | "access": "public" 52 | }, 53 | "gitHead": "208643c17d76a01d0a9cb304611d4474c4a3bff6" 54 | } 55 | -------------------------------------------------------------------------------- /packages/messaging/src/+internal/testing/index.ts: -------------------------------------------------------------------------------- 1 | export * from './messaging.testBed'; 2 | -------------------------------------------------------------------------------- /packages/messaging/src/+internal/testing/messaging.testBed.ts: -------------------------------------------------------------------------------- 1 | import { Microservice } from '../../server/messaging.server.interface'; 2 | import { TransportLayerConnection } from '../../transport/transport.interface'; 3 | 4 | export const createMicroserviceTestBed = (microservice: Promise) => { 5 | let connection: TransportLayerConnection; 6 | 7 | const getInstance = () => connection; 8 | 9 | beforeAll(async () => { 10 | const app = await microservice; 11 | connection = await app(); 12 | }); 13 | 14 | afterAll(async () => connection.close()); 15 | 16 | return { 17 | getInstance, 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /packages/messaging/src/ack/ack.spec.ts: -------------------------------------------------------------------------------- 1 | import { createEffectContext, contextFactory, lookup, Event, bindTo } from '@marblejs/core'; 2 | import { TransportLayerConnection } from '../transport/transport.interface'; 3 | import { EventTimerStoreToken, EventTimerStore } from '../eventStore/eventTimerStore'; 4 | import { ackEvent, nackEvent, nackAndResendEvent } from './ack'; 5 | 6 | const prepareEffectContext = async () => { 7 | const ctx = await contextFactory(bindTo(EventTimerStoreToken)(EventTimerStore)); 8 | const ask = lookup(ctx); 9 | const client = { 10 | ackMessage: jest.fn(), 11 | nackMessage: jest.fn(), 12 | } as unknown as TransportLayerConnection; 13 | 14 | return createEffectContext({ ask, client }); 15 | }; 16 | 17 | describe('#ackEvent, #nackEvent, #nackAndRequeueEvent', () => { 18 | test('handles event with empty metadata', async () => { 19 | // given 20 | const ctx = await prepareEffectContext(); 21 | const event: Event = { type: 'TEST' }; 22 | 23 | // when 24 | const result = Promise.all([ 25 | ackEvent(ctx)(event)(), 26 | nackEvent(ctx)(event)(), 27 | nackAndResendEvent(ctx)(event)(), 28 | ]); 29 | 30 | expect(result).resolves.toEqual([true, true, true]); 31 | }); 32 | 33 | test('handles event with metadata defined', async () => { 34 | // given 35 | const ctx = await prepareEffectContext(); 36 | const event: Event = { type: 'TEST', metadata: { correlationId: '123', raw: {} } }; 37 | 38 | // when 39 | const result = Promise.all([ 40 | ackEvent(ctx)(event)(), 41 | nackEvent(ctx)(event)(), 42 | nackAndResendEvent(ctx)(event)(), 43 | ]); 44 | 45 | expect(result).resolves.toEqual([true, true, true]); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /packages/messaging/src/effects/messaging.effects.interface.ts: -------------------------------------------------------------------------------- 1 | import { Effect, Event } from '@marblejs/core'; 2 | import { TransportLayerConnection } from '../transport/transport.interface'; 3 | 4 | type MsgClient = TransportLayerConnection; 5 | 6 | export interface MsgMiddlewareEffect< 7 | I = Event, 8 | O = Event, 9 | > extends MsgEffect {} 10 | 11 | export interface MsgErrorEffect< 12 | Err extends Error = Error, 13 | > extends MsgEffect {} 14 | 15 | export interface MsgEffect< 16 | I = Event, 17 | O = Event, 18 | Client = MsgClient, 19 | > extends Effect {} 20 | 21 | export interface MsgOutputEffect< 22 | I = Event, 23 | O = Event, 24 | > extends MsgEffect {} 25 | 26 | export interface MsgServerEffect 27 | extends MsgEffect {} 28 | -------------------------------------------------------------------------------- /packages/messaging/src/eventbus/messaging.eventBusClient.reader.ts: -------------------------------------------------------------------------------- 1 | import { createContextToken, createReader, useContext, LoggerToken, LoggerTag, LoggerLevel } from '@marblejs/core'; 2 | import { MessagingClient } from '../client/messaging.client'; 3 | 4 | export interface EventBusClient extends MessagingClient {} 5 | 6 | export const EventBusClientToken = createContextToken('EventBusClient'); 7 | 8 | /** 9 | * `EventBusClient` has to be registered eagerly after main `EventBus` 10 | * @returns asynchronous reader of `EventBus` 11 | * @since v3.0 12 | */ 13 | export const EventBusClient = createReader(ask => { 14 | const logger = useContext(LoggerToken)(ask); 15 | 16 | const logWarning = logger({ 17 | tag: LoggerTag.EVENT_BUS, 18 | level: LoggerLevel.WARN, 19 | type: 'eventBusClient', 20 | message: '"EventBus" requires to be registered eagerly before "EventBusClient" reader.', 21 | }); 22 | 23 | logWarning(); 24 | }); 25 | 26 | /** 27 | * An alias for `EventBusClient` 28 | * 29 | * @deprecated since version `v4.0`. Use `EventBusClient` instead. 30 | * Will be removed in version `v5.0` 31 | */ 32 | export const eventBusClient = EventBusClient; 33 | -------------------------------------------------------------------------------- /packages/messaging/src/index.spec.ts: -------------------------------------------------------------------------------- 1 | import * as API from './index'; 2 | 3 | describe('@marblejs/messaging', () => { 4 | test('public APIs are defined', () => { 5 | expect(API.Transport).toBeDefined(); 6 | expect(API.TransportLayerToken).toBeDefined(); 7 | expect(API.ServerEvent).toBeDefined(); 8 | expect(API.createMicroservice).toBeDefined(); 9 | expect(API.MessagingClient).toBeDefined(); 10 | expect(API.messagingListener).toBeDefined(); 11 | expect(API.EventBus).toBeDefined(); 12 | expect(API.EventBusClient).toBeDefined(); 13 | expect(API.EventBusClientToken).toBeDefined(); 14 | expect(API.EventBusToken).toBeDefined(); 15 | expect(API.reply).toBeDefined(); 16 | expect(API.ackEvent).toBeDefined(); 17 | expect(API.nackEvent).toBeDefined(); 18 | expect(API.nackAndResendEvent).toBeDefined(); 19 | expect(API.EVENT_BUS_CHANNEL).toBeDefined(); 20 | expect(API.AmqpConnectionStatus).toBeDefined(); 21 | expect(API.RedisConnectionStatus).toBeDefined(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /packages/messaging/src/index.ts: -------------------------------------------------------------------------------- 1 | // client 2 | export * from './client/messaging.client'; 3 | 4 | // transport 5 | export * from './transport/transport.interface'; 6 | export { AmqpStrategyOptions, AmqpConnectionStatus } from './transport/strategies/amqp.strategy.interface'; 7 | export { RedisStrategyOptions, RedisConnectionStatus } from './transport/strategies/redis.strategy.interface'; 8 | export { LocalStrategyOptions, EVENT_BUS_CHANNEL } from './transport/strategies/local.strategy.interface'; 9 | 10 | // effects, middlewares 11 | export * from './effects/messaging.effects.interface'; 12 | 13 | // handy functions 14 | export { reply } from './reply/reply'; 15 | export { ackEvent, nackEvent, nackAndResendEvent } from './ack/ack'; 16 | 17 | // server 18 | export * from './server/messaging.server'; 19 | export * from './server/messaging.server.interface'; 20 | export * from './server/messaging.server.tokens'; 21 | export * from './server/messaging.server.events'; 22 | export { messagingListener } from './server/messaging.server.listener'; 23 | 24 | // readers 25 | export * from './eventbus/messaging.eventBus.reader'; 26 | export * from './eventbus/messaging.eventBusClient.reader'; 27 | export * from './eventStore/eventTimerStore'; 28 | -------------------------------------------------------------------------------- /packages/messaging/src/middlewares/messaging.eventInput.middleware.ts: -------------------------------------------------------------------------------- 1 | import { map } from 'rxjs/operators'; 2 | import { createUuid } from '@marblejs/core/dist/+internal/utils'; 3 | import { MsgMiddlewareEffect } from '../effects/messaging.effects.interface'; 4 | 5 | export const idApplier$: MsgMiddlewareEffect = event$ => 6 | event$.pipe( 7 | map(event => ({ 8 | ...event, 9 | metadata: { 10 | ...event.metadata, 11 | correlationId: event.metadata?.correlationId ?? createUuid(), 12 | }, 13 | })) 14 | ); 15 | -------------------------------------------------------------------------------- /packages/messaging/src/middlewares/messaging.eventOutput.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Event, useContext } from '@marblejs/core'; 2 | import { createUuid, isNonNullable, encodeError } from '@marblejs/core/dist/+internal/utils'; 3 | import { map } from 'rxjs/operators'; 4 | import { MsgOutputEffect } from '../effects/messaging.effects.interface'; 5 | import { TransportLayerToken } from '../server/messaging.server.tokens'; 6 | 7 | export const outputRouter$: MsgOutputEffect = (event$, ctx) => { 8 | const transportLayer = useContext(TransportLayerToken)(ctx.ask); 9 | const originChannel = transportLayer.config.channel; 10 | 11 | return event$.pipe( 12 | map(event => ({ 13 | ...event, 14 | metadata: { 15 | ...event.metadata, 16 | correlationId: event.metadata?.correlationId ?? createUuid(), 17 | replyTo: event.metadata?.replyTo ?? originChannel 18 | }, 19 | })) 20 | ); 21 | }; 22 | 23 | export const outputErrorEncoder$: MsgOutputEffect> = event$ => { 24 | const hasError = (event: Event<{ error?: any }>): boolean => 25 | [event.payload?.error, event.error].some(Boolean); 26 | 27 | return event$.pipe( 28 | map(event => { 29 | if (!hasError(event)) return event; 30 | 31 | const eventError = event.error; 32 | const payloadError = event.payload?.error; 33 | 34 | if (isNonNullable(eventError)) 35 | event.error = encodeError(eventError); 36 | 37 | if (isNonNullable(payloadError)) 38 | event.payload.error = encodeError(payloadError); 39 | 40 | return event; 41 | }), 42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /packages/messaging/src/reply/reply.ts: -------------------------------------------------------------------------------- 1 | import { Event, EventMetadata } from '@marblejs/core'; 2 | import { isString, NamedError } from '@marblejs/core/dist/+internal/utils'; 3 | 4 | export const UNKNOWN_TAG = '_UNKNOWN_'; 5 | 6 | export class MissingEventTypeError extends NamedError { 7 | constructor() { 8 | super('MissingEventTypeError', `#reply - Missing type literal`); 9 | } 10 | } 11 | 12 | function assertEventType(event: Partial): asserts event is Required { 13 | if (!event.type) throw new MissingEventTypeError(); 14 | } 15 | 16 | function isEventMetadata(metadata: any): metadata is EventMetadata { 17 | return metadata.correlationId || metadata.replyTo; 18 | } 19 | 20 | const composeMetadata = (originMetadata?: EventMetadata) => (customMetadata?: EventMetadata): EventMetadata => ({ 21 | raw: originMetadata?.raw ?? customMetadata?.raw, 22 | correlationId: originMetadata?.correlationId ?? customMetadata?.correlationId, 23 | replyTo: originMetadata?.replyTo ?? customMetadata?.replyTo ?? UNKNOWN_TAG, 24 | }); 25 | 26 | export const reply = (to: string | EventMetadata | Event) => (event: T): T => { 27 | assertEventType(event); 28 | 29 | return isString(to) 30 | ? { ...event, metadata: composeMetadata(event.metadata)({ replyTo: to }) } 31 | : isEventMetadata(to) 32 | ? { ...event, metadata: composeMetadata(event.metadata)(to) } 33 | : { ...to, ...event, metadata: composeMetadata(event.metadata)(to.metadata) }; 34 | }; 35 | -------------------------------------------------------------------------------- /packages/messaging/src/server/messaging.server.events.ts: -------------------------------------------------------------------------------- 1 | import { createEvent, EventsUnion, Event } from '@marblejs/core'; 2 | 3 | export enum ServerEventType { 4 | STATUS = 'status', 5 | CLOSE = 'close', 6 | ERROR = 'error', 7 | } 8 | 9 | export const ServerEvent = { 10 | status: createEvent( 11 | ServerEventType.STATUS, 12 | (host: string, channel: string, type: string) => ({ host, channel, type }), 13 | ), 14 | close: createEvent( 15 | ServerEventType.CLOSE, 16 | ), 17 | error: createEvent( 18 | ServerEventType.ERROR, 19 | (error: Error) => ({ error }), 20 | ) 21 | }; 22 | 23 | export type AllServerEvents = EventsUnion; 24 | 25 | export function isStatusEvent(event: Event): event is ReturnType { 26 | return event.type === ServerEventType.STATUS; 27 | } 28 | 29 | export function isCloseEvent(event: Event): event is ReturnType { 30 | return event.type === ServerEventType.CLOSE; 31 | } 32 | 33 | export function isErrorEvent(event: Event): event is ReturnType { 34 | return event.type === ServerEventType.ERROR; 35 | } 36 | -------------------------------------------------------------------------------- /packages/messaging/src/server/messaging.server.interface.ts: -------------------------------------------------------------------------------- 1 | import { ServerConfig, ServerIO } from '@marblejs/core'; 2 | import { MsgServerEffect } from '../effects/messaging.effects.interface'; 3 | import { TransportStrategy, TransportLayerConnection } from '../transport/transport.interface'; 4 | import { messagingListener } from './messaging.server.listener'; 5 | 6 | type MessagingListenerFn = ReturnType; 7 | type ConfigurationBase = ServerConfig; 8 | 9 | export type CreateMicroserviceConfig = 10 | & TransportStrategy 11 | & ConfigurationBase 12 | ; 13 | 14 | export type Microservice = ServerIO; 15 | -------------------------------------------------------------------------------- /packages/messaging/src/server/messaging.server.tokens.ts: -------------------------------------------------------------------------------- 1 | import { createContextToken } from '@marblejs/core'; 2 | import { Subject } from 'rxjs'; 3 | import { TransportLayer } from '../transport/transport.interface'; 4 | import { AllServerEvents } from './messaging.server.events'; 5 | 6 | export const TransportLayerToken = createContextToken('TransportLayerToken'); 7 | export const ServerEventsToken = createContextToken>('ServerEventsToken'); 8 | -------------------------------------------------------------------------------- /packages/messaging/src/transport/strategies/amqp.strategy.interface.ts: -------------------------------------------------------------------------------- 1 | import { NamedError } from '@marblejs/core/dist/+internal/utils'; 2 | import { Transport } from '../transport.interface'; 3 | 4 | export interface AmqpStrategy { 5 | transport: Transport.AMQP; 6 | options: AmqpStrategyOptions; 7 | } 8 | 9 | export interface AmqpStrategyOptions { 10 | host: string; 11 | queue: string; 12 | queueOptions?: { 13 | exclusive?: boolean; 14 | durable?: boolean; 15 | autoDelete?: boolean; 16 | arguments?: any; 17 | messageTtl?: number; 18 | expires?: number; 19 | deadLetterExchange?: string; 20 | deadLetterRoutingKey?: string; 21 | maxLength?: number; 22 | maxPriority?: number; 23 | }; 24 | prefetchCount?: number; 25 | expectAck?: boolean; 26 | timeout?: number; 27 | } 28 | 29 | export enum AmqpConnectionStatus { 30 | CONNECTED = 'CONNECTED', 31 | CHANNEL_CONNECTED = 'CHANNEL_CONNECTED', 32 | CONNECTION_LOST = 'CONNECTION_LOST', 33 | CHANNEL_CONNECTION_LOST = 'CHANNEL_CONNECTION_LOST', 34 | } 35 | 36 | export enum AmqpErrorType { 37 | CANNOT_SET_ACK_FOR_NON_CONSUMER_CONNECTION = 'AmqpCannotSetExpectAckForNonConsumerConnection', 38 | } 39 | 40 | export class AmqpCannotSetExpectAckForNonConsumerConnection extends NamedError { 41 | constructor() { 42 | super( 43 | AmqpErrorType.CANNOT_SET_ACK_FOR_NON_CONSUMER_CONNECTION, 44 | `Non consumer connections cannot set "expectAck" attribute`, 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/messaging/src/transport/strategies/local.strategy.interface.ts: -------------------------------------------------------------------------------- 1 | import { Transport } from '../transport.interface'; 2 | 3 | export const EVENT_BUS_CHANNEL = 'event_bus'; 4 | 5 | export interface LocalStrategy { 6 | transport: Transport.LOCAL; 7 | options: LocalStrategyOptions; 8 | } 9 | 10 | export interface LocalStrategyOptions { 11 | timeout?: number; 12 | } 13 | -------------------------------------------------------------------------------- /packages/messaging/src/transport/strategies/redis.strategy.interface.ts: -------------------------------------------------------------------------------- 1 | import { Transport } from '../transport.interface'; 2 | 3 | export interface RedisStrategy { 4 | transport: Transport.REDIS; 5 | options: RedisStrategyOptions; 6 | } 7 | 8 | export interface RedisStrategyOptions { 9 | host: string; 10 | channel: string; 11 | port?: number; 12 | password?: string; 13 | timeout?: number; 14 | } 15 | 16 | 17 | export enum RedisConnectionStatus { 18 | READY = 'READY', 19 | CONNECT = 'CONNECT', 20 | RECONNECTING = 'RECONNECTING', 21 | END = 'END', 22 | } 23 | -------------------------------------------------------------------------------- /packages/messaging/src/transport/strategies/tcp.strategy.interface.ts: -------------------------------------------------------------------------------- 1 | import { Transport } from '../transport.interface'; 2 | 3 | export interface TcpStrategy { 4 | transport: Transport.TCP; 5 | options: TcpStrategyOptions; 6 | } 7 | 8 | export interface TcpStrategyOptions { 9 | timeout?: number; 10 | } 11 | -------------------------------------------------------------------------------- /packages/messaging/src/transport/strategies/tcp.strategy.ts: -------------------------------------------------------------------------------- 1 | 2 | import { TransportLayer, Transport } from '../transport.interface'; 3 | 4 | /* istanbul ignore next */ 5 | export const createTcpStrategy = (): TransportLayer => { 6 | // @TODO 7 | return {} as any; 8 | }; 9 | -------------------------------------------------------------------------------- /packages/messaging/src/transport/transport.error.ts: -------------------------------------------------------------------------------- 1 | import { coreErrorFactory, CoreErrorOptions } from '@marblejs/core'; 2 | import { NamedError } from '@marblejs/core/dist/+internal/utils'; 3 | 4 | export enum ErrorType { 5 | UNSUPPORTED_ERROR = 'UnsupportedError', 6 | } 7 | 8 | export class UnsupportedError extends NamedError { 9 | constructor(public readonly message: string) { 10 | super(ErrorType.UNSUPPORTED_ERROR, message); 11 | } 12 | } 13 | 14 | export const throwUnsupportedError = (transportName: string) => (method: string) => { 15 | const message = `Unsupported operation.`; 16 | const detail = `Method "${method}" is unsupported for ${transportName} transport layer.`; 17 | const error = new UnsupportedError(`${message} ${detail}`); 18 | const coreErrorOptions: CoreErrorOptions = { contextMethod: method, offset: 1 }; 19 | const coreError = coreErrorFactory(error.message, coreErrorOptions); 20 | 21 | console.error(coreError.stack); 22 | 23 | throw error; 24 | }; 25 | -------------------------------------------------------------------------------- /packages/messaging/src/transport/transport.transformer.ts: -------------------------------------------------------------------------------- 1 | import { Subject } from 'rxjs'; 2 | import { flow, identity, pipe } from 'fp-ts/lib/function'; 3 | import * as E from 'fp-ts/lib/Either'; 4 | import { Event } from '@marblejs/core'; 5 | import { TransportMessageTransformer, TransportMessage } from './transport.interface'; 6 | 7 | export type DecodeMessageConfig = { msgTransformer: TransportMessageTransformer; errorSubject: Subject }; 8 | export type DecodeMessage = (config: DecodeMessageConfig) => (msg: TransportMessage) => Event; 9 | 10 | export const jsonTransformer: TransportMessageTransformer = { 11 | decode: event => JSON.parse(event.toString()), 12 | encode: event => flow(JSON.stringify, Buffer.from)(event), 13 | }; 14 | 15 | const applyMetadata = (raw: TransportMessage) => (event: Event) => ({ 16 | ...event, 17 | metadata: { 18 | replyTo: raw.replyTo, 19 | correlationId: raw.correlationId, 20 | raw, 21 | }, 22 | }); 23 | 24 | export const decodeMessage: DecodeMessage = ({ msgTransformer, errorSubject }) => msg => 25 | pipe( 26 | E.tryCatch( 27 | () => msgTransformer.decode(msg.data), 28 | error => { 29 | errorSubject.next(error as Error); 30 | return applyMetadata(msg)({ type: 'UNKNOWN' }); 31 | }), 32 | E.map(applyMetadata(msg)), 33 | E.fold(identity, identity), 34 | ); 35 | -------------------------------------------------------------------------------- /packages/messaging/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist/", 5 | "rootDir": "./src" 6 | }, 7 | "include": ["src/**/*.ts"], 8 | "references": [ 9 | { "path": "../core" } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /packages/middleware-body/README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Marble.js logo 4 | 5 |

6 | 7 | # @marblejs/middleware-body 8 | 9 | A request body parser middleware for [Marble.js](https://github.com/marblejs/marble). 10 | 11 | ## Installation 12 | 13 | ``` 14 | $ npm i @marblejs/middleware-body 15 | ``` 16 | Requires `@marblejs/core` and `@marblejs/http` to be installed. 17 | 18 | ## Documentation 19 | 20 | For the latest updates, documentation, change log, and release information visit [docs.marblejs.com](https://docs.marblejs.com) and follow [@marble_js](https://twitter.com/marble_js) on Twitter. 21 | 22 | ## Usage 23 | 24 | ```typescript 25 | import { bodyParser$ } from '@marblejs/middleware-body'; 26 | ​ 27 | const middlewares = [ 28 | bodyParser$(), 29 | // ... 30 | ]; 31 | ​ 32 | const effects = [ 33 | // ... 34 | ]; 35 | ​ 36 | export const app = httpListener({ middlewares, effects }); 37 | ``` 38 | License: MIT 39 | -------------------------------------------------------------------------------- /packages/middleware-body/jest.config.js: -------------------------------------------------------------------------------- 1 | const config = require('../../jest.config'); 2 | module.exports = config; 3 | -------------------------------------------------------------------------------- /packages/middleware-body/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@marblejs/middleware-body", 3 | "version": "4.1.0", 4 | "description": "Body parser middleware for Marble.js", 5 | "main": "./dist/index.js", 6 | "types": "./dist/index.d.ts", 7 | "scripts": { 8 | "start": "yarn watch", 9 | "watch": "tsc -w", 10 | "build": "tsc", 11 | "test": "jest --config ./jest.config.js", 12 | "clean": "rimraf dist" 13 | }, 14 | "files": [ 15 | "dist/" 16 | ], 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/marblejs/marble.git" 20 | }, 21 | "engines": { 22 | "node": ">= 8.0.0", 23 | "yarn": ">= 1.7.0", 24 | "npm": ">= 5.0.0" 25 | }, 26 | "author": "marblejs", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/marblejs/marble/issues" 30 | }, 31 | "homepage": "https://github.com/marblejs/marble#readme", 32 | "peerDependencies": { 33 | "@marblejs/core": "^4.0.0", 34 | "@marblejs/http": "^4.0.0", 35 | "fp-ts": "^2.13.1", 36 | "rxjs": "^7.5.7" 37 | }, 38 | "dependencies": { 39 | "@types/content-type": "1.1.2", 40 | "@types/qs": "^6.5.1", 41 | "@types/type-is": "~1.6.2", 42 | "content-type": "~1.0.4", 43 | "qs": "^6.6.0", 44 | "type-is": "~1.6.16" 45 | }, 46 | "devDependencies": { 47 | "@marblejs/core": "^4.1.0", 48 | "@marblejs/http": "^4.1.0" 49 | }, 50 | "publishConfig": { 51 | "access": "public" 52 | }, 53 | "gitHead": "208643c17d76a01d0a9cb304611d4474c4a3bff6" 54 | } 55 | -------------------------------------------------------------------------------- /packages/middleware-body/src/body.middleware.ts: -------------------------------------------------------------------------------- 1 | import { HttpError, HttpStatus, HttpMiddlewareEffect } from '@marblejs/http'; 2 | import { of, throwError } from 'rxjs'; 3 | import { catchError, map, tap, mergeMap } from 'rxjs/operators'; 4 | import { pipe } from 'fp-ts/lib/function'; 5 | import { defaultParser } from './parsers'; 6 | import { RequestBodyParser } from './body.model'; 7 | import { matchType, getBody, hasBody, isMultipart } from './body.util'; 8 | 9 | const PARSEABLE_METHODS = ['POST', 'PUT', 'PATCH']; 10 | 11 | interface BodyParserOptions { 12 | parser?: RequestBodyParser; 13 | type?: string[]; 14 | } 15 | 16 | export const bodyParser$ = ({ 17 | type = ['*/*'], 18 | parser = defaultParser, 19 | }: BodyParserOptions = {}): HttpMiddlewareEffect => req$ => 20 | req$.pipe( 21 | mergeMap(req => 22 | PARSEABLE_METHODS.includes(req.method) 23 | && !hasBody(req) 24 | && !isMultipart(req) 25 | && matchType(type)(req) 26 | ? pipe( 27 | getBody(req), 28 | map(parser(req)), 29 | tap(body => req.body = body), 30 | map(() => req), 31 | catchError(error => throwError(() => 32 | new HttpError(`Request body parse error: "${error.toString()}"`, HttpStatus.BAD_REQUEST, undefined, req), 33 | ))) 34 | : of(req), 35 | ), 36 | ); 37 | -------------------------------------------------------------------------------- /packages/middleware-body/src/body.model.ts: -------------------------------------------------------------------------------- 1 | import { HttpRequest } from '@marblejs/http'; 2 | 3 | export type RequestBodyParser = (reqOrContentType: HttpRequest | string) => 4 | (body: Buffer) => Buffer | Record | Array | string | undefined; 5 | -------------------------------------------------------------------------------- /packages/middleware-body/src/body.util.ts: -------------------------------------------------------------------------------- 1 | import * as typeIs from 'type-is'; 2 | import { pipe } from 'fp-ts/lib/function'; 3 | import { Observable } from 'rxjs'; 4 | import { map, toArray } from 'rxjs/operators'; 5 | import { HttpRequest } from '@marblejs/http'; 6 | import { getContentTypeUnsafe } from '@marblejs/http/dist/+internal/contentType.util'; 7 | import { fromReadableStream } from '@marblejs/core/dist/+internal/observable'; 8 | 9 | export const matchType = (type: string[]) => (req: HttpRequest): boolean => 10 | !!typeIs.is(getContentTypeUnsafe(req.headers), type); 11 | 12 | export const isMultipart = (req: HttpRequest): boolean => 13 | getContentTypeUnsafe(req.headers).includes('multipart/'); 14 | 15 | export const hasBody = (req: HttpRequest): boolean => 16 | req.body !== undefined && req.body !== null; 17 | 18 | export const getBody = (req: HttpRequest): Observable => 19 | pipe( 20 | fromReadableStream(req), 21 | toArray(), 22 | map(chunks => Buffer.concat(chunks)), 23 | ); 24 | -------------------------------------------------------------------------------- /packages/middleware-body/src/index.ts: -------------------------------------------------------------------------------- 1 | export { bodyParser$ } from './body.middleware'; 2 | export { RequestBodyParser } from './body.model'; 3 | export { defaultParser, jsonParser, urlEncodedParser, textParser, rawParser } from './parsers'; 4 | -------------------------------------------------------------------------------- /packages/middleware-body/src/parsers/default.body.parser.ts: -------------------------------------------------------------------------------- 1 | import * as typeis from 'type-is'; 2 | import { getContentTypeUnsafe } from '@marblejs/http/dist/+internal/contentType.util'; 3 | import { RequestBodyParser } from '../body.model'; 4 | import { jsonParser } from './json.body.parser'; 5 | import { textParser } from './text.body.parser'; 6 | import { rawParser } from './raw.body.parser'; 7 | import { urlEncodedParser } from './url.body.parser'; 8 | 9 | const SUPPORTED_CONTENT_TYPES = ['json', 'urlencoded', 'application/octet-stream', 'text', 'html']; 10 | 11 | export const defaultParser: RequestBodyParser = reqOrContentType => body => { 12 | const contentType = typeof reqOrContentType === 'string' 13 | ? reqOrContentType 14 | : getContentTypeUnsafe(reqOrContentType.headers); 15 | 16 | switch (typeis.is(contentType, SUPPORTED_CONTENT_TYPES)) { 17 | case 'json': 18 | return jsonParser(reqOrContentType)(body); 19 | case 'urlencoded': 20 | return urlEncodedParser(reqOrContentType)(body); 21 | case 'application/octet-stream': 22 | return rawParser(reqOrContentType)(body); 23 | case 'text': 24 | case 'html': 25 | return textParser(reqOrContentType)(body); 26 | default: 27 | return undefined; 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /packages/middleware-body/src/parsers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './url.body.parser'; 2 | export * from './raw.body.parser'; 3 | export * from './json.body.parser'; 4 | export * from './text.body.parser'; 5 | export * from './default.body.parser'; 6 | -------------------------------------------------------------------------------- /packages/middleware-body/src/parsers/json.body.parser.ts: -------------------------------------------------------------------------------- 1 | import * as contentType from 'content-type'; 2 | import { pipe } from 'fp-ts/lib/function'; 3 | import { RequestBodyParser } from '../body.model'; 4 | 5 | export const jsonParser: RequestBodyParser = reqOrContentType => body => 6 | pipe( 7 | contentType.parse(reqOrContentType), 8 | parsedContentType => body.toString(parsedContentType.parameters.charset), 9 | stringifiedBody => JSON.parse(stringifiedBody), 10 | ); 11 | -------------------------------------------------------------------------------- /packages/middleware-body/src/parsers/raw.body.parser.ts: -------------------------------------------------------------------------------- 1 | import { RequestBodyParser } from '../body.model'; 2 | 3 | export const rawParser: RequestBodyParser = _ => body => body; 4 | -------------------------------------------------------------------------------- /packages/middleware-body/src/parsers/specs/default.body.parser.spec.ts: -------------------------------------------------------------------------------- 1 | import { defaultParser } from '../default.body.parser'; 2 | 3 | test('#defaultParser handles Content-Type as a first argument', () => { 4 | const body = { 5 | test: 'value', 6 | }; 7 | const buffer = Buffer.from(JSON.stringify(body)); 8 | expect(defaultParser('application/json')(buffer)).toEqual(body); 9 | }); 10 | -------------------------------------------------------------------------------- /packages/middleware-body/src/parsers/text.body.parser.ts: -------------------------------------------------------------------------------- 1 | import * as contentType from 'content-type'; 2 | import { pipe } from 'fp-ts/lib/function'; 3 | import { RequestBodyParser } from '../body.model'; 4 | 5 | export const textParser: RequestBodyParser = reqOrContentType => body => 6 | pipe( 7 | contentType.parse(reqOrContentType), 8 | parsedContentType => body.toString(parsedContentType.parameters.charset), 9 | ); 10 | -------------------------------------------------------------------------------- /packages/middleware-body/src/parsers/url.body.parser.ts: -------------------------------------------------------------------------------- 1 | import * as qs from 'qs'; 2 | import * as contentType from 'content-type'; 3 | import { pipe } from 'fp-ts/lib/function'; 4 | import { RequestBodyParser } from '../body.model'; 5 | 6 | export const urlEncodedParser: RequestBodyParser = reqOrContentType => body => 7 | pipe( 8 | contentType.parse(reqOrContentType), 9 | parsedContentType => body.toString(parsedContentType.parameters.charset), 10 | stringifiedBody => qs.parse(stringifiedBody), 11 | ); 12 | -------------------------------------------------------------------------------- /packages/middleware-body/src/specs/body.util.spec.ts: -------------------------------------------------------------------------------- 1 | import { createHttpRequest } from '@marblejs/http/dist/+internal/testing.util'; 2 | import { hasBody } from '../body.util'; 3 | 4 | test('#hasBody checks if request has body', () => { 5 | expect(hasBody(createHttpRequest({ body: null }))).toEqual(false); 6 | expect(hasBody(createHttpRequest({ body: undefined }))).toEqual(false); 7 | expect(hasBody(createHttpRequest({ body: 'test' }))).toEqual(true); 8 | expect(hasBody(createHttpRequest({ body: {} }))).toEqual(true); 9 | expect(hasBody(createHttpRequest({ body: 1 }))).toEqual(true); 10 | }); 11 | -------------------------------------------------------------------------------- /packages/middleware-body/src/specs/index.spec.ts: -------------------------------------------------------------------------------- 1 | import * as API from '../index'; 2 | 3 | describe('@marblejs/middleware-body public API', () => { 4 | test('apis are defined', () => { 5 | expect(API.bodyParser$).toBeDefined(); 6 | expect(API.defaultParser).toBeDefined(); 7 | expect(API.jsonParser).toBeDefined(); 8 | expect(API.rawParser).toBeDefined(); 9 | expect(API.textParser).toBeDefined(); 10 | expect(API.urlEncodedParser).toBeDefined(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /packages/middleware-body/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist/", 5 | "rootDir": "./src" 6 | }, 7 | "include": ["src/**/*.ts"], 8 | "references": [ 9 | { "path": "../core" }, 10 | { "path": "../http" } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /packages/middleware-cors/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Edouard Bozon 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 | -------------------------------------------------------------------------------- /packages/middleware-cors/README.md: -------------------------------------------------------------------------------- 1 | Middleware CORS 2 | ======= 3 | 4 | A CORS middleware for [Marble.js](https://github.com/marblejs/marble). 5 | 6 | ## Usage 7 | 8 | Example to allow incoming requests. 9 | 10 | ```typescript 11 | import { cors$ } from '@marblejs/middleware-cors'; 12 | 13 | const middlewares = [ 14 | logger$, 15 | cors$({ 16 | origin: '*', 17 | methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], 18 | optionsSuccessStatus: 204, 19 | allowHeaders: '*', 20 | maxAge: 3600, 21 | }) 22 | ]; 23 | 24 | const effects = [ 25 | endpoint1$, 26 | endpoint2$, 27 | ... 28 | ]; 29 | 30 | const app = httpListener({ middlewares, effects }); 31 | ``` 32 | 33 | ## Available options 34 | 35 | To configure CORS middleware you can follow this interface. 36 | 37 | ```typescript 38 | interface CORSOptions { 39 | origin?: string | string[] | RegExp; 40 | methods?: HttpMethod[]; 41 | optionsSuccessStatus?: HttpStatus; 42 | allowHeaders?: string | string[]; 43 | exposeHeaders?: string[]; 44 | withCredentials?: boolean; 45 | maxAge?: number; 46 | } 47 | ``` 48 | 49 | License: MIT 50 | -------------------------------------------------------------------------------- /packages/middleware-cors/jest.config.js: -------------------------------------------------------------------------------- 1 | const config = require('../../jest.config'); 2 | module.exports = config; 3 | -------------------------------------------------------------------------------- /packages/middleware-cors/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@marblejs/middleware-cors", 3 | "version": "4.1.0", 4 | "description": "A CORS middleware for Marble.js", 5 | "main": "dist/index.js", 6 | "types": "./dist/index.d.ts", 7 | "scripts": { 8 | "start": "yarn watch", 9 | "watch": "tsc -w", 10 | "build": "tsc", 11 | "test": "jest --config ./jest.config.js", 12 | "clean": "rimraf dist" 13 | }, 14 | "files": [ 15 | "dist/" 16 | ], 17 | "keywords": [ 18 | "marble.js", 19 | "cors", 20 | "middleware", 21 | "http", 22 | "rxjs" 23 | ], 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/marblejs/marble.git" 27 | }, 28 | "engines": { 29 | "node": ">= 8.0.0", 30 | "yarn": ">= 1.7.0", 31 | "npm": ">= 5.0.0" 32 | }, 33 | "author": "Edouard Bozon ", 34 | "license": "MIT", 35 | "bugs": { 36 | "url": "https://github.com/marblejs/marble/issues" 37 | }, 38 | "homepage": "https://github.com/marblejs/marble#readme", 39 | "peerDependencies": { 40 | "@marblejs/core": "^4.0.0", 41 | "@marblejs/http": "^4.0.0", 42 | "fp-ts": "^2.13.1", 43 | "rxjs": "^7.5.7" 44 | }, 45 | "devDependencies": { 46 | "@marblejs/core": "^4.1.0", 47 | "@marblejs/http": "^4.1.0" 48 | }, 49 | "publishConfig": { 50 | "access": "public" 51 | }, 52 | "gitHead": "208643c17d76a01d0a9cb304611d4474c4a3bff6" 53 | } 54 | -------------------------------------------------------------------------------- /packages/middleware-cors/src/applyHeaders.ts: -------------------------------------------------------------------------------- 1 | import { HttpResponse } from '@marblejs/http'; 2 | 3 | export interface ConfiguredHeader { 4 | key: AccessControlHeader; 5 | value: string; 6 | } 7 | 8 | export enum AccessControlHeader { 9 | Origin = 'Access-Control-Allow-Origin', 10 | Methods = 'Access-Control-Allow-Methods', 11 | Headers = 'Access-Control-Allow-Headers', 12 | Credentials = 'Access-Control-Allow-Credentials', 13 | MaxAge = 'Access-Control-Max-Age', 14 | ExposeHeaders = 'Access-Control-Expose-Headers', 15 | } 16 | 17 | export const applyHeaders = ( 18 | headers: ConfiguredHeader[], 19 | res: HttpResponse, 20 | ): void => { 21 | headers.forEach(({ key, value }) => { 22 | res.setHeader(key, value); 23 | }); 24 | }; 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /packages/middleware-cors/src/checkArrayOrigin.ts: -------------------------------------------------------------------------------- 1 | export const checkArrayOrigin = ( 2 | origin: string, 3 | option: string | string[] | RegExp, 4 | ): boolean => 5 | Array.isArray(option) && option.length > 0 && option.includes(origin) 6 | ? true 7 | : false; 8 | -------------------------------------------------------------------------------- /packages/middleware-cors/src/checkOrigin.ts: -------------------------------------------------------------------------------- 1 | import { HttpRequest } from '@marblejs/http'; 2 | import { checkStringOrigin } from './checkStringOrigin'; 3 | import { checkArrayOrigin } from './checkArrayOrigin'; 4 | import { checkRegexpOrigin } from './checkRegexpOrigin'; 5 | 6 | export const checkOrigin = ( 7 | req: HttpRequest, 8 | option: string | string[] | RegExp, 9 | ): boolean => { 10 | const origin = req.headers.origin as string; 11 | 12 | return [ 13 | checkStringOrigin, 14 | checkArrayOrigin, 15 | checkRegexpOrigin, 16 | ].some(check => check(origin, option)); 17 | }; 18 | -------------------------------------------------------------------------------- /packages/middleware-cors/src/checkRegexpOrigin.ts: -------------------------------------------------------------------------------- 1 | export const checkRegexpOrigin = ( 2 | origin: string, 3 | option: string | string[] | RegExp, 4 | ): boolean => (option instanceof RegExp && option.test(origin) ? true : false); 5 | -------------------------------------------------------------------------------- /packages/middleware-cors/src/checkStringOrigin.ts: -------------------------------------------------------------------------------- 1 | import { isString } from './util'; 2 | 3 | export const checkStringOrigin = ( 4 | origin: string, 5 | option: string | string[] | RegExp, 6 | ): boolean => { 7 | if (isString(option) && option === '*') { 8 | return true; 9 | } else if ( 10 | isString(option) && 11 | option !== '*' && 12 | origin.match(option as string) 13 | ) { 14 | return true; 15 | } 16 | 17 | return false; 18 | }; 19 | -------------------------------------------------------------------------------- /packages/middleware-cors/src/configureResponse.ts: -------------------------------------------------------------------------------- 1 | import { HttpRequest, HttpResponse } from '@marblejs/http'; 2 | 3 | import { AccessControlHeader, applyHeaders, ConfiguredHeader } from './applyHeaders'; 4 | import { CORSOptions } from './middleware'; 5 | import { capitalize } from './util'; 6 | 7 | export function configureResponse( 8 | req: HttpRequest, 9 | res: HttpResponse, 10 | options: CORSOptions, 11 | ): void { 12 | const headers: ConfiguredHeader[] = []; 13 | const origin = req.headers.origin as string; 14 | 15 | headers.push({ key: AccessControlHeader.Origin, value: origin }); 16 | 17 | if (options.withCredentials) { 18 | headers.push({ key: AccessControlHeader.Credentials, value: 'true' }); 19 | } 20 | 21 | if ( 22 | Array.isArray(options.exposeHeaders) && 23 | options.exposeHeaders.length > 0 24 | ) { 25 | headers.push({ 26 | key: AccessControlHeader.ExposeHeaders, 27 | value: options.exposeHeaders.map(header => capitalize(header)).join(', '), 28 | }); 29 | } 30 | 31 | applyHeaders(headers, res); 32 | } 33 | -------------------------------------------------------------------------------- /packages/middleware-cors/src/index.ts: -------------------------------------------------------------------------------- 1 | export { cors$ } from './middleware'; 2 | -------------------------------------------------------------------------------- /packages/middleware-cors/src/middleware.ts: -------------------------------------------------------------------------------- 1 | import { of, EMPTY, defer } from 'rxjs'; 2 | import { mergeMap } from 'rxjs/operators'; 3 | import { isString } from '@marblejs/core/dist/+internal/utils'; 4 | import { endRequest } from '@marblejs/http/dist/response/http.responseHandler'; 5 | import { HttpMethod, HttpMiddlewareEffect, HttpRequest, HttpStatus } from '@marblejs/http'; 6 | import { pipe } from 'fp-ts/lib/function'; 7 | import { configurePreflightResponse } from './configurePreflightResponse'; 8 | import { configureResponse } from './configureResponse'; 9 | 10 | export interface CORSOptions { 11 | origin?: string | string[] | RegExp; 12 | methods?: HttpMethod[]; 13 | optionsSuccessStatus?: HttpStatus; 14 | allowHeaders?: string | string[]; 15 | exposeHeaders?: string[]; 16 | withCredentials?: boolean; 17 | maxAge?: number; 18 | } 19 | 20 | const DEFAULT_OPTIONS: CORSOptions = { 21 | origin: '*', 22 | methods: ['HEAD', 'GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], 23 | withCredentials: false, 24 | optionsSuccessStatus: HttpStatus.NO_CONTENT, 25 | }; 26 | 27 | const isCORSRequest = (req: HttpRequest): boolean => 28 | !isString(req.headers.origin); 29 | 30 | export const cors$ = (options: CORSOptions = {}): HttpMiddlewareEffect => req$ => { 31 | options = { ...DEFAULT_OPTIONS, ...options }; 32 | 33 | return req$.pipe( 34 | mergeMap(req => { 35 | if (isCORSRequest(req)) 36 | return of(req); 37 | 38 | if (req.method === 'OPTIONS') { 39 | configurePreflightResponse(req, req.response, options); 40 | return pipe( 41 | defer(endRequest(req.response)), 42 | mergeMap(() => EMPTY)); 43 | } 44 | 45 | configureResponse(req, req.response, options); 46 | return of(req); 47 | }), 48 | ); 49 | }; 50 | -------------------------------------------------------------------------------- /packages/middleware-cors/src/spec/applyHeaders.spec.ts: -------------------------------------------------------------------------------- 1 | import { createHttpResponse } from '@marblejs/http/dist/+internal/testing.util'; 2 | import { AccessControlHeader, applyHeaders, ConfiguredHeader } from '../applyHeaders'; 3 | 4 | describe('applyHeaders', () => { 5 | test('should handle many methods correctly', done => { 6 | const configured: ConfiguredHeader[] = [ 7 | { key: AccessControlHeader.Origin, value: '*' }, 8 | { key: AccessControlHeader.Methods, value: 'POST' }, 9 | ]; 10 | const res = createHttpResponse(); 11 | 12 | applyHeaders(configured, res); 13 | 14 | expect(res.setHeader).toBeCalledTimes(2); 15 | expect(res.setHeader).toBeCalledWith('Access-Control-Allow-Origin', '*'); 16 | expect(res.setHeader).toBeCalledWith( 17 | 'Access-Control-Allow-Methods', 18 | 'POST', 19 | ); 20 | done(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /packages/middleware-cors/src/spec/checkArrayOrigin.spec.ts: -------------------------------------------------------------------------------- 1 | import { checkArrayOrigin } from '../checkArrayOrigin'; 2 | 3 | describe('checkArrayOrigin', () => { 4 | test('check array option correctly', done => { 5 | const option1 = ['fake-origin']; 6 | const option2 = ['fake-origin-2']; 7 | 8 | expect(checkArrayOrigin('fake-origin', option2)).toBeFalsy(); 9 | expect(checkArrayOrigin('fake-origin', option1)).toBeTruthy(); 10 | done(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /packages/middleware-cors/src/spec/checkOrigin.spec.ts: -------------------------------------------------------------------------------- 1 | import { checkOrigin } from '../checkOrigin'; 2 | import { createMockRequest } from '../util'; 3 | 4 | describe('checkOrigin', () => { 5 | test('check wildcard option correctly', done => { 6 | const option = '*'; 7 | const req = createMockRequest('GET', { origin: 'fake-origin' }); 8 | 9 | expect(checkOrigin(req, option)).toBeTruthy(); 10 | done(); 11 | }); 12 | 13 | test('check string option correctly', done => { 14 | const option1 = 'fake-origin'; 15 | const option2 = 'fake-origin-2'; 16 | const req1 = createMockRequest('GET', { origin: 'fake-origin' }); 17 | const req2 = createMockRequest('GET', { origin: 'fake-origin' }); 18 | 19 | expect(checkOrigin(req1, option1)).toBeTruthy(); 20 | expect(checkOrigin(req2, option2)).toBeFalsy(); 21 | done(); 22 | }); 23 | 24 | test('check array option correctly', done => { 25 | const option1 = ['fake-origin-b', 'fake-origin-a']; 26 | const option2 = ['another-origin-a', 'another-origin-b']; 27 | const req1 = createMockRequest('GET', { origin: 'fake-origin-a' }); 28 | const req2 = createMockRequest('GET', { origin: 'fake-origin-c' }); 29 | 30 | expect(checkOrigin(req1, option1)).toBeTruthy(); 31 | expect(checkOrigin(req2, option2)).toBeFalsy(); 32 | done(); 33 | }); 34 | 35 | test('check regexp option correctly', done => { 36 | const option1 = /[a-z]/; 37 | const option2 = /[0-9]/; 38 | const req1 = createMockRequest('GET', { origin: 'fake-origin-a' }); 39 | const req2 = createMockRequest('GET', { origin: 'fake-origin-c' }); 40 | 41 | expect(checkOrigin(req1, option1)).toBeTruthy(); 42 | expect(checkOrigin(req2, option2)).toBeFalsy(); 43 | done(); 44 | }); 45 | }); 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /packages/middleware-cors/src/spec/checkRegexpOrigin.spec.ts: -------------------------------------------------------------------------------- 1 | import { checkRegexpOrigin } from '../checkRegexpOrigin'; 2 | 3 | describe('checkStringOrigin', () => { 4 | test('check regexp option correctly', done => { 5 | const option1 = /[a-z]/; 6 | const option2 = /[0-9]/; 7 | 8 | expect(checkRegexpOrigin('fake-origin', option1)).toBeTruthy(); 9 | expect(checkRegexpOrigin('fake-origin', option2)).toBeFalsy(); 10 | done(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /packages/middleware-cors/src/spec/checkStringOrigin.spec.ts: -------------------------------------------------------------------------------- 1 | import { checkStringOrigin } from '../checkStringOrigin'; 2 | 3 | describe('checkStringOrigin', () => { 4 | test('check string option correctly', done => { 5 | const option1 = 'fake-origin'; 6 | const option2 = 'fake-origin-2'; 7 | 8 | expect(checkStringOrigin('fake-origin', option1)).toBeTruthy(); 9 | expect(checkStringOrigin('fake-origin', option2)).toBeFalsy(); 10 | done(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /packages/middleware-cors/src/spec/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { cors$ } from '../index'; 2 | 3 | describe('@marblejs/middleware-cors public API', () => { 4 | it('should be defined', () => { 5 | expect(cors$).toBeDefined(); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/middleware-cors/src/spec/util.spec.ts: -------------------------------------------------------------------------------- 1 | import { capitalize, isString } from '../util'; 2 | 3 | describe('Utils', () => { 4 | describe('Capitalize', () => { 5 | test('should capitalize a header correctly', done => { 6 | const header = 'capitalize-any-header'; 7 | expect(capitalize(header)).toEqual('Capitalize-Any-Header'); 8 | done(); 9 | }); 10 | }); 11 | 12 | describe('isString', () => { 13 | test('should return true for a string', done => { 14 | const str = 'string'; 15 | expect(isString(str)).toBeTruthy(); 16 | done(); 17 | }); 18 | 19 | test('should return false for any other type', done => { 20 | const obj = {}; 21 | const num = 42; 22 | const arr = []; 23 | const n = null; 24 | expect(isString(obj)).toBeFalsy(); 25 | expect(isString(num)).toBeFalsy(); 26 | expect(isString(arr)).toBeFalsy(); 27 | expect(isString(n)).toBeFalsy(); 28 | done(); 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /packages/middleware-cors/src/util.ts: -------------------------------------------------------------------------------- 1 | import * as http from 'http'; 2 | import { createEffectContext, createContext, lookup } from '@marblejs/core'; 3 | import { HttpMethod } from '@marblejs/http'; 4 | import { createHttpRequest } from '@marblejs/http/dist/+internal/testing.util'; 5 | 6 | export const capitalize = (str: string): string => 7 | str 8 | .split('-') 9 | .map(part => part.charAt(0).toUpperCase() + part.slice(1)) 10 | .join('-'); 11 | 12 | export const isString = (str: any): boolean => 13 | typeof str === 'string' || str instanceof String; 14 | 15 | export const createMockRequest = ( 16 | method: HttpMethod = 'GET', 17 | headers: any = { origin: 'fake-origin' }, 18 | ) => createHttpRequest({ method, headers }); 19 | 20 | export const createMockEffectContext = () => { 21 | const context = createContext(); 22 | const client = http.createServer(); 23 | return createEffectContext({ ask: lookup(context), client }); 24 | }; 25 | -------------------------------------------------------------------------------- /packages/middleware-cors/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist/", 5 | "rootDir": "./src" 6 | }, 7 | "include": ["src/**/*.ts"], 8 | "references": [ 9 | { "path": "../core" }, 10 | { "path": "../http" } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /packages/middleware-io/README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Marble.js logo 4 | 5 |

6 | 7 | # @marblejs/middleware-io 8 | 9 | IO validation middleware for Marble.js for [Marble.js](https://github.com/marblejs/marble). 10 | 11 | ## Installation 12 | 13 | ``` 14 | $ npm i @marblejs/middleware-io 15 | ``` 16 | Requires `@marblejs/core` to be installed. 17 | 18 | ## Documentation 19 | 20 | For the latest updates, documentation, change log, and release information visit [docs.marblejs.com](https://docs.marblejs.com) and follow [@marble_js](https://twitter.com/marble_js) on Twitter. 21 | 22 | ## Usage 23 | 24 | ```typescript 25 | import { r } from '@marblejs/http'; 26 | import { requestValidator$, t } from '@marblejs/middleware-io'; 27 | 28 | const userSchema = t.type({ 29 | id: t.string, 30 | firstName: t.string, 31 | lastName: t.string, 32 | roles: t.array(t.union([ 33 | t.literal('ADMIN'), 34 | t.literal('GUEST'), 35 | ])), 36 | }); 37 | 38 | type User = t.TypeOf; 39 | 40 | const effect$ = r.pipe( 41 | r.matchPath('/'), 42 | r.matchType('POST'), 43 | r.useEffect(req$ => req$.pipe( 44 | requestValidator$({ body: userSchema }), 45 | // .. 46 | ))); 47 | ``` 48 | License: MIT 49 | -------------------------------------------------------------------------------- /packages/middleware-io/jest.config.js: -------------------------------------------------------------------------------- 1 | const config = require('../../jest.config'); 2 | module.exports = config; 3 | -------------------------------------------------------------------------------- /packages/middleware-io/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@marblejs/middleware-io", 3 | "version": "4.1.0", 4 | "description": "IO middleware for Marble.js", 5 | "main": "./dist/index.js", 6 | "types": "./dist/index.d.ts", 7 | "scripts": { 8 | "start": "yarn watch", 9 | "watch": "tsc -w", 10 | "build": "tsc", 11 | "clean": "rimraf dist", 12 | "test": "jest --config ./jest.config.js" 13 | }, 14 | "files": [ 15 | "dist/" 16 | ], 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/marblejs/marble.git" 20 | }, 21 | "engines": { 22 | "node": ">= 8.0.0", 23 | "yarn": ">= 1.7.0", 24 | "npm": ">= 5.0.0" 25 | }, 26 | "author": "marblejs", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/marblejs/marble/issues" 30 | }, 31 | "homepage": "https://github.com/marblejs/marble#readme", 32 | "peerDependencies": { 33 | "@marblejs/core": "^4.0.0", 34 | "@marblejs/http": "^4.0.0", 35 | "fp-ts": "^2.13.1", 36 | "rxjs": "^7.5.7" 37 | }, 38 | "dependencies": { 39 | "@types/json-schema": "^7.0.3", 40 | "io-ts": "^2.2.19" 41 | }, 42 | "devDependencies": { 43 | "@marblejs/core": "^4.1.0", 44 | "@marblejs/http": "^4.1.0" 45 | }, 46 | "publishConfig": { 47 | "access": "public" 48 | }, 49 | "gitHead": "208643c17d76a01d0a9cb304611d4474c4a3bff6" 50 | } 51 | -------------------------------------------------------------------------------- /packages/middleware-io/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as t from 'io-ts'; 2 | 3 | // package public API 4 | export { Schema, ValidatorOptions, validator$ } from './io.middleware'; 5 | export { requestValidator$ } from './io.request.middleware'; 6 | export { eventValidator$ } from './io.event.middleware'; 7 | export { defaultReporter } from './io.reporter'; 8 | export { ioTypeToJsonSchema, withJsonSchema } from './io.json-schema'; 9 | export { t }; 10 | -------------------------------------------------------------------------------- /packages/middleware-io/src/io.error.ts: -------------------------------------------------------------------------------- 1 | import { NamedError } from '@marblejs/core/dist/+internal/utils'; 2 | 3 | export enum ErrorType { 4 | IO_ERROR = 'IOError', 5 | } 6 | 7 | export class IOError extends NamedError { 8 | constructor( 9 | public readonly message: string, 10 | public readonly data: Record | Array, 11 | public readonly context?: string, 12 | ) { 13 | super(ErrorType.IO_ERROR, message); 14 | } 15 | } 16 | 17 | export const isIOError = (error: Error): error is IOError => 18 | error.name === ErrorType.IO_ERROR; 19 | -------------------------------------------------------------------------------- /packages/middleware-io/src/io.event.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Event, EventError, ValidatedEvent, isEventCodec } from '@marblejs/core'; 2 | import { Observable, of, throwError, isObservable } from 'rxjs'; 3 | import { pipe } from 'fp-ts/lib/function'; 4 | import { mergeMap, catchError, map } from 'rxjs/operators'; 5 | import { Schema, ValidatorOptions, validator$ } from './io.middleware'; 6 | import { IOError } from './io.error'; 7 | 8 | type ValidationResult = U['_A'] extends { type: string; payload: any } 9 | ? ValidatedEvent 10 | : ValidatedEvent 11 | 12 | export const eventValidator$ = (schema: U, options?: ValidatorOptions) => { 13 | const eventValidator$ = validator$(schema, options); 14 | 15 | const validateByEventSchema = (incomingEvent: Event) => 16 | pipe( 17 | of(incomingEvent), 18 | eventValidator$, 19 | map(decodedEvent => ({ ...incomingEvent, ...decodedEvent }) as ValidatedEvent), 20 | ); 21 | 22 | const validateByPayloadSchema = (incomingEvent: Event) => 23 | pipe( 24 | of(incomingEvent.payload), 25 | eventValidator$, 26 | map(payload => ({ ...incomingEvent, payload }) as ValidatedEvent), 27 | ); 28 | 29 | const validate = (event: Event) => 30 | pipe( 31 | isEventCodec(schema) 32 | ? validateByEventSchema(event) 33 | : validateByPayloadSchema(event), 34 | catchError((error: IOError) => throwError(() => 35 | new EventError(event, error.message, error.data), 36 | )), 37 | ) as Observable>; 38 | 39 | return (input: Observable | Event) => 40 | isObservable(input) 41 | ? input.pipe(mergeMap(validate)) 42 | : validate(input); 43 | }; 44 | -------------------------------------------------------------------------------- /packages/middleware-io/src/io.middleware.ts: -------------------------------------------------------------------------------- 1 | import * as t from 'io-ts'; 2 | import { Reporter } from 'io-ts/lib/Reporter'; 3 | import { identity } from 'fp-ts/lib/function'; 4 | import * as E from 'fp-ts/lib/Either'; 5 | import { Observable } from 'rxjs'; 6 | import { map} from 'rxjs/operators'; 7 | import { throwException } from '@marblejs/core/dist/+internal/utils'; 8 | import { defaultReporter } from './io.reporter'; 9 | import { IOError } from './io.error'; 10 | 11 | export type Schema = t.Any; 12 | 13 | export interface ValidatorOptions { 14 | reporter?: Reporter; 15 | context?: string; 16 | } 17 | 18 | const validateError = (reporter: Reporter = defaultReporter, context?: string) => (result: E.Either) => 19 | E.fold( 20 | () => throwException(new IOError('Validation error', reporter.report(result), context)), 21 | identity, 22 | )(result); 23 | 24 | export function validator$(schema: U, options?: ValidatorOptions): (i$: Observable) => Observable>; 25 | export function validator$(schema: undefined, options?: ValidatorOptions): (i$: Observable) => Observable; 26 | export function validator$(schema: U | undefined, options: ValidatorOptions = {}) { 27 | return (i$: Observable) => 28 | !schema ? i$ : i$.pipe( 29 | map(input => schema.decode(input)), 30 | map(validateError(options.reporter, options.context)), 31 | ); 32 | } 33 | 34 | -------------------------------------------------------------------------------- /packages/middleware-io/src/io.reporter.ts: -------------------------------------------------------------------------------- 1 | import * as t from 'io-ts'; 2 | import * as O from 'fp-ts/lib/Option'; 3 | import * as E from 'fp-ts/lib/Either'; 4 | import { pipe } from 'fp-ts/lib/function'; 5 | import { Reporter } from 'io-ts/lib/Reporter'; 6 | import { stringify, getLast } from '@marblejs/core/dist/+internal/utils'; 7 | 8 | export interface ReporterResult { 9 | path: string; 10 | expected: string; 11 | got: any; 12 | } 13 | 14 | const getPath = (context: t.Context) => 15 | context 16 | .map(c => c.key) 17 | .filter(Boolean) 18 | .join('.'); 19 | 20 | const getExpectedType = (context: t.ContextEntry[]) => pipe( 21 | getLast(context), 22 | O.map(c => c.type.name), 23 | O.getOrElse(() => 'any'), 24 | ); 25 | 26 | const getErrorMessage = (value: any, context: t.Context): ReporterResult => ({ 27 | path: getPath(context), 28 | expected: getExpectedType(context as t.ContextEntry[]), 29 | got: stringify(value), 30 | }); 31 | 32 | const failure = (errors: t.ValidationError[]): ReporterResult[] => 33 | errors.map(error => getErrorMessage(error.value, error.context)); 34 | 35 | const success = () => []; 36 | 37 | export const defaultReporter: Reporter = { 38 | report: E.fold(failure, success), 39 | }; 40 | -------------------------------------------------------------------------------- /packages/middleware-io/src/specs/index.spec.ts: -------------------------------------------------------------------------------- 1 | import * as API from '../index'; 2 | 3 | describe('@marblejs/middleware-io public API', () => { 4 | test('apis are defined', () => { 5 | expect(API.defaultReporter).toBeDefined(); 6 | expect(API.eventValidator$).toBeDefined(); 7 | expect(API.requestValidator$).toBeDefined(); 8 | expect(API.validator$).toBeDefined(); 9 | expect(API.t).toBeDefined(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /packages/middleware-io/src/specs/io.error.spec.ts: -------------------------------------------------------------------------------- 1 | import { IOError, isIOError } from '../io.error'; 2 | 3 | test('#isIOError checks if error is of type IOError', () => { 4 | const ioError = new IOError('test', {}); 5 | const otherError = new Error(); 6 | 7 | expect(isIOError(ioError)).toBe(true); 8 | expect(isIOError(otherError)).toBe(false); 9 | }); 10 | -------------------------------------------------------------------------------- /packages/middleware-io/test/io-http.integration.spec.ts: -------------------------------------------------------------------------------- 1 | import { createHttpTestBed, createTestBedSetup } from '@marblejs/testing'; 2 | import { pipe } from 'fp-ts/lib/function'; 3 | import { listener } from './io-http.integration'; 4 | 5 | const testBed = createHttpTestBed({ listener }); 6 | const useTestBedSetup = createTestBedSetup({ testBed }); 7 | 8 | describe('@marblejs/middleware-io - HTTP integration', () => { 9 | const testBedSetup = useTestBedSetup(); 10 | 11 | afterEach(async () => { 12 | await testBedSetup.cleanup(); 13 | }); 14 | 15 | test('POST / returns 200 with user object', async () => { 16 | const { request } = await testBedSetup.useTestBed(); 17 | const user = { id: 'id', name: 'name', age: 100 }; 18 | 19 | const response = await pipe( 20 | request('POST'), 21 | request.withPath('/'), 22 | request.withBody({ user }), 23 | request.send, 24 | ); 25 | 26 | expect(response.statusCode).toEqual(200); 27 | expect(response.body).toEqual(user); 28 | }); 29 | 30 | test('POST / returns 400 with validation error object', async () => { 31 | const { request } = await testBedSetup.useTestBed(); 32 | const user = { id: 'id', name: 'name', age: '100' }; 33 | 34 | const response = await pipe( 35 | request('POST'), 36 | request.withPath('/'), 37 | request.withBody({ user }), 38 | request.send, 39 | ); 40 | 41 | expect(response.statusCode).toEqual(400); 42 | expect(response.body).toEqual({ 43 | error: { 44 | status: 400, 45 | message: 'Validation error', 46 | context: 'body', 47 | data: [{ 48 | path: 'user.age', 49 | expected: 'number', 50 | got: '"100"', 51 | }] 52 | } 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /packages/middleware-io/test/io-http.integration.ts: -------------------------------------------------------------------------------- 1 | import { r, httpListener } from '@marblejs/http'; 2 | import { bodyParser$ } from '@marblejs/middleware-body'; 3 | import { map } from 'rxjs/operators'; 4 | import { requestValidator$, t } from '../src'; 5 | 6 | const user = t.type({ 7 | id: t.string, 8 | name: t.string, 9 | age: t.number, 10 | }); 11 | 12 | const validateRequest = requestValidator$({ 13 | body: t.type({ user }) 14 | }); 15 | 16 | const effect$ = r.pipe( 17 | r.matchPath('/'), 18 | r.matchType('POST'), 19 | r.useEffect(req$ => req$.pipe( 20 | validateRequest, 21 | map(req => ({ body: req.body.user })), 22 | ))); 23 | 24 | export const listener = httpListener({ 25 | middlewares: [bodyParser$()], 26 | effects: [effect$], 27 | }); 28 | -------------------------------------------------------------------------------- /packages/middleware-io/test/io-ws.integration.ts: -------------------------------------------------------------------------------- 1 | import { act, matchEvent } from '@marblejs/core'; 2 | import { webSocketListener, WsEffect } from '@marblejs/websockets'; 3 | import { eventValidator$, t } from '../src'; 4 | 5 | const user = t.type({ 6 | id: t.string, 7 | age: t.number, 8 | }); 9 | 10 | const postUser$: WsEffect = event$ => 11 | event$.pipe( 12 | matchEvent('POST_USER'), 13 | act(eventValidator$(user)), 14 | ); 15 | 16 | export const listener = webSocketListener({ 17 | effects: [postUser$], 18 | }); 19 | -------------------------------------------------------------------------------- /packages/middleware-io/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist/", 5 | "rootDir": "./src" 6 | }, 7 | "include": ["src/**/*.ts"], 8 | "references": [ 9 | { "path": "../core" } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /packages/middleware-logger/README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Marble.js logo 4 | 5 |

6 | 7 | # @marblejs/middleware-logger 8 | 9 | A logger middleware for [Marble.js](https://github.com/marblejs/marble). 10 | 11 | ## Installation 12 | 13 | ``` 14 | $ npm i @marblejs/middleware-logger 15 | ``` 16 | Requires `@marblejs/core` and `@marblejs/http` to be installed. 17 | 18 | ## Documentation 19 | 20 | For the latest updates, documentation, change log, and release information visit [docs.marblejs.com](https://docs.marblejs.com) and follow [@marble_js](https://twitter.com/marble_js) on Twitter. 21 | 22 | ## Usage 23 | 24 | ```typescript 25 | import { logger$ } from '@marblejs/middleware-logger'; 26 | 27 | const middlewares = [ 28 | logger$(), 29 | ... 30 | ]; 31 | 32 | const effects = [ 33 | ... 34 | ]; 35 | 36 | export const app = httpListener({ middlewares, effects }); 37 | ``` 38 | License: MIT 39 | -------------------------------------------------------------------------------- /packages/middleware-logger/jest.config.js: -------------------------------------------------------------------------------- 1 | const config = require('../../jest.config'); 2 | module.exports = config; 3 | -------------------------------------------------------------------------------- /packages/middleware-logger/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@marblejs/middleware-logger", 3 | "version": "4.1.0", 4 | "description": "Logger middleware for Marble.js", 5 | "main": "./dist/index.js", 6 | "types": "./dist/index.d.ts", 7 | "scripts": { 8 | "start": "yarn watch", 9 | "watch": "tsc -w", 10 | "build": "tsc", 11 | "clean": "rimraf dist", 12 | "test": "jest --config ./jest.config.js" 13 | }, 14 | "files": [ 15 | "dist/" 16 | ], 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/marblejs/marble.git" 20 | }, 21 | "engines": { 22 | "node": ">= 8.0.0", 23 | "yarn": ">= 1.7.0", 24 | "npm": ">= 5.0.0" 25 | }, 26 | "author": "marblejs", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/marblejs/marble/issues" 30 | }, 31 | "homepage": "https://github.com/marblejs/marble#readme", 32 | "peerDependencies": { 33 | "@marblejs/core": "^4.0.0", 34 | "@marblejs/http": "^4.0.0", 35 | "fp-ts": "^2.13.1", 36 | "rxjs": "^7.5.7" 37 | }, 38 | "dependencies": { 39 | "chalk": "^2.4.1" 40 | }, 41 | "devDependencies": { 42 | "@marblejs/core": "^4.1.0", 43 | "@marblejs/http": "^4.1.0", 44 | "@types/chalk": "^2.2.0" 45 | }, 46 | "publishConfig": { 47 | "access": "public" 48 | }, 49 | "gitHead": "208643c17d76a01d0a9cb304611d4474c4a3bff6" 50 | } 51 | -------------------------------------------------------------------------------- /packages/middleware-logger/src/index.ts: -------------------------------------------------------------------------------- 1 | export { logger$ } from './logger.middleware'; 2 | -------------------------------------------------------------------------------- /packages/middleware-logger/src/logger.factory.ts: -------------------------------------------------------------------------------- 1 | import { Timestamp } from 'rxjs'; 2 | import { HttpRequest } from '@marblejs/http'; 3 | import { factorizeTime } from './logger.util'; 4 | 5 | export const factorizeLog = (stamp: Timestamp) => (req: HttpRequest) => { 6 | const { method, url } = stamp.value; 7 | const statusCode = String(req.response.statusCode); 8 | const time = factorizeTime(stamp.timestamp); 9 | 10 | return `${method} ${url} ${statusCode} ${time}`; 11 | }; 12 | -------------------------------------------------------------------------------- /packages/middleware-logger/src/logger.handler.ts: -------------------------------------------------------------------------------- 1 | import { Logger, LoggerTag, LoggerLevel } from '@marblejs/core'; 2 | import { HttpRequest } from '@marblejs/http'; 3 | import { fromEvent, Timestamp, Observable } from 'rxjs'; 4 | import { take, filter, map, tap } from 'rxjs/operators'; 5 | import { factorizeLog } from './logger.factory'; 6 | import { LoggerOptions } from './logger.model'; 7 | import { isNotSilent, filterResponse } from './logger.util'; 8 | 9 | export const loggerHandler = (opts: LoggerOptions, logger: Logger) => (stamp: Timestamp): Observable => { 10 | const req = stamp.value; 11 | const res = req.response; 12 | 13 | return fromEvent(res, 'finish').pipe( 14 | take(1), 15 | map(() => req), 16 | filter(isNotSilent(opts)), 17 | filter(filterResponse(opts)), 18 | map(factorizeLog(stamp)), 19 | tap(message => { 20 | const level = res.statusCode >= 500 21 | ? LoggerLevel.ERROR 22 | : res.statusCode >= 400 23 | ? LoggerLevel.WARN 24 | : LoggerLevel.INFO; 25 | 26 | const log = logger({ tag: LoggerTag.HTTP, type: 'RequestLogger', message, level }); 27 | return log(); 28 | }), 29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /packages/middleware-logger/src/logger.middleware.ts: -------------------------------------------------------------------------------- 1 | import { useContext, LoggerToken } from '@marblejs/core'; 2 | import { HttpMiddlewareEffect } from '@marblejs/http'; 3 | import { timestamp, tap, map } from 'rxjs/operators'; 4 | import { LoggerOptions } from './logger.model'; 5 | import { loggerHandler } from './logger.handler'; 6 | 7 | export const logger$ = (opts: LoggerOptions = {}): HttpMiddlewareEffect => (req$, ctx) => { 8 | const logger = useContext(LoggerToken)(ctx.ask); 9 | 10 | return req$.pipe( 11 | timestamp(), 12 | tap(stamp => loggerHandler(opts, logger)(stamp).subscribe()), 13 | map(({ value: req }) => req), 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /packages/middleware-logger/src/logger.model.ts: -------------------------------------------------------------------------------- 1 | import { HttpRequest } from '@marblejs/http'; 2 | 3 | export type WritableLike = { 4 | write: (chunk: any) => void; 5 | } 6 | 7 | export interface LoggerOptions { 8 | silent?: boolean; 9 | filter?: (req: HttpRequest) => boolean; 10 | } 11 | 12 | export interface LogParams { 13 | method: string; 14 | url: string; 15 | statusCode: string; 16 | time: string; 17 | } 18 | -------------------------------------------------------------------------------- /packages/middleware-logger/src/logger.util.ts: -------------------------------------------------------------------------------- 1 | import * as O from 'fp-ts/lib/Option'; 2 | import { flow, pipe } from 'fp-ts/lib/function'; 3 | import { HttpRequest } from '@marblejs/http'; 4 | import { LoggerOptions } from './logger.model'; 5 | 6 | export const getDateFromTimestamp = (t: number) => new Date(t); 7 | 8 | export const isNotSilent = (opts: LoggerOptions) => (_: HttpRequest) => 9 | !opts.silent; 10 | 11 | export const filterResponse = (opts: LoggerOptions) => (req: HttpRequest) => pipe( 12 | O.fromNullable(opts.filter), 13 | O.map(filter => filter(req)), 14 | O.getOrElse(() => true), 15 | ); 16 | 17 | export const formatTime = (timeInMms: number) => 18 | timeInMms > 1000 19 | ? `+${timeInMms / 1000}s` 20 | : `+${timeInMms}ms`; 21 | 22 | export const getTimeDifferenceInMs = (startDate: Date): number => 23 | new Date().getTime() - startDate.getTime(); 24 | 25 | export const factorizeTime = flow( 26 | getDateFromTimestamp, 27 | getTimeDifferenceInMs, 28 | formatTime, 29 | ); 30 | -------------------------------------------------------------------------------- /packages/middleware-logger/src/spec/index.spec.ts: -------------------------------------------------------------------------------- 1 | import * as index from '../index'; 2 | 3 | test('index exposes public API', () => { 4 | expect(index.logger$).toBeDefined(); 5 | }); 6 | -------------------------------------------------------------------------------- /packages/middleware-logger/src/spec/logger.factory.spec.ts: -------------------------------------------------------------------------------- 1 | import { createHttpResponse, createHttpRequest } from '@marblejs/http/dist/+internal/testing.util'; 2 | import { factorizeLog } from '../logger.factory'; 3 | 4 | describe('Logger factory', () => { 5 | let loggerUtilModule; 6 | 7 | beforeEach(() => { 8 | jest.unmock('../logger.util.ts'); 9 | loggerUtilModule = require('../logger.util.ts'); 10 | }); 11 | 12 | test('#factorizeLog factorizes logger message', () => { 13 | // given 14 | const response = createHttpResponse({ statusCode: 200 }); 15 | const req = createHttpRequest({ method: 'GET', url: '/api/v1', response }); 16 | const stamp = { value: req, timestamp: 1539031930521 }; 17 | 18 | // when 19 | loggerUtilModule.factorizeTime = jest.fn(() => '+300ms'); 20 | const log = factorizeLog(stamp)(req); 21 | 22 | // then 23 | expect(log).toEqual('GET /api/v1 200 +300ms'); 24 | }); 25 | 26 | }); 27 | -------------------------------------------------------------------------------- /packages/middleware-logger/src/spec/logger.middleware.spec.ts: -------------------------------------------------------------------------------- 1 | import { firstValueFrom, of } from 'rxjs'; 2 | import { HttpServer } from '@marblejs/http'; 3 | import { createHttpRequest } from '@marblejs/http/dist/+internal/testing.util'; 4 | import { createEffectContext, lookup, register, LoggerToken, bindTo, createContext, Logger, EffectContext } from '@marblejs/core'; 5 | import { logger$ } from '../logger.middleware'; 6 | 7 | describe('logger$', () => { 8 | let logger: Logger; 9 | let ctx: EffectContext; 10 | 11 | beforeEach(() => { 12 | const client = jest.fn() as any as HttpServer; 13 | logger = jest.fn(() => jest.fn()); 14 | 15 | const boundLogger = bindTo(LoggerToken)(() => logger); 16 | const context = register(boundLogger)(createContext()); 17 | 18 | ctx = createEffectContext({ ask: lookup(context), client }); 19 | }); 20 | 21 | test('reacts to 200 status', async () => { 22 | // given 23 | const req = createHttpRequest({ url: '/', method: 'GET' }); 24 | const req$ = of(req); 25 | req.response.statusCode = 200; 26 | 27 | // when 28 | await firstValueFrom(logger$()(req$, ctx)); 29 | req.response.emit('finish'); 30 | 31 | // then 32 | expect(logger).toHaveBeenCalled(); 33 | }); 34 | 35 | test('reacts to 400 status', async () => { 36 | // given 37 | const req = createHttpRequest({ url: '/test', method: 'POST' }); 38 | const req$ = of(req); 39 | req.response.statusCode = 403; 40 | 41 | // when 42 | await firstValueFrom(logger$()(req$, ctx)); 43 | req.response.emit('finish'); 44 | 45 | // then 46 | expect(logger).toHaveBeenCalled(); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /packages/middleware-logger/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist/", 5 | "rootDir": "./src" 6 | }, 7 | "include": ["src/**/*.ts"], 8 | "references": [ 9 | { "path": "../core" } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /packages/middleware-multipart/jest.config.js: -------------------------------------------------------------------------------- 1 | const config = require('../../jest.config'); 2 | module.exports = config; 3 | -------------------------------------------------------------------------------- /packages/middleware-multipart/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@marblejs/middleware-multipart", 3 | "version": "4.1.0", 4 | "description": "A multipart/form-data middleware for Marble.js", 5 | "main": "dist/index.js", 6 | "types": "./dist/index.d.ts", 7 | "scripts": { 8 | "start": "yarn watch", 9 | "watch": "tsc -w", 10 | "build": "tsc", 11 | "test": "jest --config ./jest.config.js", 12 | "clean": "rimraf dist" 13 | }, 14 | "files": [ 15 | "dist/" 16 | ], 17 | "keywords": [ 18 | "marble.js", 19 | "multipart", 20 | "middleware", 21 | "http", 22 | "rxjs" 23 | ], 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/marblejs/marble.git" 27 | }, 28 | "engines": { 29 | "node": ">= 8.0.0", 30 | "yarn": ">= 1.7.0", 31 | "npm": ">= 5.0.0" 32 | }, 33 | "author": "marblejs", 34 | "license": "MIT", 35 | "bugs": { 36 | "url": "https://github.com/marblejs/marble/issues" 37 | }, 38 | "homepage": "https://github.com/marblejs/marble#readme", 39 | "dependencies": { 40 | "@fastify/busboy": "^1.0.0" 41 | }, 42 | "peerDependencies": { 43 | "@marblejs/core": "^4.0.0", 44 | "@marblejs/http": "^4.0.0", 45 | "fp-ts": "^2.13.1", 46 | "rxjs": "^7.5.7" 47 | }, 48 | "devDependencies": { 49 | "@marblejs/core": "^4.1.0", 50 | "@marblejs/http": "^4.1.0", 51 | "@marblejs/testing": "^4.1.0", 52 | "@types/form-data": "^2.5.0", 53 | "form-data": "^3.0.0" 54 | }, 55 | "publishConfig": { 56 | "access": "public" 57 | }, 58 | "gitHead": "208643c17d76a01d0a9cb304611d4474c4a3bff6" 59 | } 60 | -------------------------------------------------------------------------------- /packages/middleware-multipart/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './multipart.middleware'; 2 | export * from './multipart.interface'; 3 | -------------------------------------------------------------------------------- /packages/middleware-multipart/src/multipart.interface.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs'; 2 | 3 | export interface File { 4 | destination?: any; 5 | buffer?: Buffer; 6 | size?: number; 7 | encoding: string; 8 | mimetype: string; 9 | filename?: string; 10 | fieldname: string; 11 | } 12 | 13 | export interface FileIncomingData { 14 | file: NodeJS.ReadableStream; 15 | filename: string; 16 | fieldname: string; 17 | encoding: string; 18 | mimetype: string; 19 | } 20 | 21 | export interface WithFile { 22 | files: Record; 23 | } 24 | 25 | interface StreamHandlerOutput { 26 | destination: any; 27 | size?: number; 28 | } 29 | 30 | export interface StreamHandler { 31 | (opts: FileIncomingData): Promise | Observable; 32 | } 33 | 34 | export interface ParserOpts { 35 | files?: string[]; 36 | stream?: StreamHandler; 37 | maxFileSize?: number; 38 | maxFileCount?: number; 39 | maxFieldSize?: number; 40 | maxFieldCount?: number; 41 | } 42 | -------------------------------------------------------------------------------- /packages/middleware-multipart/src/multipart.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Observable, throwError } from 'rxjs'; 2 | import { mergeMap, map } from 'rxjs/operators'; 3 | import { HttpRequest, HttpError, HttpStatus } from '@marblejs/http'; 4 | import { ContentType } from '@marblejs/http/dist/+internal/contentType.util'; 5 | import { parseMultipart } from './multipart.parser'; 6 | import { shouldParseMultipart } from './multipart.util'; 7 | import { WithFile, ParserOpts } from './multipart.interface'; 8 | 9 | export const multipart$ = (opts: ParserOpts = {}) => (req$: Observable) => 10 | req$.pipe( 11 | mergeMap(req => shouldParseMultipart(req) 12 | ? parseMultipart(opts)(req) 13 | : throwError(() => 14 | new HttpError(`Content-Type must be of type ${ContentType.MULTIPART_FORM_DATA}`, HttpStatus.PRECONDITION_FAILED, undefined, req) 15 | )), 16 | map(req => req as T & WithFile), 17 | ); 18 | -------------------------------------------------------------------------------- /packages/middleware-multipart/src/multipart.parser.field.ts: -------------------------------------------------------------------------------- 1 | import { HttpRequest } from '@marblejs/http'; 2 | import { Observable } from 'rxjs'; 3 | import { takeUntil, tap, defaultIfEmpty, map } from 'rxjs/operators'; 4 | 5 | export type FieldEvent = [string, any, boolean, boolean, string, string]; 6 | 7 | export const parseField = (req: HttpRequest) => (event$: Observable, finish$: Observable) => 8 | event$.pipe( 9 | takeUntil(finish$), 10 | tap(([ fieldname, value ]) => { 11 | req.body = { 12 | ...req.body ?? {}, 13 | [fieldname]: value, 14 | }; 15 | }), 16 | map(() => req), 17 | defaultIfEmpty(req), 18 | ); 19 | -------------------------------------------------------------------------------- /packages/middleware-multipart/src/specs/index.spec.ts: -------------------------------------------------------------------------------- 1 | import * as API from '../index'; 2 | 3 | describe('@marblejs/middleware-multipart public API', () => { 4 | test('apis are defined', () => { 5 | expect(API.multipart$).toBeDefined(); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/middleware-multipart/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist/", 5 | "rootDir": "./src" 6 | }, 7 | "include": ["src/**/*.ts"], 8 | "references": [ 9 | { "path": "../core" } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /packages/testing/README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Marble.js logo 4 | 5 |

6 | 7 | # @marblejs/testing 8 | 9 | A testing module for [Marble.js](https://github.com/marblejs/marble). 10 | 11 | ## Installation 12 | 13 | ``` 14 | $ npm i @marblejs/testing 15 | ``` 16 | Requires `@marblejs/core`, `@marblejs/http`, `@marblejs/messaging`, `@marblejs/websockets`, `fp-ts` and `rxjs` to be installed. 17 | 18 | ## Documentation 19 | 20 | For the latest updates, documentation, change log, and release information visit [docs.marblejs.com](https://docs.marblejs.com) and follow [@marble_js](https://twitter.com/marble_js) on Twitter. 21 | -------------------------------------------------------------------------------- /packages/testing/jest.config.js: -------------------------------------------------------------------------------- 1 | const config = require('../../jest.config'); 2 | module.exports = config; 3 | -------------------------------------------------------------------------------- /packages/testing/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@marblejs/testing", 3 | "version": "4.1.0", 4 | "description": "Testing module for Marble.js", 5 | "main": "./dist/index.js", 6 | "types": "./dist/index.d.ts", 7 | "scripts": { 8 | "start": "yarn watch", 9 | "watch": "tsc -w", 10 | "build": "tsc", 11 | "clean": "rimraf dist", 12 | "test": "jest --config ./jest.config.js" 13 | }, 14 | "files": [ 15 | "dist/" 16 | ], 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/marblejs/marble.git" 20 | }, 21 | "engines": { 22 | "node": ">= 8.0.0", 23 | "yarn": ">= 1.7.0", 24 | "npm": ">= 5.0.0" 25 | }, 26 | "author": "Józef Flakus ", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/marblejs/marble/issues" 30 | }, 31 | "homepage": "https://github.com/marblejs/marble#readme", 32 | "peerDependencies": { 33 | "@marblejs/core": "^4.0.0", 34 | "@marblejs/http": "^4.0.0", 35 | "@marblejs/messaging": "^4.0.0", 36 | "@marblejs/websockets": "^4.0.0", 37 | "fp-ts": "^2.13.1", 38 | "rxjs": "^7.5.7" 39 | }, 40 | "devDependencies": { 41 | "@marblejs/core": "^4.1.0", 42 | "@marblejs/http": "^4.1.0", 43 | "@marblejs/messaging": "^4.1.0", 44 | "@marblejs/websockets": "^4.1.0" 45 | }, 46 | "publishConfig": { 47 | "access": "public" 48 | }, 49 | "gitHead": "208643c17d76a01d0a9cb304611d4474c4a3bff6" 50 | } 51 | -------------------------------------------------------------------------------- /packages/testing/src/index.spec.ts: -------------------------------------------------------------------------------- 1 | import * as API from './index'; 2 | 3 | describe('@marblejs/testing', () => { 4 | test('public APIs are defined', () => { 5 | expect(API.TestBedType).toBeDefined(); 6 | expect(API.createTestBedSetup).toBeDefined(); 7 | expect(API.createHttpTestBed).toBeDefined(); 8 | expect(API.createTestBedContainer).toBeDefined(); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /packages/testing/src/index.ts: -------------------------------------------------------------------------------- 1 | // TestBed setup 2 | export * from './testBed/testBedSetup'; 3 | export * from './testBed/testBedSetup.interface'; 4 | 5 | // TestBed container 6 | export * from './testBed/testBedContainer'; 7 | export * from './testBed/testBedContainer.interface'; 8 | 9 | // TestBed 10 | export * from './testBed/testBed.interface'; 11 | 12 | // TestBed HTTP 13 | export * from './testBed/http/http.testBed'; 14 | export * from './testBed/http/http.testBed.interface'; 15 | export * from './testBed/http/http.testBed.request.interface'; 16 | export * from './testBed/http/http.testBed.response.interface'; 17 | -------------------------------------------------------------------------------- /packages/testing/src/testBed/http/http.testBed.interface.ts: -------------------------------------------------------------------------------- 1 | import { Reader } from 'fp-ts/lib/Reader'; 2 | import { Context } from '@marblejs/core'; 3 | import { HttpHeaders, HttpListener, HttpMethod } from '@marblejs/http'; 4 | import { TestBed, TestBedType } from '../testBed.interface'; 5 | import { HttpTestBedRequest, HttpTestBedRequestBuilder } from './http.testBed.request.interface'; 6 | import { HttpTestBedResponse } from './http.testBed.response.interface'; 7 | 8 | export interface HttpTestBedConfig { 9 | listener: Reader; 10 | defaultHeaders?: HttpHeaders; 11 | } 12 | 13 | export interface HttpTestBed extends TestBed { 14 | type: TestBedType.HTTP; 15 | send: (req: HttpTestBedRequest) => Promise; 16 | request: HttpTestBedRequestBuilder; 17 | } 18 | -------------------------------------------------------------------------------- /packages/testing/src/testBed/http/http.testBed.request.interface.ts: -------------------------------------------------------------------------------- 1 | import { HttpMethod, HttpHeaders } from '@marblejs/http'; 2 | import { withHeaders, withBody, withPath } from './http.testBed.request'; 3 | import { HttpTestBedResponse } from './http.testBed.response.interface'; 4 | 5 | export interface HttpTestBedRequest extends Readonly<{ 6 | host: string; 7 | port: number; 8 | protocol: string; 9 | headers: HttpHeaders; 10 | method: T; 11 | path: string; 12 | body?: any; 13 | }> {} 14 | 15 | export interface WithBodyApplied { 16 | readonly body: T; 17 | } 18 | 19 | export interface HttpTestBedRequestBuilder { 20 | (method: T): HttpTestBedRequest; 21 | withHeaders: typeof withHeaders; 22 | withBody: typeof withBody; 23 | withPath: typeof withPath; 24 | send: (req: HttpTestBedRequest) => Promise; 25 | } 26 | -------------------------------------------------------------------------------- /packages/testing/src/testBed/http/http.testBed.request.ts: -------------------------------------------------------------------------------- 1 | import * as O from 'fp-ts/lib/Option'; 2 | import { pipe } from 'fp-ts/lib/function'; 3 | import { HttpMethod, HttpHeaders } from '@marblejs/http'; 4 | import { createRequestMetadataHeader } from '@marblejs/http/dist/+internal/metadata.util'; 5 | import { ContentType, getMimeType, getContentType } from '@marblejs/http/dist/+internal/contentType.util'; 6 | import { HttpTestBedRequest, WithBodyApplied } from './http.testBed.request.interface'; 7 | 8 | export const createRequest = (port: number, host: string, headers?: HttpHeaders) => 9 | (method: T): HttpTestBedRequest => ({ 10 | protocol: 'http:', 11 | path: '/', 12 | headers: { 13 | ...createRequestMetadataHeader(), 14 | ...headers, 15 | }, 16 | method, 17 | host, 18 | port, 19 | }); 20 | 21 | export const withPath = (path: string) => (req: HttpTestBedRequest): HttpTestBedRequest => ({ 22 | ...req, 23 | path, 24 | }); 25 | 26 | export const withHeaders = (headers: HttpHeaders) => (req: HttpTestBedRequest): HttpTestBedRequest => ({ 27 | ...req, 28 | headers: { ...req.headers, ...headers }, 29 | }); 30 | 31 | export const withBody = (body: T) => (req: HttpTestBedRequest): HttpTestBedRequest & WithBodyApplied => 32 | pipe( 33 | getContentType(req.headers), 34 | O.map(() => ({ ...req, body })), 35 | O.getOrElse(() => pipe( 36 | getMimeType(body, req.path), 37 | type => withHeaders({ 'Content-Type': type ?? ContentType.APPLICATION_JSON })(req), 38 | req => ({ ...req, body }), 39 | )) 40 | ); 41 | -------------------------------------------------------------------------------- /packages/testing/src/testBed/http/http.testBed.response.interface.ts: -------------------------------------------------------------------------------- 1 | import { HttpHeaders, HttpRequestMetadata, HttpStatus } from '@marblejs/http'; 2 | import { HttpTestBedRequest } from './http.testBed.request.interface'; 3 | 4 | export interface HttpTestBedResponseProps { 5 | statusCode?: number; 6 | statusMessage?: string; 7 | headers: HttpHeaders; 8 | body?: Buffer; 9 | } 10 | 11 | export interface HttpTestBedResponse extends HttpTestBedResponseProps { 12 | req: HttpTestBedRequest; 13 | metadata: HttpRequestMetadata; 14 | statusCode: HttpStatus; 15 | body?: any; 16 | } 17 | -------------------------------------------------------------------------------- /packages/testing/src/testBed/http/http.testBed.response.ts: -------------------------------------------------------------------------------- 1 | import * as O from 'fp-ts/lib/Option'; 2 | import { pipe } from 'fp-ts/lib/function'; 3 | import { HttpStatus } from '@marblejs/http'; 4 | import { parseJson } from '@marblejs/core/dist/+internal/utils'; 5 | import { getContentTypeUnsafe, ContentType } from '@marblejs/http/dist/+internal/contentType.util'; 6 | import { HttpTestBedResponseProps, HttpTestBedResponse } from './http.testBed.response.interface'; 7 | import { HttpTestBedRequest } from './http.testBed.request.interface'; 8 | 9 | const parseResponseBody = (props: HttpTestBedResponseProps): string | Array | Record | undefined => 10 | pipe( 11 | O.fromNullable(props.body), 12 | O.map(body => { 13 | switch (getContentTypeUnsafe(props.headers)) { 14 | case ContentType.APPLICATION_JSON: 15 | return pipe( 16 | body.toString(), 17 | O.fromPredicate(Boolean), 18 | O.map(parseJson), 19 | O.toUndefined); 20 | case ContentType.TEXT_PLAIN: 21 | case ContentType.TEXT_HTML: 22 | return body.toString(); 23 | default: 24 | return body; 25 | } 26 | }), 27 | O.toUndefined); 28 | 29 | export const createResponse = (req: HttpTestBedRequest) => (props: HttpTestBedResponseProps): HttpTestBedResponse => 30 | pipe( 31 | parseResponseBody(props), 32 | body => ({ 33 | statusCode: props.statusCode ?? HttpStatus.OK, 34 | statusMessage: props.statusMessage, 35 | headers: props.headers, 36 | metadata: {}, 37 | req, 38 | body, 39 | }), 40 | ); 41 | -------------------------------------------------------------------------------- /packages/testing/src/testBed/testBed.interface.ts: -------------------------------------------------------------------------------- 1 | import { Task } from 'fp-ts/lib/Task'; 2 | import { ContextProvider, BoundDependency } from '@marblejs/core'; 3 | 4 | export enum TestBedType { 5 | HTTP, 6 | MESSAGING, 7 | WEBSOCKETS, 8 | } 9 | 10 | export interface TestBed { 11 | type: TestBedType; 12 | ask: ContextProvider; 13 | finish: Task; 14 | } 15 | 16 | export type TestBedFactory = (dependencies?: BoundDependency[]) => Promise 17 | -------------------------------------------------------------------------------- /packages/testing/src/testBed/testBedContainer.interface.ts: -------------------------------------------------------------------------------- 1 | import { Task } from 'fp-ts/lib/Task'; 2 | import { ContextToken } from '@marblejs/core'; 3 | import { TestBed } from './testBed.interface'; 4 | 5 | export type DependencyCleanup = { 6 | token: ContextToken; 7 | cleanup: (dependency: T) => Promise; 8 | } 9 | 10 | export interface TestBedContainerConfig { 11 | cleanups?: readonly DependencyCleanup[]; 12 | } 13 | 14 | export interface TestBedContainer { 15 | cleanup: Task; 16 | register: (instance: T) => Task; 17 | } 18 | -------------------------------------------------------------------------------- /packages/testing/src/testBed/testBedSetup.interface.ts: -------------------------------------------------------------------------------- 1 | import { Task } from 'fp-ts/lib/Task'; 2 | import { BoundDependency } from '@marblejs/core'; 3 | import { DependencyCleanup } from './testBedContainer.interface'; 4 | import { TestBed, TestBedFactory } from './testBed.interface'; 5 | 6 | export interface TestBedSetupConfig { 7 | testBed: TestBedFactory; 8 | dependencies?: readonly BoundDependency[]; 9 | cleanups?: readonly DependencyCleanup[]; 10 | } 11 | 12 | export interface TestBedSetup { 13 | useTestBed: TestBedFactory; 14 | cleanup: Task; 15 | } 16 | -------------------------------------------------------------------------------- /packages/testing/src/testBed/testBedSetup.ts: -------------------------------------------------------------------------------- 1 | import { BoundDependency } from '@marblejs/core'; 2 | import * as T from 'fp-ts/lib/Task'; 3 | import { pipe } from 'fp-ts/lib/function'; 4 | import { TestBedSetupConfig, TestBedSetup } from './testBedSetup.interface'; 5 | import { createTestBedContainer } from './testBedContainer'; 6 | import { TestBed } from './testBed.interface'; 7 | 8 | type CreateTestBedSetup = 9 | (config: TestBedSetupConfig) => 10 | (prependDependencies?: readonly BoundDependency[]) => 11 | TestBedSetup 12 | 13 | export const createTestBedSetup: CreateTestBedSetup = config => prependDependencies => { 14 | const { dependencies: defaultDependencies, cleanups = [] } = config; 15 | 16 | const { cleanup, register } = createTestBedContainer({ cleanups }); 17 | 18 | const useTestBed = async (dependencies: BoundDependency[] = []) => pipe( 19 | () => config.testBed([ 20 | ...defaultDependencies ?? [], 21 | ...prependDependencies ?? [], 22 | ...dependencies 23 | ]), 24 | T.chain(register), 25 | )(); 26 | 27 | return { 28 | useTestBed, 29 | cleanup, 30 | }; 31 | }; 32 | -------------------------------------------------------------------------------- /packages/testing/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist/", 5 | "rootDir": "./src" 6 | }, 7 | "include": ["src/**/*.ts"], 8 | "references": [ 9 | { "path": "../core" }, 10 | { "path": "../http" }, 11 | { "path": "../messaging" }, 12 | { "path": "../websockets" } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /packages/websockets/README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Marble.js logo 4 | 5 |

6 | 7 | # @marblejs/websockets 8 | 9 | A WebSockets module for [Marble.js](https://github.com/marblejs/marble). 10 | 11 | ## Installation 12 | 13 | ``` 14 | $ npm i @marblejs/websockets 15 | ``` 16 | Requires `@marblejs/core`, `rxjs` and `fp-ts` to be installed. 17 | 18 | ## Documentation 19 | 20 | For the latest updates, documentation, change log, and release information visit [docs.marblejs.com](https://docs.marblejs.com) and follow [@marble_js](https://twitter.com/marble_js) on Twitter. 21 | 22 | License: MIT 23 | -------------------------------------------------------------------------------- /packages/websockets/jest.config.js: -------------------------------------------------------------------------------- 1 | const config = require('../../jest.config'); 2 | module.exports = config; 3 | -------------------------------------------------------------------------------- /packages/websockets/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@marblejs/websockets", 3 | "version": "4.1.0", 4 | "description": "Websockets module for Marble.js", 5 | "main": "./dist/index.js", 6 | "types": "./dist/index.d.ts", 7 | "scripts": { 8 | "start": "yarn watch", 9 | "watch": "tsc -w", 10 | "build": "tsc", 11 | "clean": "rimraf dist", 12 | "test": "jest --config ./jest.config.js" 13 | }, 14 | "files": [ 15 | "dist/" 16 | ], 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/marblejs/marble.git" 20 | }, 21 | "engines": { 22 | "node": ">= 8.0.0", 23 | "yarn": ">= 1.7.0", 24 | "npm": ">= 5.0.0" 25 | }, 26 | "author": "Józef Flakus ", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/marblejs/marble/issues" 30 | }, 31 | "homepage": "https://github.com/marblejs/marble#readme", 32 | "peerDependencies": { 33 | "@marblejs/core": "^4.0.0", 34 | "fp-ts": "^2.13.1", 35 | "rxjs": "^7.5.7" 36 | }, 37 | "dependencies": { 38 | "@types/ws": "~6.0.1", 39 | "path-to-regexp": "^6.1.0", 40 | "ws": "~7.4.6" 41 | }, 42 | "devDependencies": { 43 | "@marblejs/core": "^4.1.0", 44 | "@marblejs/http": "^4.1.0" 45 | }, 46 | "publishConfig": { 47 | "access": "public" 48 | }, 49 | "gitHead": "208643c17d76a01d0a9cb304611d4474c4a3bff6" 50 | } 51 | -------------------------------------------------------------------------------- /packages/websockets/src/+internal/index.ts: -------------------------------------------------------------------------------- 1 | export * from './websocket.test.util'; 2 | -------------------------------------------------------------------------------- /packages/websockets/src/effects/websocket.effects.interface.ts: -------------------------------------------------------------------------------- 1 | import * as http from 'http'; 2 | import { Event, Effect } from '@marblejs/core'; 3 | import { WebSocketClientConnection } from '../server/websocket.server.interface'; 4 | 5 | export interface WsMiddlewareEffect< 6 | I = Event, 7 | O = Event, 8 | > extends WsEffect {} 9 | 10 | export interface WsErrorEffect< 11 | Err extends Error = Error, 12 | > extends WsEffect {} 13 | 14 | export interface WsConnectionEffect< 15 | T extends http.IncomingMessage = http.IncomingMessage 16 | > extends WsEffect {} 17 | 18 | export interface WsServerEffect 19 | extends WsEffect {} 20 | 21 | export interface WsOutputEffect< 22 | T extends Event = Event 23 | > extends WsEffect {} 24 | 25 | export interface WsEffect< 26 | T = Event, 27 | U = Event, 28 | V = WebSocketClientConnection, 29 | > extends Effect {} 30 | -------------------------------------------------------------------------------- /packages/websockets/src/error/specs/websocket.error.effect.spec.ts: -------------------------------------------------------------------------------- 1 | import { Marbles } from '@marblejs/core/dist/+internal/testing'; 2 | import { defaultError$ } from '../websocket.error.effect'; 3 | 4 | describe('defaultError$', () => { 5 | test('returns stream of error events for defined error object', () => { 6 | // given 7 | const error = new Error('Test error message'); 8 | const outgoingEvent = { 9 | type: 'UNHANDLED_ERROR', 10 | error: { 11 | name: error.name, 12 | message: error.message, 13 | }, 14 | }; 15 | 16 | // then 17 | Marbles.assertEffect(defaultError$, [ 18 | ['--a--', { a: error }], 19 | ['--b--', { b: outgoingEvent }], 20 | ]); 21 | }); 22 | 23 | test('returns stream of error events for undefined error object', () => { 24 | // given 25 | const error = undefined; 26 | const outgoingEvent = { 27 | type: 'UNHANDLED_ERROR', 28 | error: {}, 29 | }; 30 | 31 | // then 32 | Marbles.assertEffect(defaultError$, [ 33 | ['--a--', { a: error }], 34 | ['--b--', { b: outgoingEvent }], 35 | ]); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /packages/websockets/src/error/specs/websocket.error.model.spec.ts: -------------------------------------------------------------------------------- 1 | import { WebSocketConnectionError } from '../websocket.error.model'; 2 | import { WebSocketStatus } from '../../websocket.interface'; 3 | 4 | test('WebSocketConnectionError creates error object', () => { 5 | // given 6 | const errorMessage = 'test_message'; 7 | const errorStatus = WebSocketStatus.BAD_GATEWAY; 8 | 9 | // when 10 | const error = new WebSocketConnectionError(errorMessage, errorStatus); 11 | 12 | // then 13 | expect(error.name).toEqual('WebSocketConnectionError'); 14 | expect(error.message).toEqual(errorMessage); 15 | expect(error.status).toEqual(errorStatus); 16 | }); 17 | -------------------------------------------------------------------------------- /packages/websockets/src/error/websocket.error.effect.ts: -------------------------------------------------------------------------------- 1 | import { map } from 'rxjs/operators'; 2 | import { Event, isEventError, EventError } from '@marblejs/core'; 3 | import { WsErrorEffect } from '../effects/websocket.effects.interface'; 4 | 5 | const mapError = (error: Error | undefined): Event => ({ 6 | type: 'UNHANDLED_ERROR', 7 | error: { 8 | name: error?.name, 9 | message: error?.message, 10 | }, 11 | }); 12 | 13 | const mapEventError = (error: EventError): Event => ({ 14 | type: error.event.type, 15 | error: { 16 | name: error.name, 17 | message: error.message, 18 | data: error.data, 19 | }, 20 | }); 21 | 22 | export const defaultError$: WsErrorEffect = event$ => 23 | event$.pipe( 24 | map(error => error && isEventError(error) 25 | ? mapEventError(error) 26 | : mapError(error)), 27 | ); 28 | -------------------------------------------------------------------------------- /packages/websockets/src/error/websocket.error.model.ts: -------------------------------------------------------------------------------- 1 | import { NamedError } from '@marblejs/core/dist/+internal/utils'; 2 | import { WebSocketStatus } from '../websocket.interface'; 3 | 4 | export class WebSocketConnectionError extends NamedError { 5 | constructor( 6 | public readonly message: string, 7 | public readonly status: WebSocketStatus | number, 8 | ) { 9 | super('WebSocketConnectionError', message); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/websockets/src/index.spec.ts: -------------------------------------------------------------------------------- 1 | import * as API from './index'; 2 | 3 | describe('@marblejs/websockets', () => { 4 | test('public APIs are defined', () => { 5 | expect(API.broadcast).toBeDefined(); 6 | expect(API.mapToServer).toBeDefined(); 7 | expect(API.defaultError$).toBeDefined(); 8 | expect(API.jsonTransformer).toBeDefined(); 9 | expect(API.webSocketListener).toBeDefined(); 10 | expect(API.createWebSocketServer).toBeDefined(); 11 | expect(API.WebSocketStatus).toBeDefined(); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /packages/websockets/src/index.ts: -------------------------------------------------------------------------------- 1 | // server 2 | export { webSocketListener, WebSocketListenerConfig } from './server/websocket.server.listener'; 3 | export { createWebSocketServer } from './server/websocket.server'; 4 | export * from './server/websocket.server.event'; 5 | export * from './server/websocket.server.interface'; 6 | 7 | // common 8 | export { WebSocketStatus } from './websocket.interface'; 9 | 10 | // transformer 11 | export { jsonTransformer } from './transformer/websocket.json.transformer'; 12 | export { EventTransformer } from './transformer/websocket.transformer.interface'; 13 | 14 | // error 15 | export { defaultError$ } from './error/websocket.error.effect'; 16 | export { WebSocketConnectionError } from './error/websocket.error.model'; 17 | 18 | // effects 19 | export * from './effects/websocket.effects.interface'; 20 | 21 | // operators 22 | export * from './operators'; 23 | -------------------------------------------------------------------------------- /packages/websockets/src/operators/broadcast/websocket.broadcast.operator.spec.ts: -------------------------------------------------------------------------------- 1 | import { Marbles } from '@marblejs/core/dist/+internal/testing'; 2 | import { of } from 'rxjs'; 3 | import { WsEffect } from '../../effects/websocket.effects.interface'; 4 | import { broadcast } from './websocket.broadcast.operator'; 5 | 6 | describe('#broadcast operator', () => { 7 | const client = { sendBroadcastResponse: jest.fn(() => of(true)) }; 8 | const ctx = { client }; 9 | 10 | test('sends broadcast response', () => { 11 | // given 12 | const incomingEvent = { type: 'TEST_EVENT' }; 13 | const outgoingEvent = { type: 'TEST_EVENT_RESPONSE', payload: 'test_payload' }; 14 | 15 | // when 16 | const effect$: WsEffect = (event$, { client }) => 17 | event$.pipe( 18 | broadcast(client, () => outgoingEvent), 19 | ); 20 | 21 | // then 22 | Marbles.assertEffect(effect$, [ 23 | ['-a--', { a: incomingEvent }], 24 | ['-b--', { b: outgoingEvent }], 25 | ], { ctx }); 26 | 27 | expect(client.sendBroadcastResponse).toHaveBeenCalledWith(outgoingEvent); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /packages/websockets/src/operators/broadcast/websocket.broadcast.operator.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs'; 2 | import { mergeMap, map } from 'rxjs/operators'; 3 | import { Event } from '@marblejs/core'; 4 | import { WebSocketClientConnection } from '../../server/websocket.server.interface'; 5 | 6 | export const broadcast = 7 | (client: WebSocketClientConnection, fn: (input: Input) => T) => 8 | (input$: Observable): Observable => 9 | input$.pipe( 10 | map(fn), 11 | mergeMap(data => client.sendBroadcastResponse(data).pipe( 12 | map(() => data), 13 | )), 14 | ); 15 | -------------------------------------------------------------------------------- /packages/websockets/src/operators/index.ts: -------------------------------------------------------------------------------- 1 | export { broadcast } from './broadcast/websocket.broadcast.operator'; 2 | export { mapToServer } from './mapToServer/websocket.mapToServer.operator'; 3 | -------------------------------------------------------------------------------- /packages/websockets/src/server/websocket.server.event.subscriber.ts: -------------------------------------------------------------------------------- 1 | import * as http from 'http'; 2 | import * as WebSocket from 'ws'; 3 | import { Subject, Observable } from 'rxjs'; 4 | import { ServerEventType, ServerEvent, AllServerEvents } from './websocket.server.event'; 5 | import { DEFAULT_HOSTNAME, WebSocketServerConnection } from './websocket.server.interface'; 6 | 7 | export const subscribeWebSocketEvents = (server: WebSocketServerConnection): { 8 | serverEvent$: Observable; 9 | serverEventSubject: Subject; 10 | } => { 11 | const serverEventSubject = new Subject(); 12 | 13 | server.on(ServerEventType.HEADERS, (headers: string[], req: http.IncomingMessage) => 14 | serverEventSubject.next(ServerEvent.headers(headers, req)), 15 | ); 16 | 17 | server.on(ServerEventType.CLOSE, () => 18 | serverEventSubject.next(ServerEvent.close()), 19 | ); 20 | 21 | server.on(ServerEventType.ERROR, (error: Error) => 22 | serverEventSubject.next(ServerEvent.error(error)), 23 | ); 24 | 25 | server.on(ServerEventType.LISTENING, () => { 26 | const hostname = server.options.host ?? DEFAULT_HOSTNAME; 27 | const port = (server.address() as WebSocket.AddressInfo).port; 28 | serverEventSubject.next(ServerEvent.listening(port, hostname)); 29 | }); 30 | 31 | return { 32 | serverEvent$: serverEventSubject.asObservable(), 33 | serverEventSubject, 34 | }; 35 | }; 36 | -------------------------------------------------------------------------------- /packages/websockets/src/server/websocket.server.interface.ts: -------------------------------------------------------------------------------- 1 | import * as WebSocket from 'ws'; 2 | import { Observable } from 'rxjs'; 3 | import { Event, ServerConfig } from '@marblejs/core'; 4 | import { WsServerEffect } from '../effects/websocket.effects.interface'; 5 | import { webSocketListener } from './websocket.server.listener'; 6 | 7 | export const DEFAULT_HOSTNAME = '127.0.0.1'; 8 | 9 | type WebSocketListenerFn = ReturnType; 10 | 11 | export interface WebSocketServerConfig extends ServerConfig { 12 | options?: WebSocket.ServerOptions; 13 | } 14 | 15 | export interface WebSocketServerConnection extends WebSocket.Server { 16 | sendBroadcastResponse: (response: T) => Observable; 17 | } 18 | 19 | export interface WebSocketClientConnection extends WebSocket { 20 | id: string; 21 | address: string; 22 | isAlive: boolean; 23 | sendResponse: (response: T) => Observable; 24 | sendBroadcastResponse: (response: T) => Observable; 25 | } 26 | -------------------------------------------------------------------------------- /packages/websockets/src/transformer/websocket.json.transformer.spec.ts: -------------------------------------------------------------------------------- 1 | import { jsonTransformer } from './websocket.json.transformer'; 2 | 3 | test('#jsonTransformer decodes and encodes incoming and outgoing data', () => { 4 | // given 5 | const jsonObject = { type: 'EVENT', payload: 'test_data' }; 6 | const jsonString = JSON.stringify(jsonObject); 7 | 8 | // when 9 | const decodedData = jsonTransformer.decode(jsonString); 10 | const encodedData = jsonTransformer.encode(jsonObject); 11 | 12 | // then 13 | expect(decodedData).toEqual(jsonObject); 14 | expect(encodedData).toEqual(jsonString); 15 | }); 16 | -------------------------------------------------------------------------------- /packages/websockets/src/transformer/websocket.json.transformer.ts: -------------------------------------------------------------------------------- 1 | import { EventTransformer } from './websocket.transformer.interface'; 2 | 3 | export const jsonTransformer: EventTransformer = { 4 | decode: event => JSON.parse(event), 5 | encode: event => JSON.stringify(event), 6 | }; 7 | -------------------------------------------------------------------------------- /packages/websockets/src/transformer/websocket.transformer.interface.ts: -------------------------------------------------------------------------------- 1 | import { Event } from '@marblejs/core'; 2 | import { WebSocketData } from '../websocket.interface'; 3 | 4 | export interface EventTransformer { 5 | decode: (incomingEvent: U) => Event; 6 | encode: (outgoingEvent: Event) => U; 7 | } 8 | -------------------------------------------------------------------------------- /packages/websockets/src/websocket.interface.ts: -------------------------------------------------------------------------------- 1 | import * as WebSocket from 'ws'; 2 | 3 | export enum WebSocketStatus { 4 | NORMAL_CLOSURE = 1000, 5 | GOING_AWAY = 1001, 6 | PROTOCOL_ERROR = 1002, 7 | UNSUPPORTED_DATA = 1003, 8 | NO_STATUS_RECIEVED = 1005, 9 | ABNORMAL_CLOSURE = 1006, 10 | INVALID_FRAME_PAYLOAD_DATA = 1007, 11 | POLICY_VALIDATION = 1008, 12 | MESSAGE_TOO_BIG = 1009, 13 | MISSING_EXTENSION = 1010, 14 | INTERNAL_ERROR = 1011, 15 | SERVICE_RESTART = 1012, 16 | TRY_AGAIN_LATER = 1013, 17 | BAD_GATEWAY = 1014, 18 | TLS_HANDSHAKE = 1015, 19 | } 20 | 21 | export enum WebSocketConnectionLiveness { 22 | ALIVE, 23 | DEAD, 24 | } 25 | 26 | export type WebSocketData = string | Buffer | ArrayBuffer | Buffer[]; 27 | 28 | export const WebsocketReadyStateMap = { 29 | [WebSocket.OPEN]: 'open', 30 | [WebSocket.CLOSED]: 'closed', 31 | [WebSocket.CLOSING]: 'closing', 32 | [WebSocket.CONNECTING]: 'connecting', 33 | }; 34 | -------------------------------------------------------------------------------- /packages/websockets/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist/", 5 | "rootDir": "./src" 6 | }, 7 | "include": ["src/**/*.ts"], 8 | "references": [ 9 | { "path": "../core" }, 10 | { "path": "../http" } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /scripts/run-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | RABBITMQ_CONTAINER=marble-rabbit 4 | 5 | docker-compose -f docker-compose.yml up -d 6 | 7 | node scripts/wait-rabbitmq.js 8 | node scripts/wait-redis.js 9 | 10 | if [ "$SCOPE" == "unit" ]; then 11 | jest --expand --coverage --detectOpenHandles 12 | elif [ "$SCOPE" == "watch" ]; then 13 | jest --expand --onlyChanged --watch 14 | else 15 | jest --expand --detectOpenHandles --runInBand 16 | fi 17 | 18 | if [ $? -ne 0 ]; then 19 | exit 1 20 | fi 21 | 22 | docker-compose -f docker-compose.yml down 23 | -------------------------------------------------------------------------------- /scripts/test-helpers.js: -------------------------------------------------------------------------------- 1 | global.fail = message => { throw new Error(message); }; 2 | -------------------------------------------------------------------------------- /scripts/wait-rabbitmq.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | 3 | const amqplib = require('amqplib'); 4 | const chalk = require('chalk'); 5 | const SECOND = 1000; 6 | 7 | const log = msg => console.info(chalk.yellow(msg)); 8 | 9 | const wait = async () => { 10 | try { 11 | const conn = await amqplib.connect('amqp://localhost:5672'); 12 | const channel = await conn.createChannel(); 13 | process.exit(); 14 | } catch { 15 | log(' -- Waiting for RabbitMQ to be ready...'); 16 | setTimeout(() => wait(), 5 * SECOND); 17 | } 18 | } 19 | 20 | (async function() { 21 | await wait(); 22 | })(); 23 | -------------------------------------------------------------------------------- /scripts/wait-redis.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | 3 | const redis = require('redis'); 4 | const chalk = require('chalk'); 5 | const SECOND = 1000; 6 | 7 | const log = msg => console.info(chalk.yellow(msg)); 8 | 9 | const wait = async () => { 10 | log(' -- Waiting for Redis to be ready...'); 11 | const client = await redis.createClient(); 12 | await new Promise((res, rej) => client.on('connect', err => err ? rej() : res(undefined))); 13 | } 14 | 15 | (async function() { 16 | await wait(); 17 | process.exit(); 18 | })(); 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "declaration": true, 5 | "declarationMap": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "module": "commonjs", 9 | "moduleResolution": "node", 10 | "noImplicitAny": false, 11 | "noEmitOnError": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noImplicitReturns": true, 14 | "suppressImplicitAnyIndexErrors": true, 15 | "noUnusedLocals": false, 16 | "resolveJsonModule": true, 17 | "incremental": true, 18 | "pretty": true, 19 | "strict": true, 20 | "target": "es6", 21 | "lib": [ 22 | "ES2020" 23 | ], 24 | "baseUrl": "." 25 | }, 26 | "include": [ 27 | "./packages/**/*.ts", 28 | "./packages/**/*.d.ts", 29 | "./packages/**/*.json" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "noEmitOnError": false 5 | } 6 | } 7 | --------------------------------------------------------------------------------

= 28 | EventWithPayload 29 | 30 | export type EventsUnion any & { type?: string }; 32 | }> = ReturnType; 33 | -------------------------------------------------------------------------------- /packages/core/src/index.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable deprecation/deprecation */ 2 | import * as API from './index'; 3 | 4 | describe('@marblejs/core public API', () => { 5 | test('apis are defined', () => { 6 | expect(API.combineEffects).toBeDefined(); 7 | expect(API.combineMiddlewares).toBeDefined(); 8 | expect(API.createEffectContext).toBeDefined(); 9 | expect(API.event).toBeDefined(); 10 | expect(API.coreErrorFactory).toBeDefined(); 11 | 12 | // errors 13 | expect(API.CoreError).toBeDefined(); 14 | expect(API.EventError).toBeDefined(); 15 | expect(API.isEventError).toBeDefined(); 16 | expect(API.isCoreError).toBeDefined(); 17 | 18 | // operators 19 | expect(API.use).toBeDefined(); 20 | expect(API.act).toBeDefined(); 21 | expect(API.matchEvent).toBeDefined(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | // core - error 2 | export { coreErrorFactory, CoreErrorOptions } from './error/error.factory'; 3 | export { CoreError, EventError, isCoreError, isEventError } from './error/error.model'; 4 | 5 | // core - effects 6 | export { combineEffects, combineMiddlewares } from './effects/effects.combiner'; 7 | export { createEffectContext } from './effects/effectsContext.factory'; 8 | export * from './effects/effects.interface'; 9 | 10 | // core - operators 11 | export * from './operators'; 12 | 13 | // core - logger 14 | export * from './logger'; 15 | 16 | // core - event 17 | export * from './event/event'; 18 | export * from './event/event.factory'; 19 | export * from './event/event.interface'; 20 | 21 | // core - listener 22 | export * from './listener/listener.factory'; 23 | export * from './listener/listener.interface'; 24 | 25 | // core - context 26 | export * from './context/context.hook'; 27 | export * from './context/context.logger'; 28 | export * from './context/context'; 29 | export * from './context/context.helper'; 30 | export * from './context/context.reader.factory'; 31 | export * from './context/context.token.factory'; 32 | -------------------------------------------------------------------------------- /packages/core/src/listener/listener.factory.ts: -------------------------------------------------------------------------------- 1 | import { flow } from 'fp-ts/lib/function'; 2 | import { createReader, ReaderHandler } from '../context/context.reader.factory'; 3 | import { ListenerConfig, Listener, ListenerHandler } from './listener.interface'; 4 | 5 | export const createListener = ( 6 | fn: (config?: T) => ReaderHandler 7 | ): Listener => flow(fn, createReader); 8 | -------------------------------------------------------------------------------- /packages/core/src/listener/listener.interface.ts: -------------------------------------------------------------------------------- 1 | import { IO } from 'fp-ts/lib/IO'; 2 | import { Reader } from 'fp-ts/lib/Reader'; 3 | import { Effect } from '../effects/effects.interface'; 4 | import { Context, BoundDependency } from '../context/context'; 5 | 6 | export interface ListenerConfig { 7 | effects?: any[]; 8 | middlewares?: any[]; 9 | error$?: Effect; 10 | output$?: Effect; 11 | } 12 | 13 | export type ListenerHandler = (...args: any[]) => void; 14 | 15 | export interface Listener< 16 | T extends ListenerConfig = ListenerConfig, 17 | U extends ListenerHandler = ListenerHandler, 18 | > { 19 | (config?: T): Reader; 20 | } 21 | 22 | export interface ServerIO extends IO> { 23 | context: Context; 24 | } 25 | 26 | export interface ServerConfig< 27 | T extends Effect, 28 | U extends ReturnType = ReturnType, 29 | > { 30 | event$?: T; 31 | listener: U; 32 | dependencies?: (BoundDependency | undefined | null)[]; 33 | } 34 | -------------------------------------------------------------------------------- /packages/core/src/logger/index.ts: -------------------------------------------------------------------------------- 1 | export * from './logger'; 2 | export * from './logger.interface'; 3 | export * from './logger.token'; 4 | -------------------------------------------------------------------------------- /packages/core/src/logger/logger.interface.ts: -------------------------------------------------------------------------------- 1 | import { IO } from 'fp-ts/lib/IO'; 2 | 3 | export type Logger = (opts: LoggerOptions) => IO; 4 | 5 | export enum LoggerLevel { INFO, WARN, ERROR, DEBUG, VERBOSE } 6 | 7 | export type LoggerOptions = { 8 | tag: string; 9 | type: string; 10 | message: string; 11 | level?: LoggerLevel; 12 | data?: Record; 13 | }; 14 | 15 | export const enum LoggerTag { 16 | CORE = 'core', 17 | HTTP = 'http', 18 | MESSAGING = 'messaging', 19 | EVENT_BUS = 'event_bus', 20 | WEBSOCKETS = 'websockets', 21 | } 22 | -------------------------------------------------------------------------------- /packages/core/src/logger/logger.token.ts: -------------------------------------------------------------------------------- 1 | import { createContextToken } from '../context/context.token.factory'; 2 | import { Logger } from './logger.interface'; 3 | 4 | export const LoggerToken = createContextToken('LoggerToken'); 5 | -------------------------------------------------------------------------------- /packages/core/src/logger/logger.ts: -------------------------------------------------------------------------------- 1 | import * as chalk from 'chalk'; 2 | import * as IO from 'fp-ts/lib/IO'; 3 | import * as O from 'fp-ts/lib/Option'; 4 | import { identity, constUndefined, pipe } from 'fp-ts/lib/function'; 5 | import { createReader } from '../context/context.reader.factory'; 6 | import { trunc } from '../+internal/utils'; 7 | import { Logger, LoggerLevel } from './logger.interface'; 8 | 9 | const print = (message: string): IO.IO => () => { 10 | process.stdout.write(message + '\n'); 11 | }; 12 | 13 | const colorizeText = (level: LoggerLevel): ((s: string) => string) => 14 | pipe( 15 | O.fromNullable({ 16 | [LoggerLevel.ERROR]: chalk.red, 17 | [LoggerLevel.INFO]: chalk.green, 18 | [LoggerLevel.WARN]: chalk.yellow, 19 | [LoggerLevel.DEBUG]: chalk.magenta, 20 | [LoggerLevel.VERBOSE]: identity, 21 | }[level]), 22 | O.getOrElse(() => identity), 23 | ); 24 | 25 | const formatDate = (date: Date): string => date 26 | .toISOString() 27 | .replace(/T/, ' ') 28 | .replace(/\..+/, ''); 29 | 30 | export const logger = createReader(() => opts => { 31 | const sep = ' - '; 32 | const truncItem = trunc(15); 33 | const colorize = colorizeText(opts.level ?? LoggerLevel.INFO); 34 | 35 | const sign = chalk.magenta('λ'); 36 | const now: string = formatDate(new Date()); 37 | const pid: string = chalk.green((process.pid.toString() ?? '-')); 38 | const tag: string = chalk.gray(truncItem(opts.tag)) + ' ' + colorize(`[${opts.type}]`); 39 | const message: string = opts.level === LoggerLevel.ERROR ? chalk.red(opts.message) : opts.message; 40 | 41 | return print(sign + sep + pid + sep + now + sep + tag + sep + message); 42 | }); 43 | 44 | export const mockLogger = createReader(() => _ => IO.of(constUndefined)); 45 | -------------------------------------------------------------------------------- /packages/core/src/operators/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable deprecation/deprecation */ 2 | export { use } from './use/use.operator'; 3 | export { act } from './act/act.operator'; 4 | export { matchEvent } from './matchEvent/matchEvent.operator'; 5 | -------------------------------------------------------------------------------- /packages/core/src/operators/use/use.operator.ts: -------------------------------------------------------------------------------- 1 | import { Observable, of } from 'rxjs'; 2 | import { mergeMap } from 'rxjs/operators'; 3 | import { EffectContext, EffectMiddlewareLike } from '../../effects/effects.interface'; 4 | 5 | /** 6 | * @deprecated since version 4.0, apply middlewares direcly to the effect Observable chain 7 | * 8 | * `use` operator will be deleted in the next major version (v5.0) 9 | */ 10 | export const use = 11 | (middleware: EffectMiddlewareLike, effectContext?: EffectContext) => 12 | (source$: Observable): Observable => 13 | source$.pipe( 14 | mergeMap(req => middleware(of(req), effectContext)) 15 | ); 16 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist/", 5 | "rootDir": "./src" 6 | }, 7 | "include": ["src/**/*.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/http/README.md: -------------------------------------------------------------------------------- 1 |