├── .github └── workflows │ ├── docker.yml │ ├── generate.yml │ └── kubernetes.yml ├── .gitignore ├── LICENSE ├── README.md ├── meta.js ├── package-lock.json ├── package.json ├── template ├── .dockerignore ├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── .vscode │ └── launch.json ├── Dockerfile ├── README.md ├── docker-compose.env ├── docker-compose.yml ├── jest.config.js ├── k8s.yaml ├── mixins │ └── db.mixin.ts ├── moleculer.config.ts ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ └── main.css ├── services │ ├── api.service.ts │ ├── greeter.service.ts │ └── products.service.ts ├── test │ ├── integration │ │ └── products.service.spec.ts │ └── unit │ │ ├── mixins │ │ └── db.mixin.spec.ts │ │ └── services │ │ ├── greeter.spec.ts │ │ └── products.spec.ts ├── tsconfig.build.json ├── tsconfig.eslint.json └── tsconfig.json └── test ├── ci ├── answers.json ├── kind-config.yaml └── update-answers.js └── demo └── answers.json /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Test with Docker 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | node-version: [16.x, 18.x] 12 | # transporter: [NATS, Redis, MQTT, AMQP, AMQP10, STAN, Kafka, TCP] 13 | transporter: [NATS, Redis, MQTT, AMQP, STAN, Kafka, TCP] 14 | fail-fast: false 15 | steps: 16 | - uses: actions/checkout@v3 17 | 18 | - name: Use Node.js ${{ matrix.node-version }} with ${{ matrix.transporter }} transporter 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | 23 | - name: Install dependencies 24 | run: npm i 25 | 26 | - name: Create answers file 27 | run: node update-answers.js 28 | working-directory: ./test/ci 29 | env: 30 | TRANSPORTER: ${{ matrix.transporter }} 31 | 32 | - name: Generate project with '${{ matrix.transporter }}' transporter 33 | run: npm test 34 | 35 | - name: Run tests in the generated project 36 | run: npm test 37 | working-directory: ./ci-test 38 | 39 | - run: cat ./ci-test/docker-compose.env 40 | - run: cat ./ci-test/docker-compose.yml 41 | 42 | - name: Start containers 43 | run: npm run dc:up 44 | working-directory: ./ci-test 45 | 46 | - name: Sleeping 30 secs 47 | run: sleep 30 48 | 49 | - name: Check containers 50 | run: docker-compose ps 51 | working-directory: ./ci-test 52 | 53 | - run: curl --silent --show-error --fail http://localhost:3000/api/greeter/hello 54 | - run: curl --silent --show-error --fail http://localhost:3000/api/products 55 | 56 | - name: Check logs 57 | run: docker-compose logs 58 | working-directory: ./ci-test 59 | if: failure() 60 | 61 | - name: Stop containers 62 | run: npm run dc:down 63 | working-directory: ./ci-test 64 | -------------------------------------------------------------------------------- /.github/workflows/generate.yml: -------------------------------------------------------------------------------- 1 | name: Generate a demo project 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | generate: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | - name: Use Node.js 18.x 16 | uses: actions/setup-node@v3 17 | with: 18 | node-version: 18.x 19 | 20 | - name: Install dependencies 21 | run: npm i 22 | - name: Generate project 23 | run: node_modules/.bin/moleculer init --answers test/demo/answers.json --no-install . project-demo 24 | 25 | - name: Initialize Git repo 26 | run: git init 27 | working-directory: ./project-demo 28 | 29 | - name: Set remote 30 | run: git remote add origin https://${{ secrets.GH_TOKEN }}@github.com/moleculerjs/project-typescript-demo.git 31 | working-directory: ./project-demo 32 | 33 | - name: Add files 34 | run: git add --all 35 | working-directory: ./project-demo 36 | 37 | - name: Configure Git user 38 | run: git config --global user.email "hello@moleculer.services" && git config --global user.name "Moleculer" 39 | working-directory: ./project-demo 40 | 41 | - name: Commit 42 | run: git commit -m "Generated files" 43 | working-directory: ./project-demo 44 | 45 | - name: Git push 46 | run: git push --force origin master 47 | working-directory: ./project-demo 48 | -------------------------------------------------------------------------------- /.github/workflows/kubernetes.yml: -------------------------------------------------------------------------------- 1 | name: Test with Kubernetes 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | transporter: [NATS, Redis, MQTT, AMQP, AMQP10, STAN, Kafka] 12 | fail-fast: false 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | 17 | - name: Use Node.js with ${{ matrix.transporter }} transporter 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: 18.x 21 | 22 | - name: Install dependencies 23 | run: npm i 24 | 25 | - name: Create answers file 26 | run: node update-answers.js 27 | working-directory: ./test/ci 28 | env: 29 | TRANSPORTER: ${{ matrix.transporter }} 30 | 31 | - name: Generate project with '${{ matrix.transporter }}' transporter 32 | run: npm test 33 | 34 | - name: Run tests in the generated project 35 | run: npm test 36 | working-directory: ./ci-test 37 | 38 | - name: Start a local Docker Registry 39 | run: docker run -d --restart=always -p 5000:5000 --name registry registry:2 40 | 41 | - name: Build Docker image 42 | run: docker build -t ci-test:demo . 43 | working-directory: ./ci-test 44 | 45 | - uses: engineerd/setup-kind@v0.5.0 46 | with: 47 | version: "v0.17.0" 48 | config: ./test/ci/kind-config.yaml 49 | 50 | - run: kubectl cluster-info 51 | - run: kubectl get nodes 52 | - run: kubectl get pods -n kube-system 53 | 54 | - name: Load Docker image 55 | run: kind load docker-image ci-test:demo ci-test:demo 56 | 57 | - name: Install NGINX Ingress 58 | run: kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/kind/deploy.yaml 59 | 60 | - name: Kubectl apply 61 | working-directory: ./ci-test 62 | run: | 63 | # Fix nginx ingress issue: https://github.com/kubernetes/ingress-nginx/issues/5401#issuecomment-662424306 64 | kubectl delete -A ValidatingWebhookConfiguration ingress-nginx-admission 65 | sed 's/image: ci-test/image: ci-test:demo/g' k8s.yaml | kubectl apply -f - 66 | 67 | - name: Sleeping 120 secs 68 | run: sleep 120 69 | 70 | - name: Check pods 71 | run: kubectl get all 72 | 73 | - run: curl --silent --show-error --fail http://ci-test.127.0.0.1.nip.io/api/greeter/hello 74 | - run: curl --silent --show-error --fail http://ci-test.127.0.0.1.nip.io/api/products 75 | 76 | - name: Check logs 77 | run: kubectl logs deployment/products 78 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | .DS_Store 6 | .idea 7 | template/.DS_Store 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # nyc test coverage 20 | .nyc_output 21 | 22 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 23 | .grunt 24 | 25 | # node-waf configuration 26 | .lock-wscript 27 | 28 | # Compiled binary addons (http://nodejs.org/api/addons.html) 29 | build/Release 30 | 31 | # Dependency directories 32 | node_modules 33 | jspm_packages 34 | 35 | # Optional npm cache directory 36 | .npm 37 | 38 | # Optional REPL history 39 | .node_repl_history 40 | 41 | /tmp 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 MoleculerJS 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Moleculer template: `project-typescript` 2 | :mortar_board: Moleculer-based microservices project template for Typescript project. 3 | 4 | ## Features 5 | - Moleculer v0.14 with full-detailed `moleculer.config.ts` file. 6 | - Common mono-repo project with a demo `greeter` service. 7 | - Sample database `products` service (with file-based NeDB in development & MongoDB in production). 8 | - Optional API Gateway service with detailed service settings. 9 | - Beautiful static welcome page to test generated services & watch nodes and services. 10 | - Optional Transporter & Cacher. 11 | - Metrics & Tracing. 12 | - Docker & Docker Compose & Kubernetes files. 13 | - Unit tests with [Jest](http://facebook.github.io/jest/). 14 | - Lint with [ESLint](http://eslint.org/). 15 | - Launch file for debugging in [VSCode](https://code.visualstudio.com/). 16 | 17 | 18 | ## Install 19 | To install use the [moleculer-cli](https://github.com/moleculerjs/moleculer-cli) tool. 20 | 21 | ```bash 22 | $ moleculer init project-typescript my-project 23 | ``` 24 | 25 | ## Prompts 26 | ``` 27 | $ moleculer init project-typescript moleculer-demo 28 | 29 | Template repo: moleculerjs/moleculer-template-project-typescript 30 | ? Add API Gateway (moleculer-web) service? Yes 31 | ? Would you like to communicate with other nodes? Yes 32 | ? Select a transporter NATS (recommended) 33 | ? Would you like to use cache? Yes 34 | ? Select a cacher solution Memory 35 | ? Would you like to enable metrics? Yes 36 | ? Select a reporter solution Prometheus 37 | ? Would you like to enable tracing? Yes 38 | ? Select a exporter solution Console 39 | ? Add Docker & Kubernetes sample files? Yes 40 | ? Use ESLint to lint your code? Yes 41 | Create 'moleculer-demo' folder... 42 | ? Would you like to run 'npm install'? Yes 43 | ``` 44 | 45 | ## NPM scripts 46 | - `npm run dev`: Start development mode (load all services locally without transporter with hot-reload & REPL) 47 | - `npm run start`: Start production mode (set `SERVICES` env variable to load certain services) 48 | - `npm run cli`: Start a CLI and connect to production. _Don't forget to set production namespace with `--ns` argument in script_ 49 | - `npm run lint`: Run ESLint 50 | - `npm run ci`: Run continuous test mode with watching 51 | - `npm test`: Run tests & generate coverage report 52 | - `npm run dc:up`: Start the stack with Docker Compose 53 | - `npm run dc:logs`: Watch & follow the container logs 54 | - `npm run dc:down`: Stop the stack with Docker Compose 55 | 56 | ## License 57 | moleculer-template-project-typescript is available under the [MIT license](https://tldrlegal.com/license/mit-license). 58 | 59 | ## Contact 60 | Copyright (c) 2023 MoleculerJS 61 | 62 | [![@moleculerjs](https://img.shields.io/badge/github-moleculerjs-green.svg)](https://github.com/moleculerjs) [![@MoleculerJS](https://img.shields.io/badge/twitter-MoleculerJS-blue.svg)](https://twitter.com/MoleculerJS) 63 | -------------------------------------------------------------------------------- /meta.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = function(values) { 4 | return { 5 | questions: [ 6 | { 7 | type: "confirm", 8 | name: "apiGW", 9 | message: "Add API Gateway (moleculer-web) service?", 10 | default: true 11 | }, 12 | { 13 | type: "confirm", 14 | name: "needTransporter", 15 | message: "Would you like to communicate with other nodes?", 16 | default: true 17 | }, 18 | { 19 | type: "list", 20 | name: "transporter", 21 | message: "Select a transporter", 22 | choices: [ 23 | { name: "NATS (recommended)", value: "NATS" }, 24 | { name: "Redis", value: "Redis" }, 25 | { name: "MQTT", value: "MQTT" }, 26 | { name: "AMQP", value: "AMQP" }, 27 | { name: "TCP", value: "TCP" }, 28 | { name: "NATS Streaming", value: "STAN" }, 29 | { name: "Kafka", value: "Kafka" }, 30 | { name: "AMQP 1.0 (experimental)", value: "AMQP10" } 31 | ], 32 | when(answers) { return answers.needTransporter; }, 33 | default: "NATS" 34 | }, 35 | { 36 | type: "confirm", 37 | name: "needCacher", 38 | message: "Would you like to use cache?", 39 | default: false 40 | }, 41 | { 42 | type: "list", 43 | name: "cacher", 44 | message: "Select a cacher solution", 45 | choices: [ 46 | { name: "Memory", value: "Memory" }, 47 | { name: "Redis", value: "Redis" } 48 | ], 49 | when(answers) { return answers.needCacher; }, 50 | default: "Memory" 51 | }, 52 | { 53 | type: "confirm", 54 | name: "dbService", 55 | message: "Add DB sample service?", 56 | default: true 57 | }, 58 | { 59 | type: "confirm", 60 | name: "metrics", 61 | message: "Would you like to enable metrics?", 62 | default: true 63 | }, 64 | { 65 | type: "list", 66 | name: "reporter", 67 | message: "Select a reporter solution", 68 | choices: [ 69 | { name: "Console", value: "Console" }, 70 | { name: "CSV", value: "Redis" }, 71 | { name: "Event", value: "CSV" }, 72 | { name: "Prometheus", value: "Prometheus" }, 73 | { name: "Datadog", value: "Datadog" }, 74 | { name: "StatsD", value: "StatsD" } 75 | ], 76 | when(answers) { return answers.metrics; }, 77 | default: "Prometheus" 78 | }, 79 | { 80 | type: "confirm", 81 | name: "tracing", 82 | message: "Would you like to enable tracing?", 83 | default: true 84 | }, 85 | { 86 | type: "list", 87 | name: "exporter", 88 | message: "Select an exporter solution", 89 | choices: [ 90 | { name: "Console", value: "Console" }, 91 | { name: "EventLegacy", value: "EventLegacy" }, 92 | { name: "Event", value: "CSV" }, 93 | { name: "Jaeger", value: "Jaeger" }, 94 | { name: "Datadog", value: "Datadog" }, 95 | { name: "Zipkin", value: "Zipkin" }, 96 | { name: "NewRelic", value: "NewRelic" } 97 | ], 98 | when(answers) { return answers.tracing; }, 99 | default: "Console" 100 | }, 101 | { 102 | type: "confirm", 103 | name: "docker", 104 | message: "Add Docker & Kubernetes sample files?", 105 | default: true 106 | }, 107 | { 108 | type: "confirm", 109 | name: "lint", 110 | message: "Use ESLint to lint your code?", 111 | default: true 112 | } 113 | ], 114 | 115 | metalsmith: { 116 | before(metalsmith) { 117 | const data = metalsmith.metadata(); 118 | data.redis = data.cacher == "Redis" || data.transporter == "Redis"; 119 | data.hasDepends = (data.needCacher && data.cacher !== 'Memory') || (data.needTransporter && data.transporter != "TCP"); 120 | } 121 | }, 122 | 123 | skipInterpolation: [ 124 | //"public/index.html" 125 | ], 126 | 127 | filters: { 128 | "services/api.service.ts": "apiGW", 129 | "public/**/*": "apiGW", 130 | 131 | "services/products.service.ts": "dbService", 132 | "mixins/db.mixin.ts": "dbService", 133 | "test/mixins/db.mixin.spec.ts": "dbService", 134 | "test/integration/products.service.spec.ts": "dbService", 135 | "test/unit/services/products.spec.ts": "dbService", 136 | 137 | ".eslintrc.js": "lint", 138 | "tsconfig.eslint.json": "lint", 139 | ".prettierignore": "lint", 140 | ".prettierrc.json": "lint", 141 | 142 | ".dockerignore": "docker", 143 | "docker-compose.*": "docker", 144 | "Dockerfile": "docker", 145 | "k8s.yaml": "docker" 146 | }, 147 | 148 | completeMessage: ` 149 | To get started: 150 | 151 | cd {{projectName}} 152 | npm run dev 153 | 154 | ` 155 | }; 156 | }; 157 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "moleculer-template-project-typescript", 3 | "version": "3.0.0", 4 | "description": "Project template for Moleculer-based projects with typescript", 5 | "main": "meta.js", 6 | "scripts": { 7 | "dev": "rimraf tmp && moleculer init . tmp", 8 | "test": "moleculer init --answers test/ci/answers.json --install . ci-test", 9 | "start": "cd tmp && node node_modules/moleculer/bin/moleculer-runner services" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/moleculerjs/moleculer-template-project-typescript.git" 14 | }, 15 | "keywords": [], 16 | "author": "MoleculerJS", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/moleculerjs/moleculer-template-project-typescript/issues" 20 | }, 21 | "homepage": "https://github.com/moleculerjs/moleculer-template-project-typescript#readme", 22 | "devDependencies": { 23 | "moleculer-cli": "^0.7.0", 24 | "rimraf": "^3.0.2" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /template/.dockerignore: -------------------------------------------------------------------------------- 1 | docker-compose.env 2 | docker-compose.yml 3 | Dockerfile 4 | node_modules 5 | test 6 | .vscode 7 | data 8 | -------------------------------------------------------------------------------- /template/.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | indent_style = space 9 | indent_size = 4 10 | end_of_line = lf 11 | charset = utf-8 12 | trim_trailing_whitespace = true 13 | insert_final_newline = true 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | 18 | [*.json] 19 | indent_size = 2 20 | 21 | [*.{yml,yaml}] 22 | trim_trailing_whitespace = false 23 | indent_size = 2 24 | 25 | [*.{cjs,mjs,js,ts,tsx,css,html}] 26 | indent_style = tab 27 | indent_size = unset # reset to ide view preferences 28 | -------------------------------------------------------------------------------- /template/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | "airbnb-base", 4 | "airbnb-typescript/base", 5 | "plugin:jest/recommended", 6 | "plugin:jest/style", 7 | "plugin:@typescript-eslint/recommended", 8 | "plugin:import/typescript", 9 | "prettier", 10 | ], 11 | 12 | parserOptions: { tsconfigRootDir: __dirname, project: "./tsconfig.eslint.json" }, 13 | 14 | env: { es2021: true, node: true, "jest/globals": true }, 15 | 16 | plugins: ["jest"], 17 | 18 | ignorePatterns: [ 19 | "node_modules", 20 | "dist", 21 | "coverage", 22 | // "**/*.d.ts", 23 | "!.*.js", 24 | "!.*.cjs", 25 | "!.*.mjs", 26 | ], 27 | 28 | rules: { 29 | // enforce curly brace usage 30 | curly: ["error", "all"], 31 | 32 | // allow class methods which do not use this 33 | "class-methods-use-this": "off", 34 | 35 | // Allow use of ForOfStatement - no-restricted-syntax does not allow us to turn off a rule. This block overrides the airbnb rule entirely 36 | // https://github.com/airbnb/javascript/blob/7152396219e290426a03e47837e53af6bcd36bbe/packages/eslint-config-airbnb-base/rules/style.js#L257-L263 37 | "no-restricted-syntax": [ 38 | "error", 39 | { 40 | selector: "ForInStatement", 41 | message: 42 | "for..in loops iterate over the entire prototype chain, which is virtually never what you want. Use Object.{keys,values,entries}, and iterate over the resulting array.", 43 | }, 44 | { 45 | selector: "LabeledStatement", 46 | message: 47 | "Labels are a form of GOTO; using them makes code confusing and hard to maintain and understand.", 48 | }, 49 | { 50 | selector: "WithStatement", 51 | message: 52 | "`with` is disallowed in strict mode because it makes code impossible to predict and optimize.", 53 | }, 54 | ], 55 | 56 | // underscore dangle will be handled by @typescript-eslint/naming-convention 57 | "no-underscore-dangle": "off", 58 | 59 | // enforce consistent sort order 60 | "sort-imports": ["error", { ignoreCase: true, ignoreDeclarationSort: true }], 61 | 62 | // enforce convention in import order 63 | "import/order": [ 64 | "error", 65 | { 66 | "newlines-between": "never", 67 | groups: ["builtin", "external", "internal", "parent", "sibling", "index"], 68 | alphabetize: { order: "asc", caseInsensitive: true }, 69 | }, 70 | ], 71 | 72 | // ensure consistent array typings 73 | "@typescript-eslint/array-type": "error", 74 | 75 | // ban ts-comment except with description 76 | "@typescript-eslint/ban-ts-comment": [ 77 | "error", 78 | { 79 | "ts-expect-error": "allow-with-description", 80 | "ts-ignore": "allow-with-description", 81 | "ts-nocheck": true, 82 | "ts-check": false, 83 | }, 84 | ], 85 | 86 | // prefer type imports and exports 87 | "@typescript-eslint/consistent-type-exports": [ 88 | "error", 89 | { fixMixedExportsWithInlineTypeSpecifier: true }, 90 | ], 91 | "@typescript-eslint/consistent-type-imports": ["error", { prefer: "type-imports" }], 92 | 93 | // enforce consistent order of class members 94 | "@typescript-eslint/member-ordering": "error", 95 | 96 | // set up naming convention rules 97 | "@typescript-eslint/naming-convention": [ 98 | "error", 99 | // camelCase for everything not otherwise indicated 100 | { selector: "default", format: ["camelCase"] }, 101 | // allow known default exclusions 102 | { 103 | selector: "default", 104 | filter: { regex: "^(_id|__v)$", match: true }, 105 | format: null, 106 | }, 107 | // allow variables to be camelCase or UPPER_CASE 108 | { selector: "variable", format: ["camelCase", "UPPER_CASE"] }, 109 | // allow known variable exclusions 110 | { 111 | selector: "variable", 112 | filter: { regex: "^(_id|__v)$", match: true }, 113 | format: null, 114 | }, 115 | { 116 | // variables ending in Service should be PascalCase 117 | selector: "variable", 118 | filter: { regex: "^.*Service$", match: true }, 119 | format: ["PascalCase"], 120 | }, 121 | // do not enforce format on property names 122 | { selector: "property", format: null }, 123 | // PascalCase for classes and TypeScript keywords 124 | { 125 | selector: ["typeLike"], 126 | format: ["PascalCase"], 127 | }, 128 | ], 129 | 130 | // disallow parameter properties in favor of explicit class declarations 131 | "@typescript-eslint/no-parameter-properties": "error", 132 | 133 | // ensure unused variables are treated as an error 134 | // overrides @typescript-eslint/recommended -- '@typescript-eslint/no-unused-vars': 'warn' 135 | // https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/src/configs/recommended.ts 136 | "@typescript-eslint/no-unused-vars": "warn", 137 | }, 138 | 139 | overrides: [ 140 | { 141 | files: ["**/*.ts"], 142 | extends: ["plugin:@typescript-eslint/recommended-requiring-type-checking"], 143 | rules: { 144 | // disable rules turned on by @typescript-eslint/recommended-requiring-type-checking which are too noisy 145 | // https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/src/configs/recommended-requiring-type-checking.ts 146 | "@typescript-eslint/no-unsafe-argument": "off", 147 | "@typescript-eslint/no-unsafe-assignment": "off", 148 | "@typescript-eslint/no-unsafe-call": "off", 149 | "@typescript-eslint/no-unsafe-member-access": "off", 150 | "@typescript-eslint/no-unsafe-return": "off", 151 | "@typescript-eslint/restrict-template-expressions": "off", 152 | "@typescript-eslint/unbound-method": "off", 153 | 154 | // force explicit member accessibility modifiers 155 | "@typescript-eslint/explicit-member-accessibility": ["error", { accessibility: "no-public" }], 156 | 157 | // enforce return types on module boundaries 158 | "@typescript-eslint/explicit-module-boundary-types": "error", 159 | 160 | // allow empty functions 161 | "@typescript-eslint/no-empty-function": "off", 162 | }, 163 | }, 164 | 165 | { 166 | files: ["**/index.ts"], 167 | rules: { 168 | // prefer named exports for certain file types 169 | "import/prefer-default-export": "off", 170 | "import/no-default-export": "error", 171 | }, 172 | }, 173 | 174 | { 175 | files: [ 176 | "**/test/**/*.[jt]s?(x)", 177 | "**/__tests__/**/*.[jt]s?(x)", 178 | "**/?(*.)+(spec|test).[jt]s?(x)", 179 | ], 180 | rules: { 181 | // allow tests to create multiple classes 182 | "max-classes-per-file": "off", 183 | 184 | // allow side effect constructors 185 | "no-new": "off", 186 | 187 | // allow import with CommonJS export 188 | "import/no-import-module-exports": "off", 189 | 190 | // allow dev dependencies 191 | "import/no-extraneous-dependencies": [ 192 | "error", 193 | { devDependencies: true, optionalDependencies: false, peerDependencies: false }, 194 | ], 195 | 196 | // disallow use of "it" for test blocks 197 | "jest/consistent-test-it": ["error", { fn: "test", withinDescribe: "test" }], 198 | 199 | // ensure all tests contain an assertion 200 | "jest/expect-expect": "error", 201 | 202 | // no commented out tests 203 | "jest/no-commented-out-tests": "error", 204 | 205 | // no duplicate test hooks 206 | "jest/no-duplicate-hooks": "error", 207 | 208 | // valid titles 209 | "jest/valid-title": "error", 210 | 211 | // no if conditionals in tests 212 | "jest/no-if": "error", 213 | 214 | // expect statements in test blocks 215 | "jest/no-standalone-expect": "error", 216 | 217 | // disallow returning from test 218 | "jest/no-test-return-statement": "error", 219 | 220 | // disallow truthy and falsy in tests 221 | "jest/no-restricted-matchers": ["error", { toBeFalsy: null, toBeTruthy: null }], 222 | 223 | // prefer called with 224 | "jest/prefer-called-with": "error", 225 | 226 | "jest/no-conditional-expect": "off" 227 | }, 228 | }, 229 | 230 | { 231 | files: [ 232 | "**/test/**/*.ts?(x)", 233 | "**/__tests__/**/*.ts?(x)", 234 | "**/?(*.)+(spec|test).ts?(x)", 235 | ], 236 | rules: { 237 | // allow explicit any in tests 238 | "@typescript-eslint/no-explicit-any": "off", 239 | 240 | // allow non-null-assertions 241 | "@typescript-eslint/no-non-null-assertion": "off", 242 | 243 | // allow empty arrow functions 244 | "@typescript-eslint/no-empty-function": ["warn", { allow: ["arrowFunctions"] }], 245 | }, 246 | }, 247 | 248 | { 249 | files: ["./.*.js", "./*.js"], 250 | rules: { 251 | // allow requires in config files 252 | "@typescript-eslint/no-var-requires": "off", 253 | }, 254 | }, 255 | 256 | { 257 | files: ["**/*.d.ts"], 258 | rules: { 259 | // allow tests to create multiple classes 260 | "max-classes-per-file": "off", 261 | "@typescript-eslint/naming-convention": "off", 262 | 263 | "lines-between-class-members": "off", 264 | "@typescript-eslint/lines-between-class-members": "off", 265 | "@typescript-eslint/member-ordering": "off", 266 | "@typescript-eslint/ban-types": "off" 267 | }, 268 | }, 269 | ], 270 | }; 271 | -------------------------------------------------------------------------------- /template/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Optional npm cache directory 40 | .npm 41 | 42 | # Optional eslint cache 43 | .eslintcache 44 | 45 | # Optional REPL history 46 | .node_repl_history 47 | 48 | # Output of 'npm pack' 49 | *.tgz 50 | 51 | # Yarn Integrity file 52 | .yarn-integrity 53 | 54 | # dotenv environment variables file 55 | .env 56 | 57 | # next.js build output 58 | .next 59 | 60 | # JetBrains IDE 61 | .idea 62 | 63 | # Don't track transpiled files 64 | dist/ 65 | -------------------------------------------------------------------------------- /template/.prettierignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | tsconfig.json 3 | 4 | node_modules 5 | dist 6 | coverage 7 | -------------------------------------------------------------------------------- /template/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "tabWidth": 4, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": false, 7 | "trailingComma": "all", 8 | "bracketSpacing": true, 9 | "bracketSameLine": false, 10 | "arrowParens": "always", 11 | "overrides": [ 12 | { 13 | "files": ["*.html"], 14 | "options": { "parser": "vue" } 15 | }, 16 | { 17 | "files": ["*.{cjs,mjs,js,jsx,ts,tsx,d.ts,css,html,graphql}"], 18 | "options": { "useTabs": true } 19 | }, 20 | { 21 | "files": ["*.{json,yml,yaml}"], 22 | "options": { "tabWidth": 2 } 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /template/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible Node.js debug attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Debug", 11 | "program": "${workspaceRoot}/node_modules/moleculer/bin/moleculer-runner.js", 12 | "sourceMaps": true, 13 | "runtimeArgs": [ 14 | "-r", 15 | "ts-node/register" 16 | ], 17 | "cwd": "${workspaceRoot}", 18 | "args": [ 19 | "services/**/*.service.ts" 20 | ] 21 | }, 22 | { 23 | "type": "node", 24 | "request": "launch", 25 | "name": "Jest", 26 | "program": "${workspaceRoot}/node_modules/jest-cli/bin/jest.js", 27 | "args": [ 28 | "--runInBand" 29 | ], 30 | "cwd": "${workspaceRoot}", 31 | "runtimeArgs": [ 32 | "--nolazy" 33 | ] 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /template/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16 as build 2 | 3 | WORKDIR /tmp/app 4 | 5 | # Install dependencies 6 | COPY package.json package-lock.json ./ 7 | RUN npm ci --silent 8 | 9 | # Copy source 10 | COPY . . 11 | 12 | # Build 13 | RUN npm run build 14 | 15 | # ------------------- 16 | FROM node:16-alpine 17 | 18 | WORKDIR /app 19 | 20 | # Copy source 21 | COPY . . 22 | 23 | # Copy built files 24 | COPY --from=build /tmp/app/dist . 25 | 26 | # Build and cleanup 27 | ENV NODE_ENV=production 28 | RUN npm ci --omit=dev 29 | 30 | # Start server 31 | CMD ["node", "./node_modules/moleculer/bin/moleculer-runner.js"] 32 | -------------------------------------------------------------------------------- /template/README.md: -------------------------------------------------------------------------------- 1 | [![Moleculer](https://badgen.net/badge/Powered%20by/Moleculer/0e83cd)](https://moleculer.services) 2 | 3 | # {{projectName}} 4 | This is a [Moleculer](https://moleculer.services/)-based microservices project. Generated with the [Moleculer CLI](https://moleculer.services/docs/0.14/moleculer-cli.html). 5 | 6 | ## Usage 7 | Start the project with `npm run dev` command. 8 | {{#apiGW}} 9 | After starting, open the http://localhost:3000/ URL in your browser. 10 | On the welcome page you can test the generated services via API Gateway and check the nodes & services. 11 | 12 | {{/apiGW}} 13 | In the terminal, try the following commands: 14 | - `nodes` - List all connected nodes. 15 | - `actions` - List all registered service actions. 16 | - `call greeter.hello` - Call the `greeter.hello` action. 17 | - `call greeter.welcome --name John` - Call the `greeter.welcome` action with the `name` parameter. 18 | {{#dbService}}- `call products.list` - List the products (call the `products.list` action).{{/dbService}} 19 | 20 | 21 | ## Services 22 | - **api**: API Gateway services 23 | - **greeter**: Sample service with `hello` and `welcome` actions. 24 | {{#dbService}}- **products**: Sample DB service. To use with MongoDB, set `MONGO_URI` environment variables and install MongoDB adapter with `npm i moleculer-db-adapter-mongo`. 25 | 26 | ## Mixins 27 | - **db.mixin**: Database access mixin for services. Based on [moleculer-db](https://github.com/moleculerjs/moleculer-db#readme) 28 | {{/dbService}} 29 | 30 | 31 | ## Useful links 32 | 33 | * Moleculer website: https://moleculer.services/ 34 | * Moleculer Documentation: https://moleculer.services/docs/0.14/ 35 | 36 | ## NPM scripts 37 | 38 | - `npm run dev`: Start development mode (load all services locally with hot-reload & REPL) 39 | - `npm run start`: Start production mode (set `SERVICES` env variable to load certain services) 40 | - `npm run cli`: Start a CLI and connect to production. Don't forget to set production namespace with `--ns` argument in script{{#lint}} 41 | - `npm run lint`: Run ESLint{{/lint}} 42 | - `npm run ci`: Run continuous test mode with watching 43 | - `npm test`: Run tests & generate coverage report{{#docker}} 44 | - `npm run dc:up`: Start the stack with Docker Compose 45 | - `npm run dc:down`: Stop the stack with Docker Compose{{/docker}} 46 | -------------------------------------------------------------------------------- /template/docker-compose.env: -------------------------------------------------------------------------------- 1 | NAMESPACE= 2 | LOGGER=true 3 | LOGLEVEL=info 4 | SERVICEDIR=services 5 | MOLECULER_CONFIG=moleculer.config.js 6 | 7 | {{#if_eq transporter "NATS"}} 8 | TRANSPORTER=nats://nats:4222 9 | {{/if_eq}} 10 | {{#if_eq transporter "Redis"}} 11 | TRANSPORTER=redis://redis:6379 12 | {{/if_eq}} 13 | {{#if_eq transporter "MQTT"}} 14 | TRANSPORTER=mqtt://mqtt:1883 15 | {{/if_eq}} 16 | {{#if_eq transporter "AMQP"}} 17 | TRANSPORTER=amqp://rabbitmq:5672 18 | {{/if_eq}} 19 | {{#if_eq transporter "AMQP10"}} 20 | TRANSPORTER=amqp10://guest:guest@activemq:5672 21 | {{/if_eq}} 22 | {{#if_eq transporter "STAN"}} 23 | TRANSPORTER=stan://stan:4222 24 | {{/if_eq}} 25 | {{#if_eq transporter "Kafka"}} 26 | TRANSPORTER=kafka://kafka:9092 27 | {{/if_eq}} 28 | {{#if_eq transporter "TCP"}} 29 | TRANSPORTER=TCP 30 | {{/if_eq}} 31 | 32 | {{#if_eq cacher "Memory"}} 33 | CACHER=Memory 34 | {{/if_eq}} 35 | {{#if_eq cacher "Redis"}} 36 | CACHER=redis://redis:6379 37 | {{/if_eq}} 38 | 39 | {{#dbService}} 40 | MONGO_URI=mongodb://mongo/{{projectName}} 41 | {{/dbService}} 42 | -------------------------------------------------------------------------------- /template/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.3" 2 | 3 | services: 4 | 5 | {{#apiGW}} 6 | api: 7 | build: 8 | context: . 9 | image: {{projectName}} 10 | env_file: docker-compose.env 11 | environment: 12 | SERVICES: api 13 | PORT: 3000 14 | {{#hasDepends}} 15 | depends_on: 16 | {{/hasDepends}} 17 | {{#if_eq transporter "NATS"}} 18 | - nats 19 | {{/if_eq}} 20 | {{#redis}} 21 | - redis 22 | {{/redis}} 23 | {{#if_eq transporter "MQTT"}} 24 | - mqtt 25 | {{/if_eq}} 26 | {{#if_eq transporter "AMQP"}} 27 | - rabbitmq 28 | {{/if_eq}} 29 | {{#if_eq transporter "AMQP10"}} 30 | - activemq 31 | {{/if_eq}} 32 | {{#if_eq transporter "STAN"}} 33 | - stan 34 | {{/if_eq}} 35 | {{#if_eq transporter "Kafka"}} 36 | - zookeeper 37 | {{/if_eq}} 38 | labels: 39 | - "traefik.enable=true" 40 | - "traefik.http.routers.api-gw.rule=PathPrefix(`/`)" 41 | - "traefik.http.services.api-gw.loadbalancer.server.port=3000" 42 | networks: 43 | - internal 44 | {{/apiGW}} 45 | 46 | greeter: 47 | build: 48 | context: . 49 | image: {{projectName}} 50 | env_file: docker-compose.env 51 | environment: 52 | SERVICES: greeter 53 | {{#hasDepends}} 54 | depends_on: 55 | {{/hasDepends}} 56 | {{#if_eq transporter "NATS"}} 57 | - nats 58 | {{/if_eq}} 59 | {{#redis}} 60 | - redis 61 | {{/redis}} 62 | {{#if_eq transporter "MQTT"}} 63 | - mqtt 64 | {{/if_eq}} 65 | {{#if_eq transporter "AMQP"}} 66 | - rabbitmq 67 | {{/if_eq}} 68 | {{#if_eq transporter "AMQP10"}} 69 | - activemq 70 | {{/if_eq}} 71 | {{#if_eq transporter "STAN"}} 72 | - stan 73 | {{/if_eq}} 74 | {{#if_eq transporter "Kafka"}} 75 | - zookeeper 76 | {{/if_eq}} 77 | networks: 78 | - internal 79 | {{#dbService}} 80 | 81 | products: 82 | build: 83 | context: . 84 | image: {{projectName}} 85 | env_file: docker-compose.env 86 | environment: 87 | SERVICES: products 88 | depends_on: 89 | - mongo 90 | {{#if_eq transporter "NATS"}} 91 | - nats 92 | {{/if_eq}} 93 | {{#redis}} 94 | - redis 95 | {{/redis}} 96 | {{#if_eq transporter "MQTT"}} 97 | - mqtt 98 | {{/if_eq}} 99 | {{#if_eq transporter "AMQP"}} 100 | - rabbitmq 101 | {{/if_eq}} 102 | {{#if_eq transporter "AMQP10"}} 103 | - activemq 104 | {{/if_eq}} 105 | {{#if_eq transporter "STAN"}} 106 | - stan 107 | {{/if_eq}} 108 | {{#if_eq transporter "Kafka"}} 109 | - zookeeper 110 | {{/if_eq}} 111 | networks: 112 | - internal 113 | 114 | mongo: 115 | image: mongo:4 116 | volumes: 117 | - data:/data/db 118 | networks: 119 | - internal 120 | {{/dbService}} 121 | {{#if_eq transporter "NATS"}} 122 | 123 | nats: 124 | image: nats:2 125 | networks: 126 | - internal 127 | {{/if_eq}} 128 | {{#redis}} 129 | 130 | redis: 131 | image: redis:alpine 132 | networks: 133 | - internal 134 | {{/redis}} 135 | {{#if_eq transporter "MQTT"}} 136 | 137 | mqtt: 138 | image: ncarlier/mqtt 139 | networks: 140 | - internal 141 | {{/if_eq}} 142 | {{#if_eq transporter "AMQP"}} 143 | 144 | rabbitmq: 145 | image: rabbitmq:3 146 | networks: 147 | - internal 148 | {{/if_eq}} 149 | {{#if_eq transporter "AMQP10"}} 150 | 151 | activemq: 152 | image: rmohr/activemq 153 | networks: 154 | - internal 155 | {{/if_eq}} 156 | {{#if_eq transporter "STAN"}} 157 | 158 | stan: 159 | image: nats-streaming 160 | networks: 161 | - internal 162 | {{/if_eq}} 163 | {{#if_eq transporter "Kafka"}} 164 | 165 | zookeeper: 166 | image: bitnami/zookeeper 167 | environment: 168 | - ALLOW_ANONYMOUS_LOGIN=yes 169 | networks: 170 | - internal 171 | 172 | kafka: 173 | image: bitnami/kafka 174 | environment: 175 | - KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper:2181 176 | - KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://kafka:9092 177 | - ALLOW_PLAINTEXT_LISTENER=yes 178 | depends_on: 179 | - zookeeper 180 | networks: 181 | - internal 182 | {{/if_eq}} 183 | 184 | {{#apiGW}} 185 | traefik: 186 | image: traefik:v2.4 187 | command: 188 | - "--api.insecure=true" # Don't do that in production! 189 | - "--providers.docker=true" 190 | - "--providers.docker.exposedbydefault=false" 191 | ports: 192 | - 3000:80 193 | - 3001:8080 194 | volumes: 195 | - /var/run/docker.sock:/var/run/docker.sock:ro 196 | networks: 197 | - internal 198 | - default 199 | {{/apiGW}} 200 | 201 | networks: 202 | internal: 203 | 204 | volumes: 205 | data: 206 | -------------------------------------------------------------------------------- /template/jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | coverageDirectory: "./coverage", 6 | rootDir: "./", 7 | roots: [ 8 | "./test" 9 | ] 10 | }; 11 | -------------------------------------------------------------------------------- /template/k8s.yaml: -------------------------------------------------------------------------------- 1 | ######################################################### 2 | # Common Environment variables ConfigMap 3 | ######################################################### 4 | apiVersion: v1 5 | kind: ConfigMap 6 | metadata: 7 | name: common-env 8 | data: 9 | NAMESPACE: "" 10 | LOGLEVEL: info 11 | SERVICEDIR: services 12 | {{#if_eq transporter "NATS"}}TRANSPORTER: nats://nats:4222{{/if_eq}} 13 | {{#if_eq transporter "Redis"}}TRANSPORTER: redis://redis:6379{{/if_eq}} 14 | {{#if_eq transporter "MQTT"}}TRANSPORTER: mqtt://mqtt:1883{{/if_eq}} 15 | {{#if_eq transporter "AMQP"}}TRANSPORTER: amqp://rabbitmq:5672{{/if_eq}} 16 | {{#if_eq transporter "STAN"}}TRANSPORTER: stan://stan:4222{{/if_eq}} 17 | {{#if_eq transporter "Kafka"}}TRANSPORTER: kafka://kafka:9092{{/if_eq}} 18 | {{#if_eq transporter "AMQP10"}}TRANSPORTER: amqp10://guest:guest@activemq:5672{{/if_eq}} 19 | {{#if_eq cacher "Memory"}}CACHER: Memory{{/if_eq}} 20 | {{#if_eq cacher "Redis"}}CACHER: redis://redis:6379{{/if_eq}} 21 | {{#dbService}}MONGO_URI: mongodb://mongo/{{projectName}}{{/dbService}} 22 | 23 | {{#apiGW}} 24 | --- 25 | ######################################################### 26 | # Service for Moleculer API Gateway service 27 | ######################################################### 28 | apiVersion: v1 29 | kind: Service 30 | metadata: 31 | name: api 32 | spec: 33 | selector: 34 | app: api 35 | ports: 36 | - port: 3000 37 | targetPort: 3000 38 | 39 | --- 40 | ######################################################### 41 | # Ingress for Moleculer API Gateway 42 | ######################################################### 43 | apiVersion: networking.k8s.io/v1 44 | kind: Ingress 45 | metadata: 46 | name: ingress 47 | #annotations: 48 | # kubernetes.io/ingress.class: nginx 49 | spec: 50 | rules: 51 | - host: {{projectName}}.127.0.0.1.nip.io 52 | http: 53 | paths: 54 | - path: / 55 | pathType: Prefix 56 | backend: 57 | service: 58 | name: api 59 | port: 60 | number: 3000 61 | 62 | --- 63 | ######################################################### 64 | # API Gateway service 65 | ######################################################### 66 | apiVersion: apps/v1 67 | kind: Deployment 68 | metadata: 69 | name: api 70 | spec: 71 | selector: 72 | matchLabels: 73 | app: api 74 | replicas: 2 75 | template: 76 | metadata: 77 | labels: 78 | app: api 79 | spec: 80 | containers: 81 | - name: api 82 | image: {{projectName}} 83 | imagePullPolicy: IfNotPresent 84 | envFrom: 85 | - configMapRef: 86 | name: common-env 87 | env: 88 | - name: SERVICES 89 | value: api 90 | {{/apiGW}} 91 | 92 | --- 93 | ######################################################### 94 | # Greeter service 95 | ######################################################### 96 | apiVersion: apps/v1 97 | kind: Deployment 98 | metadata: 99 | name: greeter 100 | spec: 101 | selector: 102 | matchLabels: 103 | app: greeter 104 | replicas: 2 105 | template: 106 | metadata: 107 | labels: 108 | app: greeter 109 | spec: 110 | containers: 111 | - name: greeter 112 | image: {{projectName}} 113 | imagePullPolicy: IfNotPresent 114 | envFrom: 115 | - configMapRef: 116 | name: common-env 117 | env: 118 | - name: SERVICES 119 | value: greeter 120 | 121 | {{#dbService}} 122 | --- 123 | ######################################################### 124 | # Products service 125 | ######################################################### 126 | apiVersion: apps/v1 127 | kind: Deployment 128 | metadata: 129 | name: products 130 | spec: 131 | selector: 132 | matchLabels: 133 | app: products 134 | replicas: 2 135 | template: 136 | metadata: 137 | labels: 138 | app: products 139 | spec: 140 | containers: 141 | - name: products 142 | image: {{projectName}} 143 | imagePullPolicy: IfNotPresent 144 | envFrom: 145 | - configMapRef: 146 | name: common-env 147 | env: 148 | - name: SERVICES 149 | value: products 150 | 151 | --- 152 | ######################################################### 153 | # MongoDB server 154 | ######################################################### 155 | apiVersion: apps/v1 156 | kind: StatefulSet 157 | metadata: 158 | name: mongo 159 | labels: 160 | app: mongo 161 | spec: 162 | selector: 163 | matchLabels: 164 | app: mongo 165 | replicas: 1 166 | serviceName: mongo 167 | template: 168 | metadata: 169 | labels: 170 | app: mongo 171 | spec: 172 | containers: 173 | - image: mongo 174 | name: mongo 175 | ports: 176 | - containerPort: 27017 177 | resources: {} 178 | volumeMounts: 179 | - mountPath: /data/db 180 | name: mongo-data 181 | volumes: 182 | - name: mongo-data 183 | persistentVolumeClaim: 184 | claimName: mongo-data 185 | 186 | --- 187 | ######################################################### 188 | # Persistent volume for MongoDB 189 | ######################################################### 190 | apiVersion: v1 191 | kind: PersistentVolumeClaim 192 | metadata: 193 | name: mongo-data 194 | labels: 195 | name: mongo-data 196 | spec: 197 | accessModes: 198 | - ReadWriteOnce 199 | resources: 200 | requests: 201 | storage: 100Mi 202 | 203 | --- 204 | ######################################################### 205 | # MongoDB service 206 | ######################################################### 207 | apiVersion: v1 208 | kind: Service 209 | metadata: 210 | name: mongo 211 | labels: 212 | app: mongo 213 | spec: 214 | ports: 215 | - port: 27017 216 | targetPort: 27017 217 | selector: 218 | app: mongo 219 | 220 | {{/dbService}} 221 | 222 | {{#if_eq transporter "NATS"}} 223 | --- 224 | ######################################################### 225 | # NATS transporter service 226 | ######################################################### 227 | apiVersion: v1 228 | kind: Service 229 | metadata: 230 | name: nats 231 | spec: 232 | selector: 233 | app: nats 234 | ports: 235 | - port: 4222 236 | name: nats 237 | targetPort: 4222 238 | 239 | --- 240 | ######################################################### 241 | # NATS transporter 242 | ######################################################### 243 | apiVersion: apps/v1 244 | kind: Deployment 245 | metadata: 246 | name: nats 247 | spec: 248 | selector: 249 | matchLabels: 250 | app: nats 251 | replicas: 1 252 | strategy: 253 | type: Recreate 254 | template: 255 | metadata: 256 | labels: 257 | app: nats 258 | spec: 259 | containers: 260 | - name: nats 261 | image: nats 262 | ports: 263 | - containerPort: 4222 264 | name: nats 265 | {{/if_eq}} 266 | 267 | {{#redis}} 268 | --- 269 | ######################################################### 270 | # Redis service 271 | ######################################################### 272 | apiVersion: v1 273 | kind: Service 274 | metadata: 275 | name: redis 276 | spec: 277 | selector: 278 | app: redis 279 | ports: 280 | - port: 6379 281 | name: redis 282 | targetPort: 6379 283 | 284 | --- 285 | ######################################################### 286 | # Redis server (transporter/cacher) 287 | ######################################################### 288 | apiVersion: apps/v1 289 | kind: Deployment 290 | metadata: 291 | name: redis 292 | spec: 293 | selector: 294 | matchLabels: 295 | app: redis 296 | replicas: 1 297 | strategy: 298 | type: Recreate 299 | template: 300 | metadata: 301 | labels: 302 | app: redis 303 | spec: 304 | containers: 305 | - name: redis 306 | image: redis 307 | ports: 308 | - containerPort: 6379 309 | name: redis 310 | {{/redis}} 311 | 312 | {{#if_eq transporter "MQTT"}} 313 | --- 314 | ######################################################### 315 | # MQTT transporter service 316 | ######################################################### 317 | apiVersion: v1 318 | kind: Service 319 | metadata: 320 | name: mqtt 321 | spec: 322 | selector: 323 | app: mqtt 324 | ports: 325 | - port: 1883 326 | name: mqtt 327 | targetPort: 1883 328 | 329 | --- 330 | ######################################################### 331 | # MQTT transporter 332 | ######################################################### 333 | apiVersion: apps/v1 334 | kind: Deployment 335 | metadata: 336 | name: mqtt 337 | spec: 338 | selector: 339 | matchLabels: 340 | app: mqtt 341 | replicas: 1 342 | strategy: 343 | type: Recreate 344 | template: 345 | metadata: 346 | labels: 347 | app: mqtt 348 | spec: 349 | containers: 350 | - name: mqtt 351 | image: ncarlier/mqtt 352 | ports: 353 | - containerPort: 1883 354 | name: mqtt 355 | {{/if_eq}} 356 | 357 | {{#if_eq transporter "AMQP"}} 358 | --- 359 | ######################################################### 360 | # AMQP (RabbitMQ) transporter service 361 | ######################################################### 362 | apiVersion: v1 363 | kind: Service 364 | metadata: 365 | name: rabbitmq 366 | spec: 367 | selector: 368 | app: rabbitmq 369 | ports: 370 | - port: 5672 371 | name: rabbitmq 372 | targetPort: 5672 373 | 374 | --- 375 | ######################################################### 376 | # AMQP (RabbitMQ) transporter 377 | ######################################################### 378 | apiVersion: apps/v1 379 | kind: Deployment 380 | metadata: 381 | name: rabbitmq 382 | spec: 383 | selector: 384 | matchLabels: 385 | app: rabbitmq 386 | replicas: 1 387 | strategy: 388 | type: Recreate 389 | template: 390 | metadata: 391 | labels: 392 | app: rabbitmq 393 | spec: 394 | containers: 395 | - name: rabbitmq 396 | image: rabbitmq:3 397 | ports: 398 | - containerPort: 5672 399 | name: rabbitmq 400 | {{/if_eq}} 401 | 402 | {{#if_eq transporter "STAN"}} 403 | --- 404 | ######################################################### 405 | # NATS Streaming transporter service 406 | ######################################################### 407 | apiVersion: v1 408 | kind: Service 409 | metadata: 410 | name: stan 411 | spec: 412 | selector: 413 | app: stan 414 | ports: 415 | - port: 4222 416 | name: stan 417 | targetPort: 4222 418 | 419 | --- 420 | ######################################################### 421 | # NATS Streaming transporter 422 | ######################################################### 423 | apiVersion: apps/v1 424 | kind: Deployment 425 | metadata: 426 | name: stan 427 | spec: 428 | selector: 429 | matchLabels: 430 | app: stan 431 | replicas: 1 432 | strategy: 433 | type: Recreate 434 | template: 435 | metadata: 436 | labels: 437 | app: stan 438 | spec: 439 | containers: 440 | - name: stan 441 | image: nats-streaming 442 | ports: 443 | - containerPort: 4222 444 | name: stan 445 | {{/if_eq}} 446 | 447 | {{#if_eq transporter "Kafka"}} 448 | --- 449 | ######################################################### 450 | # Zookeeper service 451 | ######################################################### 452 | apiVersion: v1 453 | kind: Service 454 | metadata: 455 | name: zookeeper 456 | spec: 457 | selector: 458 | app: zookeeper 459 | ports: 460 | - port: 2181 461 | name: zookeeper 462 | targetPort: 2181 463 | 464 | --- 465 | ######################################################### 466 | # Zookeeper deployment 467 | ######################################################### 468 | apiVersion: apps/v1 469 | kind: Deployment 470 | metadata: 471 | name: zookeeper 472 | spec: 473 | selector: 474 | matchLabels: 475 | app: zookeeper 476 | replicas: 1 477 | strategy: 478 | type: Recreate 479 | template: 480 | metadata: 481 | labels: 482 | app: zookeeper 483 | spec: 484 | containers: 485 | - name: zookeeper 486 | image: bitnami/zookeeper 487 | ports: 488 | - containerPort: 2181 489 | name: zookeeper 490 | env: 491 | - name: ALLOW_ANONYMOUS_LOGIN 492 | value: "yes" 493 | 494 | --- 495 | ######################################################### 496 | # Kafka transporter service 497 | ######################################################### 498 | apiVersion: v1 499 | kind: Service 500 | metadata: 501 | name: kafka 502 | spec: 503 | selector: 504 | app: kafka 505 | ports: 506 | - port: 9092 507 | name: kafka 508 | targetPort: 9092 509 | 510 | --- 511 | ######################################################### 512 | # Kafka transporter 513 | ######################################################### 514 | apiVersion: apps/v1 515 | kind: Deployment 516 | metadata: 517 | name: kafka 518 | spec: 519 | selector: 520 | matchLabels: 521 | app: kafka 522 | replicas: 1 523 | strategy: 524 | type: Recreate 525 | template: 526 | metadata: 527 | labels: 528 | app: kafka 529 | spec: 530 | containers: 531 | - name: kafka 532 | image: bitnami/kafka 533 | ports: 534 | - containerPort: 9092 535 | name: kafka 536 | env: 537 | - name: KAFKA_CFG_ZOOKEEPER_CONNECT 538 | value: zookeeper:2181 539 | - name: KAFKA_CFG_ADVERTISED_LISTENERS 540 | value: PLAINTEXT://kafka:9092 541 | - name: ALLOW_PLAINTEXT_LISTENER 542 | value: "yes" 543 | {{/if_eq}} 544 | 545 | {{#if_eq transporter "AMQP10"}} 546 | --- 547 | ######################################################### 548 | # AMQP 1.0 (ActiveMQ) transporter service 549 | ######################################################### 550 | apiVersion: v1 551 | kind: Service 552 | metadata: 553 | name: activemq 554 | spec: 555 | selector: 556 | app: activemq 557 | ports: 558 | - port: 5672 559 | name: activemq 560 | targetPort: 5672 561 | 562 | --- 563 | ######################################################### 564 | # AMQP 1.0 (ActiveMQ) transporter 565 | ######################################################### 566 | apiVersion: apps/v1 567 | kind: Deployment 568 | metadata: 569 | name: activemq 570 | spec: 571 | selector: 572 | matchLabels: 573 | app: activemq 574 | replicas: 1 575 | strategy: 576 | type: Recreate 577 | revisionHistoryLimit: 0 578 | template: 579 | metadata: 580 | labels: 581 | app: activemq 582 | spec: 583 | containers: 584 | - name: activemq 585 | image: rmohr/activemq 586 | ports: 587 | - containerPort: 5672 588 | name: activemq 589 | 590 | {{/if_eq}} 591 | -------------------------------------------------------------------------------- /template/mixins/db.mixin.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import type { Context, Service, ServiceSchema } from "moleculer"; 3 | import type { DbAdapter, MoleculerDB } from "moleculer-db"; 4 | import DbService from "moleculer-db"; 5 | import MongoDbAdapter from "moleculer-db-adapter-mongo"; 6 | 7 | export type DbServiceMethods = { 8 | seedDb?(): Promise; 9 | }; 10 | 11 | type DbServiceSchema = Partial & 12 | Partial> & { 13 | collection?: string; 14 | }; 15 | 16 | export type DbServiceThis = Service & DbServiceMethods; 17 | 18 | export default function createDbServiceMixin(collection: string): DbServiceSchema { 19 | const cacheCleanEventName = `cache.clean.${collection}`; 20 | 21 | const schema: DbServiceSchema = { 22 | mixins: [DbService], 23 | 24 | events: { 25 | /** 26 | * Subscribe to the cache clean event. If it's triggered 27 | * clean the cache entries for this service. 28 | */ 29 | async [cacheCleanEventName](this: DbServiceThis) { 30 | if (this.broker.cacher) { 31 | await this.broker.cacher.clean(`${this.fullName}.*`); 32 | } 33 | }, 34 | }, 35 | 36 | methods: { 37 | /** 38 | * Send a cache clearing event when an entity changed. 39 | */ 40 | async entityChanged(type: string, json: unknown, ctx: Context): Promise { 41 | await ctx.broadcast(cacheCleanEventName); 42 | }, 43 | }, 44 | 45 | async started(this: DbServiceThis) { 46 | // Check the count of items in the DB. If it's empty, 47 | // call the `seedDB` method of the service. 48 | if (this.seedDB) { 49 | const count = await this.adapter.count(); 50 | if (count === 0) { 51 | this.logger.info( 52 | `The '${collection}' collection is empty. Seeding the collection...`, 53 | ); 54 | await this.seedDB(); 55 | this.logger.info( 56 | "Seeding is done. Number of records:", 57 | await this.adapter.count(), 58 | ); 59 | } 60 | } 61 | }, 62 | }; 63 | 64 | if (process.env.MONGO_URI) { 65 | // Mongo adapter 66 | schema.adapter = new MongoDbAdapter(process.env.MONGO_URI); 67 | schema.collection = collection; 68 | } else if (process.env.NODE_ENV === "test") { 69 | // NeDB memory adapter for testing 70 | schema.adapter = new DbService.MemoryAdapter(); 71 | } else { 72 | // NeDB file DB adapter 73 | 74 | // Create data folder 75 | if (!fs.existsSync("./data")) { 76 | fs.mkdirSync("./data"); 77 | } 78 | 79 | schema.adapter = new DbService.MemoryAdapter({ filename: `./data/${collection}.db` }); 80 | } 81 | 82 | return schema; 83 | } 84 | -------------------------------------------------------------------------------- /template/moleculer.config.ts: -------------------------------------------------------------------------------- 1 | import type { BrokerOptions, MetricRegistry, ServiceBroker } from "moleculer"; 2 | import { Errors } from "moleculer"; 3 | 4 | /** 5 | * Moleculer ServiceBroker configuration file 6 | * 7 | * More info about options: 8 | * https://moleculer.services/docs/0.14/configuration.html 9 | * 10 | * 11 | * Overwriting options in production: 12 | * ================================ 13 | * You can overwrite any option with environment variables. 14 | * For example to overwrite the "logLevel" value, use `LOGLEVEL=warn` env var. 15 | * To overwrite a nested parameter, e.g. retryPolicy.retries, use `RETRYPOLICY_RETRIES=10` env var. 16 | * 17 | * To overwrite broker’s deeply nested default options, which are not presented in "moleculer.config.js", 18 | * use the `MOL_` prefix and double underscore `__` for nested properties in .env file. 19 | * For example, to set the cacher prefix to `MYCACHE`, you should declare an env var as `MOL_CACHER__OPTIONS__PREFIX=mycache`. 20 | * It will set this: 21 | * { 22 | * cacher: { 23 | * options: { 24 | * prefix: "mycache" 25 | * } 26 | * } 27 | * } 28 | */ 29 | const brokerConfig: BrokerOptions = { 30 | // Namespace of nodes to segment your nodes on the same network. 31 | namespace: "", 32 | // Unique node identifier. Must be unique in a namespace. 33 | nodeID: null, 34 | // Custom metadata store. Store here what you want. Accessing: `this.broker.metadata` 35 | metadata: {}, 36 | 37 | // Enable/disable logging or use custom logger. More info: https://moleculer.services/docs/0.14/logging.html 38 | // Available logger types: "Console", "File", "Pino", "Winston", "Bunyan", "debug", "Log4js", "Datadog" 39 | logger: { 40 | type: "Console", 41 | options: { 42 | // Using colors on the output 43 | colors: true, 44 | // Print module names with different colors (like docker-compose for containers) 45 | moduleColors: false, 46 | // Line formatter. It can be "json", "short", "simple", "full", a `Function` or a template string like "{timestamp} {level} {nodeID}/{mod}: {msg}" 47 | formatter: "full", 48 | // Custom object printer. If not defined, it uses the `util.inspect` method. 49 | objectPrinter: null, 50 | // Auto-padding the module name in order to messages begin at the same column. 51 | autoPadding: false, 52 | }, 53 | }, 54 | // Default log level for built-in console logger. It can be overwritten in logger options above. 55 | // Available values: trace, debug, info, warn, error, fatal 56 | logLevel: "info", 57 | 58 | // Define transporter. 59 | // More info: https://moleculer.services/docs/0.14/networking.html 60 | // Note: During the development, you don't need to define it because all services will be loaded locally. 61 | // In production you can set it via `TRANSPORTER=nats://localhost:4222` environment variable. 62 | transporter: null,{{#if needTransporter}} // "{{transporter}}"{{/if}} 63 | 64 | // Define a cacher. 65 | // More info: https://moleculer.services/docs/0.14/caching.html 66 | {{#if needCacher}}cacher: "{{cacher}}"{{/if}}{{#unless needCacher}}cacher: null{{/unless}}, 67 | 68 | // Define a serializer. 69 | // Available values: "JSON", "Avro", "ProtoBuf", "MsgPack", "Notepack", "Thrift". 70 | // More info: https://moleculer.services/docs/0.14/networking.html#Serialization 71 | serializer: "JSON", 72 | 73 | // Number of milliseconds to wait before reject a request with a RequestTimeout error. Disabled: 0 74 | requestTimeout: 10 * 1000, 75 | 76 | // Retry policy settings. More info: https://moleculer.services/docs/0.14/fault-tolerance.html#Retry 77 | retryPolicy: { 78 | // Enable feature 79 | enabled: false, 80 | // Count of retries 81 | retries: 5, 82 | // First delay in milliseconds. 83 | delay: 100, 84 | // Maximum delay in milliseconds. 85 | maxDelay: 1000, 86 | // Backoff factor for delay. 2 means exponential backoff. 87 | factor: 2, 88 | // A function to check failed requests. 89 | check: (err: Error) => 90 | err && err instanceof Errors.MoleculerRetryableError && !!err.retryable, 91 | }, 92 | 93 | // Limit of calling level. If it reaches the limit, broker will throw an MaxCallLevelError error. (Infinite loop protection) 94 | maxCallLevel: 100, 95 | 96 | // Number of seconds to send heartbeat packet to other nodes. 97 | heartbeatInterval: 10, 98 | // Number of seconds to wait before setting node to unavailable status. 99 | heartbeatTimeout: 30, 100 | 101 | // Cloning the params of context if enabled. High performance impact, use it with caution! 102 | contextParamsCloning: false, 103 | 104 | // Tracking requests and waiting for running requests before shuting down. More info: https://moleculer.services/docs/0.14/context.html#Context-tracking 105 | tracking: { 106 | // Enable feature 107 | enabled: false, 108 | // Number of milliseconds to wait before shuting down the process. 109 | shutdownTimeout: 5000, 110 | }, 111 | 112 | // Disable built-in request & emit balancer. (Transporter must support it, as well.). More info: https://moleculer.services/docs/0.14/networking.html#Disabled-balancer 113 | disableBalancer: false, 114 | 115 | // Settings of Service Registry. More info: https://moleculer.services/docs/0.14/registry.html 116 | registry: { 117 | // Define balancing strategy. More info: https://moleculer.services/docs/0.14/balancing.html 118 | // Available values: "RoundRobin", "Random", "CpuUsage", "Latency", "Shard" 119 | strategy: "RoundRobin", 120 | // Enable local action call preferring. Always call the local action instance if available. 121 | preferLocal: true, 122 | }, 123 | 124 | // Settings of Circuit Breaker. More info: https://moleculer.services/docs/0.14/fault-tolerance.html#Circuit-Breaker 125 | circuitBreaker: { 126 | // Enable feature 127 | enabled: false, 128 | // Threshold value. 0.5 means that 50% should be failed for tripping. 129 | threshold: 0.5, 130 | // Minimum request count. Below it, CB does not trip. 131 | minRequestCount: 20, 132 | // Number of seconds for time window. 133 | windowTime: 60, 134 | // Number of milliseconds to switch from open to half-open state 135 | halfOpenTime: 10 * 1000, 136 | // A function to check failed requests. 137 | check: (err: Error) => err && err instanceof Errors.MoleculerError && err.code >= 500, 138 | }, 139 | 140 | // Settings of bulkhead feature. More info: https://moleculer.services/docs/0.14/fault-tolerance.html#Bulkhead 141 | bulkhead: { 142 | // Enable feature. 143 | enabled: false, 144 | // Maximum concurrent executions. 145 | concurrency: 10, 146 | // Maximum size of queue 147 | maxQueueSize: 100, 148 | }, 149 | 150 | // Enable action & event parameter validation. More info: https://moleculer.services/docs/0.14/validating.html 151 | validator: true, 152 | 153 | errorHandler: null, 154 | 155 | // Enable/disable built-in metrics function. More info: https://moleculer.services/docs/0.14/metrics.html 156 | metrics: { 157 | enabled: {{#if metrics}}true{{/if}}{{#unless metrics}}false{{/unless}}, 158 | // Available built-in reporters: "Console", "CSV", "Event", "Prometheus", "Datadog", "StatsD" 159 | reporter: { 160 | type: "{{reporter}}", 161 | {{#if_eq reporter "Console"}} 162 | options: { 163 | // HTTP port 164 | port: 3030, 165 | // HTTP URL path 166 | path: "/metrics", 167 | // Default labels which are appended to all metrics labels 168 | defaultLabels: (registry: MetricRegistry) => ({ 169 | namespace: registry.broker.namespace, 170 | nodeID: registry.broker.nodeID, 171 | }), 172 | }, 173 | {{/if_eq}} 174 | {{#if_eq reporter "CSV"}} 175 | options: { 176 | // Folder of CSV files. 177 | folder: "./reports/metrics", 178 | // CSV field delimiter 179 | delimiter: ",", 180 | // CSV row delimiter 181 | rowDelimiter: "\n", 182 | // Saving mode. 183 | // - "metric" - save metrics to individual files 184 | // - "label" - save metrics by labels to individual files 185 | mode: "metric", 186 | // Saved metrics types. 187 | types: null, 188 | // Saving interval in seconds 189 | interval: 5, 190 | // Custom filename formatter 191 | filenameFormatter: null, 192 | // Custom CSV row formatter. 193 | rowFormatter: null, 194 | }, 195 | {{/if_eq}} 196 | {{#if_eq reporter "Event"}} 197 | options: { 198 | // Event name 199 | eventName: "$metrics.snapshot", 200 | // Broadcast or emit 201 | broadcast: false, 202 | // Event groups 203 | groups: null, 204 | // Send only changed metrics 205 | onlyChanges: false, 206 | // Sending interval in seconds 207 | interval: 5, 208 | }, 209 | {{/if_eq}} 210 | {{#if_eq reporter "Datadog"}} 211 | options: { 212 | // Hostname 213 | host: "my-host", 214 | // Base URL 215 | baseUrl: "https://api.datadoghq.eu/api/", // Default is https://api.datadoghq.com/api/ 216 | // API version 217 | apiVersion: "v1", 218 | // Server URL path 219 | path: "/series", 220 | // Datadog API Key 221 | apiKey: process.env.DATADOG_API_KEY, 222 | // Default labels which are appended to all metrics labels 223 | defaultLabels: (registry: MetricRegistry) => ({ 224 | namespace: registry.broker.namespace, 225 | nodeID: registry.broker.nodeID, 226 | }), 227 | // Sending interval in seconds 228 | interval: 10 229 | }, 230 | {{/if_eq}} 231 | {{#if_eq reporter "Prometheus"}} 232 | options: { 233 | // HTTP port 234 | port: 3030, 235 | // HTTP URL path 236 | path: "/metrics", 237 | // Default labels which are appended to all metrics labels 238 | defaultLabels: (registry: MetricRegistry) => ({ 239 | namespace: registry.broker.namespace, 240 | nodeID: registry.broker.nodeID, 241 | }), 242 | }, 243 | {{/if_eq}} 244 | {{#if_eq reporter "StatsD"}} 245 | options: { 246 | // Server host 247 | host: "localhost", 248 | // Server port 249 | port: 8125, 250 | // Maximum payload size. 251 | maxPayloadSize: 1300 252 | }, 253 | {{/if_eq}} 254 | }, 255 | }, 256 | 257 | // Enable built-in tracing function. More info: https://moleculer.services/docs/0.14/tracing.html 258 | tracing: { 259 | enabled: {{#if tracing}}true{{/if}}{{#unless tracing}}false{{/unless}}, 260 | // Available built-in exporters: "Console", "Datadog", "Event", "EventLegacy", "Jaeger", "Zipkin" 261 | exporter: { 262 | type: "{{exporter}}", // Console exporter is only for development! 263 | {{#if_eq exporter "Console"}} 264 | options: { 265 | // Custom logger 266 | logger: null, 267 | // Using colors 268 | colors: true, 269 | // Width of row 270 | width: 100, 271 | // Gauge width in the row 272 | gaugeWidth: 40, 273 | }, 274 | {{/if_eq}} 275 | {{#if_eq exporter "Datadog"}} 276 | options: { 277 | // Datadog Agent URL 278 | agentUrl: process.env.DD_AGENT_URL || "http://localhost:8126", 279 | // Environment variable 280 | env: process.env.DD_ENVIRONMENT || null, 281 | // Sampling priority. More info: https://docs.datadoghq.com/tracing/guide/trace_sampling_and_storage/?tab=java#sampling-rules 282 | samplingPriority: "AUTO_KEEP", 283 | // Default tags. They will be added into all span tags. 284 | defaultTags: null, 285 | // Custom Datadog Tracer options. More info: https://datadog.github.io/dd-trace-js/#tracer-settings 286 | tracerOptions: null, 287 | }, 288 | {{/if_eq}} 289 | {{#if_eq exporter "Event"}} 290 | options: { 291 | // Name of event 292 | eventName: "$tracing.spans", 293 | // Send event when a span started 294 | sendStartSpan: false, 295 | // Send event when a span finished 296 | sendFinishSpan: true, 297 | // Broadcast or emit event 298 | broadcast: false, 299 | // Event groups 300 | groups: null, 301 | // Sending time interval in seconds 302 | interval: 5, 303 | // Custom span object converter before sending 304 | spanConverter: null, 305 | // Default tags. They will be added into all span tags. 306 | defaultTags: null 307 | }, 308 | {{/if_eq}} 309 | {{#if_eq exporter "Jaeger"}} 310 | options: { 311 | // HTTP Reporter endpoint. If set, HTTP Reporter will be used. 312 | endpoint: null, 313 | // UDP Sender host option. 314 | host: "127.0.0.1", 315 | // UDP Sender port option. 316 | port: 6832, 317 | // Jaeger Sampler configuration. 318 | sampler: { 319 | // Sampler type. More info: https://www.jaegertracing.io/docs/1.14/sampling/#client-sampling-configuration 320 | type: "Const", 321 | // Sampler specific options. 322 | options: {} 323 | }, 324 | // Additional options for `Jaeger.Tracer` 325 | tracerOptions: {}, 326 | // Default tags. They will be added into all span tags. 327 | defaultTags: null 328 | }, 329 | {{/if_eq}} 330 | {{#if_eq exporter "Zipkin"}} 331 | options: { 332 | // Base URL for Zipkin server. 333 | baseURL: "http://localhost:9411", 334 | // Sending time interval in seconds. 335 | interval: 5, 336 | // Additional payload options. 337 | payloadOptions: { 338 | // Set `debug` property in payload. 339 | debug: false, 340 | // Set `shared` property in payload. 341 | shared: false, 342 | }, 343 | // Default tags. They will be added into all span tags. 344 | defaultTags: null 345 | }, 346 | {{/if_eq}} 347 | {{#if_eq exporter "NewRelic"}} 348 | options: { 349 | // Base URL for NewRelic server 350 | baseURL: 'https://trace-api.newrelic.com', 351 | // NewRelic Insert Key 352 | insertKey: 'my-secret-key', 353 | // Sending time interval in seconds. 354 | interval: 5, 355 | // Additional payload options. 356 | payloadOptions: { 357 | // Set `debug` property in payload. 358 | debug: false, 359 | // Set `shared` property in payload. 360 | shared: false, 361 | }, 362 | // Default tags. They will be added into all span tags. 363 | defaultTags: null, 364 | }, 365 | {{/if_eq}} 366 | }, 367 | }, 368 | 369 | // Register custom middlewares 370 | middlewares: [], 371 | 372 | // Register custom REPL commands. 373 | replCommands: null, 374 | 375 | // Called after broker created. 376 | // created(broker: ServiceBroker): void {}, 377 | 378 | // Called after broker started. 379 | // async started(broker: ServiceBroker): Promise {}, 380 | 381 | // Called after broker stopped. 382 | // async stopped(broker: ServiceBroker): Promise {}, 383 | 384 | }; 385 | 386 | export = brokerConfig; 387 | -------------------------------------------------------------------------------- /template/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "{{projectName}}", 3 | "version": "1.0.0", 4 | "description": "My Moleculer-based microservices project", 5 | "scripts": { 6 | "build": "tsc --project tsconfig.build.json", 7 | "dev": "ts-node ./node_modules/moleculer/bin/moleculer-runner.js --config moleculer.config.ts --hot --repl services/**/*.service.ts", 8 | "start": "moleculer-runner --config dist/moleculer.config.js", 9 | "test:types": "concurrently npm:prettier npm:lint npm:typecheck", 10 | "typecheck": "tsc --noEmit && echo \"tsc: no typecheck errors\"", 11 | "ci": "jest --watch", 12 | "test": "jest --coverage"{{#lint}}, 13 | "lint": "cross-env TIMING=1 eslint . --ext cjs,mjs,js,jsx,ts,tsx", 14 | "lint:fix": "cross-env TIMING=1 eslint . --ext cjs,mjs,js,jsx,ts,tsx --fix", 15 | "prettier": "prettier . --ignore-unknown --check", 16 | "prettier:fix": "prettier . --ignore-unknown --write"{{/lint}}{{#docker}}, 17 | "dc:up": "docker-compose up --build -d", 18 | "dc:logs": "docker-compose logs -f", 19 | "dc:down": "docker-compose down"{{/docker}} 20 | }, 21 | "keywords": [ 22 | "microservices", 23 | "moleculer" 24 | ], 25 | "author": "", 26 | "devDependencies": { 27 | {{#lint}} 28 | "@typescript-eslint/eslint-plugin": "^5.44.0", 29 | "@typescript-eslint/parser": "^5.44.0", 30 | "eslint": "^8.28.0", 31 | "eslint-config-airbnb-base": "^15.0.0", 32 | "eslint-config-airbnb-typescript": "^17.0.0", 33 | "eslint-config-prettier": "^8.5.0", 34 | "eslint-plugin-import": "^2.26.0", 35 | "eslint-plugin-jest": "^27.1.6", 36 | "prettier": "^2.8.0", 37 | {{/lint}} 38 | "@jest/globals": "^29.3.1", 39 | "@types/jest": "^29.2.3", 40 | "@types/node": "^18.11.9", 41 | "concurrently": "^7.6.0", 42 | "cross-env": "^7.0.3", 43 | "jest": "^29.3.1", 44 | "moleculer-repl": "^0.7.3", 45 | "ts-jest": "^29.0.3", 46 | "ts-node": "^10.9.1", 47 | "typescript": "^4.9.3" 48 | }, 49 | "dependencies": { 50 | {{#apiGW}} 51 | "moleculer-web": "^0.10.5", 52 | {{/apiGW}} 53 | {{#dbService}} 54 | "moleculer-db": "^0.8.21", 55 | "moleculer-db-adapter-mongo": "^0.4.16", 56 | {{/dbService}} 57 | {{#if_eq transporter "NATS"}} 58 | "nats": "^2.7.1", 59 | {{/if_eq}} 60 | {{#if_eq transporter "MQTT"}} 61 | "mqtt": "^4.3.7", 62 | {{/if_eq}} 63 | {{#if_eq transporter "AMQP"}} 64 | "amqplib": "^0.10.0", 65 | {{/if_eq}} 66 | {{#if_eq transporter "AMQP10"}} 67 | "rhea-promise": "^2.1.0", 68 | {{/if_eq}} 69 | {{#if_eq transporter "STAN"}} 70 | "node-nats-streaming": "^0.3.2", 71 | {{/if_eq}} 72 | {{#if_eq transporter "Kafka"}} 73 | "kafka-node": "^5.0.0", 74 | {{/if_eq}} 75 | {{#redis}} 76 | "ioredis": "^5.0.0", 77 | {{/redis}} 78 | "moleculer": "^0.14.27" 79 | }, 80 | "engines": { 81 | "node": ">= 16.x.x" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /template/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moleculerjs/moleculer-template-project-typescript/cbeb0d0b47c96d25b12e24fe046dc2bebc7205c3/template/public/favicon.ico -------------------------------------------------------------------------------- /template/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{projectName}} - Moleculer Microservices Project 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | {{{{projectName}}}} 16 |
17 |
18 | 19 | 20 | 25 |
26 | 27 |
28 |
29 |
30 |

Welcome to your Moleculer microservices project!

31 |

Check out the Moleculer documentation to learn how to customize this project.

32 | 33 | 80 |
81 |
82 |
83 |
84 | 85 | 89 | 93 |
94 |
95 | 161 |
162 |
163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 186 | 187 | 188 |
Node IDTypeVersionIPHostnameStatusCPU
{{ node.id }}{{ node.client.type }}{{ node.client.version }}{{ node.ipList[0] }}{{ node.hostname }}
{{ node.available ? "Online": "Offline" }}
183 |
184 | {{ node.cpu != null ? Number(node.cpu).toFixed(0) + '%' : '-' }} 185 |
189 |
190 |
191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 234 | 235 |
Service/Action nameRESTParametersInstancesStatus
236 |
237 |
238 | 239 | 250 |
251 | 252 | 290 |
291 |
292 | {{{{/projectName}}}} 293 | 727 | 728 | 729 | -------------------------------------------------------------------------------- /template/public/main.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-family: "Source Sans Pro", Helvetica, Arial, sans-serif; 3 | font-size: 18px; 4 | -webkit-font-smoothing: antialiased; 5 | -moz-osx-font-smoothing: grayscale; 6 | } 7 | 8 | body { 9 | padding: 0; 10 | margin: 0; 11 | color: black; 12 | text-shadow: 1px 1px 3px rgba(0,0,0,0.2); 13 | padding-bottom: 60px; /* footer */ 14 | } 15 | 16 | .cursor-pointer { 17 | cursor: pointer; 18 | user-select: none; 19 | } 20 | 21 | header, footer { 22 | text-align: center; 23 | color: white; 24 | text-shadow: 1px 1px 3px rgba(0,0,0,0.6); 25 | } 26 | 27 | header a, header a.router-link-exact-active, 28 | footer a, footer a.router-link-exact-active 29 | { 30 | color: #63dcfd; 31 | } 32 | 33 | header { 34 | background-image: linear-gradient(45deg, #e37682 0%, #5f4d93 100%); 35 | padding: 1em; 36 | box-shadow: 0 3px 10px rgba(0,0,0,0.6); 37 | margin-bottom: 1em; 38 | } 39 | 40 | footer { 41 | background-image: linear-gradient(135deg, #e37682 0%, #5f4d93 100%); 42 | padding: 0.75em; 43 | font-size: 0.8em; 44 | box-shadow: 0 -3px 10px rgba(0,0,0,0.6); 45 | position: fixed; 46 | left: 0; right: 0; bottom: 0; 47 | margin-top: 1em; 48 | } 49 | 50 | .m-r-xs{ 51 | margin-right: 0.5em; 52 | } 53 | 54 | .m-l-xs{ 55 | margin-left: 0.5em; 56 | } 57 | 58 | .m-t-xs{ 59 | margin-top: 0.5em; 60 | } 61 | 62 | .m-b-xs{ 63 | margin-bottom: 0.5em; 64 | } 65 | 66 | .m-x-xs{ 67 | margin-left: 0.5em; 68 | margin-right: 0.5em; 69 | } 70 | 71 | .m-y-xs{ 72 | margin-top: 0.5em; 73 | margin-bottom: 0.5em; 74 | } 75 | 76 | .m-t-sm{ 77 | margin-top: 1em; 78 | } 79 | 80 | .m-b-sm{ 81 | margin-bottom: 1em; 82 | } 83 | 84 | .m-x-sm{ 85 | margin-left: 1em; 86 | margin-right: 1em; 87 | } 88 | 89 | .m-y-sm{ 90 | margin-top: 1em; 91 | margin-bottom: 1em; 92 | } 93 | 94 | .m-t-md{ 95 | margin-top: 2em; 96 | } 97 | 98 | .m-b-md{ 99 | margin-bottom: 2em; 100 | } 101 | 102 | .m-x-md{ 103 | margin-left: 2em; 104 | margin-right: 2em; 105 | } 106 | 107 | .m-y-md{ 108 | margin-top: 2em; 109 | margin-bottom: 2em; 110 | } 111 | 112 | .m-t-lg{ 113 | margin-top: 3em; 114 | } 115 | 116 | .m-b-lg{ 117 | margin-bottom: 3em; 118 | } 119 | 120 | .m-x-lg{ 121 | margin-left: 3em; 122 | margin-right: 3em; 123 | } 124 | 125 | .m-y-lg{ 126 | margin-top: 3em; 127 | margin-bottom: 3em; 128 | } 129 | 130 | .m-t-xl{ 131 | margin-top: 4em; 132 | } 133 | 134 | 135 | footer .footer-links { 136 | margin-top: 0.5em; 137 | } 138 | 139 | footer .footer-links a { 140 | margin: 0 0.5em; 141 | } 142 | 143 | a, a.router-link-exact-active { 144 | color: #3CAFCE; 145 | text-decoration: none; 146 | } 147 | 148 | nav ul { 149 | list-style: none; 150 | padding: 0; 151 | margin: 0; 152 | } 153 | 154 | nav ul li { 155 | display: inline-block; 156 | padding: 0.25em 0.75em; 157 | cursor: pointer; 158 | font-weight: 300; 159 | font-size: 1.25em; 160 | border-bottom: 2px solid transparent; 161 | transition: color .1s linear, border-bottom .1s linear; 162 | } 163 | 164 | nav ul li.active { 165 | border-bottom: 2px solid #63dcfd; 166 | } 167 | 168 | nav ul li:hover { 169 | color: #63dcfd; 170 | } 171 | 172 | button, .button { 173 | background-color: #3CAFCE; 174 | border: 0; 175 | border-radius: 8px; 176 | color: white; 177 | font-family: "Source Sans Pro", Helvetica, Arial, sans-serif; 178 | font-size: 16px; 179 | font-weight: 400; 180 | padding: 0.5em 1em; 181 | box-shadow: 0 4px 6px -1px rgba(0,0,0,.2); 182 | cursor: pointer; 183 | user-select: none; 184 | min-width: 100px; 185 | display: flex; 186 | flex-direction: row; 187 | flex-wrap: nowrap; 188 | align-items: center; 189 | justify-content: center; 190 | } 191 | 192 | button i, .button i { 193 | margin-right: 0.5em; 194 | } 195 | 196 | button:hover, .button:hover { 197 | filter: brightness(120%); 198 | } 199 | 200 | .button.outlined{ 201 | background-color: transparent; 202 | border: 1px solid #3CAFCE; 203 | color: #3CAFCE; 204 | } 205 | 206 | .button.flat{ 207 | background-color: transparent; 208 | border: unset; 209 | color: #3CAFCE; 210 | box-shadow: unset; 211 | } 212 | 213 | .button.flat:hover{ 214 | box-shadow: 0 4px 6px -1px rgba(0,0,0,.2); 215 | transition: .1s ease-in-out; 216 | } 217 | 218 | 219 | .button.flat.negative{ 220 | background-color: transparent; 221 | border: unset; 222 | color: #b2184e; 223 | } 224 | .button.flat.positive{ 225 | background-color: transparent; 226 | border: unset; 227 | color: #28a728; 228 | } 229 | .button.flat.info{ 230 | background-color: transparent; 231 | border: unset; 232 | color: #285fa7; 233 | } 234 | .button.flat.warning{ 235 | background-color: transparent; 236 | border: unset; 237 | color: #b2ad18; 238 | } 239 | 240 | .button.outlined.negative{ 241 | background-color: transparent; 242 | border: 1px solid #b2184e; 243 | color: #b2184e; 244 | } 245 | .button.outlined.positive{ 246 | background-color: transparent; 247 | border: 1px solid #28a728; 248 | color: #28a728; 249 | } 250 | .button.outlined.info{ 251 | background-color: transparent; 252 | border: 1px solid #285fa7; 253 | color: #285fa7; 254 | } 255 | .button.outlined.warning{ 256 | background-color: transparent; 257 | border: 1px solid #b2ad18; 258 | color: #b2ad18; 259 | } 260 | 261 | 262 | .button.negative{ 263 | background-color: #b2184e; 264 | } 265 | .button.positive{ 266 | background-color: #28a728; 267 | } 268 | .button.info{ 269 | background-color: #285fa7; 270 | } 271 | .button.warning{ 272 | background-color: #b2ad18; 273 | } 274 | 275 | code { 276 | font-family: "Consolas", 'Courier New', Courier, monospace; 277 | color: #555; 278 | } 279 | 280 | main { 281 | max-width: 1260px; 282 | margin: 0 auto; 283 | padding: 1em 1em; 284 | } 285 | 286 | main section#home > .content { 287 | text-align: center; 288 | } 289 | 290 | main section#home h1 { 291 | font-size: 2em; 292 | font-weight: 400; 293 | margin-top: 0; 294 | } 295 | 296 | main section#home h3 { 297 | font-size: 1.25em; 298 | font-weight: 600; 299 | } 300 | 301 | 302 | 303 | 304 | pre.broker-options { 305 | display: inline-block; 306 | text-align: left; 307 | font-size: 0.9em; 308 | } 309 | 310 | .boxes { 311 | display: flex; 312 | flex-wrap: wrap; 313 | justify-content: center; 314 | } 315 | 316 | .boxes .box { 317 | width: 200px; 318 | padding: 0.25em 1em; 319 | margin: 0.5em; 320 | background: rgba(60, 175, 206, 0.1); 321 | 322 | border: 1px solid grey; 323 | border-radius: 0.25em; 324 | } 325 | 326 | .boxes .box .caption { 327 | font-weight: 300; 328 | font-size: 0.9em; 329 | margin-bottom: 0.5em; 330 | } 331 | 332 | .boxes .box .value { 333 | font-weight: 600; 334 | font-size: 1.1em; 335 | } 336 | 337 | main input { 338 | border: 1px solid #3CAFCE; 339 | border-radius: 4px; 340 | padding: 2px 6px; 341 | font-family: "Source Sans Pro"; 342 | } 343 | 344 | main fieldset { 345 | border: 1px solid lightgrey; 346 | border-radius: 8px; 347 | box-shadow: 2px 2px 10px rgba(0,0,0,0.4); 348 | background-color: rgba(240, 244, 247, 0.802); 349 | margin-bottom: 2em; 350 | } 351 | 352 | main fieldset legend { 353 | background-color: #cce7ff; 354 | border: 1px solid lightgrey; 355 | padding: 4px 10px; 356 | border-radius: 8px; 357 | } 358 | 359 | main fieldset .content { 360 | display: flex; 361 | flex-direction: column; 362 | flex:1; 363 | } 364 | 365 | main fieldset .action-card { 366 | 367 | } 368 | 369 | .action-card { 370 | display: flex; 371 | flex: 1; 372 | flex-direction: column; 373 | margin-bottom: .2em; 374 | margin-top: .2em; 375 | border: 1px solid lightgrey; 376 | border-radius: 4px; 377 | 378 | } 379 | 380 | .action-card.expand { 381 | 382 | } 383 | 384 | .action-card-header{ 385 | padding: 8px; 386 | border-bottom: 1px solid lightgrey; 387 | border-radius: 4px; 388 | display: flex; 389 | flex:1; 390 | flex-direction: row; 391 | align-items: center; 392 | transition: .25s ease-in-out all; 393 | } 394 | 395 | .action-card-header:hover{ 396 | filter: brightness(1.2); 397 | cursor: pointer; 398 | } 399 | 400 | .action-card-header.expand{ 401 | 402 | } 403 | 404 | 405 | 406 | .action-card-section{ 407 | display: none; 408 | } 409 | 410 | .action-card-section.expand{ 411 | display: block; 412 | transition: .300s ease-in-out display; 413 | } 414 | 415 | .flex-spacer{ 416 | flex-grow: 1; 417 | } 418 | 419 | 420 | .action-card-section-parameters{ 421 | 422 | 423 | } 424 | .action-card-section-parameters-header{ 425 | background-color: #fbfbfbbb; 426 | padding: 8px; 427 | display: flex;justify-items: center;align-items: center;flex-direction: row; flex: 1; 428 | 429 | } 430 | .action-card-section-parameters-body{ 431 | padding: 8px; 432 | 433 | } 434 | 435 | .action-card-section-response{ 436 | background-color: #fbfbfb92; 437 | } 438 | 439 | .action-card-section-response-header{ 440 | background-color: #fbfbfbbb; 441 | padding: 8px; 442 | display: flex;justify-items: center;align-items: center;flex-direction: row; flex: 1; 443 | } 444 | 445 | .action-card-section-response-body{ 446 | padding: 4px 16px; 447 | } 448 | 449 | main fieldset .parameters .field { 450 | margin-bottom: 0.25em; 451 | } 452 | 453 | main fieldset .parameters .field label { 454 | min-width: 80px; 455 | display: inline-block; 456 | text-align: right; 457 | margin-right: 0.5em; 458 | } 459 | 460 | main fieldset .response { 461 | margin-top: 1em; 462 | } 463 | 464 | main fieldset .response pre { 465 | margin: 0.5em 0; 466 | font-size: 0.9em; 467 | } 468 | 469 | pre.json .string { color: #885800; } 470 | pre.json .number { color: blue; } 471 | pre.json .boolean { color: magenta; } 472 | pre.json .null { color: red; } 473 | pre.json .key { color: green; } 474 | 475 | 476 | 477 | main h4 { 478 | font-weight: 600; 479 | margin: 0.25em -1.0em; 480 | } 481 | 482 | .badge { 483 | display: inline-block; 484 | background-color: dimgray; 485 | color: white; 486 | padding: 2px 6px; 487 | border-radius: 4px; 488 | font-size: 0.7em; 489 | font-weight: 600; 490 | } 491 | 492 | .badge.lg { 493 | padding: 4px 8px; 494 | } 495 | 496 | .badge.lg.fixed { 497 | width: 80px; 498 | } 499 | 500 | .badge.green { 501 | background-color: limegreen; 502 | } 503 | 504 | .badge.red { 505 | background-color: firebrick; 506 | } 507 | 508 | .badge.orange { 509 | background-color: #fab000; 510 | color: black; 511 | } 512 | 513 | .badge.light { 514 | background-color: #669aa9a6; 515 | } 516 | 517 | table { 518 | width: 100%; 519 | /*max-width: 1000px;*/ 520 | border: 1px solid lightgrey; 521 | border-radius: 8px; 522 | background-color: aliceblue; 523 | } 524 | 525 | table th { 526 | padding: 2px 4px; 527 | background-color: #cce7ff; 528 | border-radius: 4px; 529 | } 530 | 531 | table tr.offline td { 532 | font-style: italic; 533 | color: #777; 534 | } 535 | 536 | table tr.local td { 537 | /*color: blue;*/ 538 | } 539 | 540 | table tr:not(:last-child) td { 541 | border-bottom: 1px solid #ddd; 542 | } 543 | 544 | table td { 545 | text-align: center; 546 | position: relative; 547 | padding: 2px 4px; 548 | } 549 | 550 | table th:nth-child(1), table td:nth-child(1) { 551 | text-align: left 552 | } 553 | 554 | table tr.service td:nth-child(1) { 555 | font-weight: bold; 556 | } 557 | 558 | table tr.action td:nth-child(1) { 559 | padding-left: 2em; 560 | } 561 | 562 | table tr td:nth-child(2) { 563 | font-family: monospace; 564 | font-size: 0.8em; 565 | } 566 | 567 | .bar { 568 | position: absolute; 569 | left: 0; right: 0; top: 0; bottom: 0; 570 | width: 0; 571 | height: 100%; 572 | background-color: rgba(0,0,0,0.3); 573 | } 574 | 575 | input[type=text], input[type=password], input[type=number], input[type=email], input[type=url], input[type=tel], input[type=date], input[type=month], input[type=week], input[type=time], input[type=datetime], input[type=datetime-local], input[type=color], textarea, select { 576 | background-color: #f0f0f0; 577 | border: 1px solid rgba(42, 51, 150, 0.806); 578 | border-radius: 4px; 579 | padding: 2px 8px; 580 | height: 1.5em; 581 | } 582 | 583 | input[type=checkbox] { 584 | margin-right: 0.5em; 585 | height: 1.25em; 586 | width: 1.25em; 587 | } 588 | 589 | input[type=radio] { 590 | margin-right: 0.5em; 591 | height: 1.25em; 592 | width: 1.25em; 593 | } 594 | 595 | input[required]:invalid { 596 | background-color: #d0c0c0d0; 597 | border: 1px solid rgb(161, 54, 54); 598 | border-radius: 4px; 599 | padding: 4px; 600 | 601 | } 602 | 603 | input[required]:after{ 604 | content: "*"; 605 | color: red; 606 | font-size: 0.8em; 607 | position: absolute; 608 | right: 0.5em; 609 | top: 0.5em; 610 | } 611 | 612 | .bg-primary { 613 | background-color: #3CAFCE; 614 | } 615 | 616 | .bg-secondary { 617 | background-color: #999; 618 | } 619 | 620 | .bg-method-post { 621 | background-color: #1e8847; 622 | } 623 | 624 | .bg-method-get { 625 | background-color: #1f697e; 626 | } 627 | 628 | .bg-method-put { 629 | background-color: #b79f27; 630 | } 631 | 632 | .bg-method-patch { 633 | background-color: #916d18; 634 | } 635 | 636 | .bg-method-delete { 637 | background-color: #b72727; 638 | } 639 | 640 | .bg-method-options { 641 | background-color: #80449a; 642 | } 643 | 644 | .action-method-post { 645 | background-color: #1e884740; 646 | border: 1px solid #1e8847; 647 | } 648 | 649 | .action-method-get { 650 | background-color: #1f697e44; 651 | border: 1px solid #1f697e; 652 | } 653 | 654 | .action-method-put { 655 | background-color: #b79f2740; 656 | border: 1px solid #b79f27; 657 | } 658 | 659 | .action-method-patch { 660 | background-color: #916d183e; 661 | border: 1px solid #916d18; 662 | } 663 | 664 | .action-method-delete { 665 | background-color: #b727273d; 666 | border: 1px solid #b72727; 667 | } 668 | 669 | .action-method-options { 670 | background-color: #80449a61; 671 | border: 1px solid #80449a; 672 | } 673 | 674 | 675 | .text-title { 676 | font-size: 1.25em; 677 | font-weight: 400; 678 | } 679 | .text-subtitle1 { 680 | font-size: 1.25em; 681 | font-weight: 200; 682 | } 683 | 684 | .text-subtitle2 { 685 | font-size: 1.15em; 686 | font-weight: 200; 687 | } 688 | 689 | .text-h1 { 690 | font-size: 2em; 691 | font-weight: 400; 692 | } 693 | 694 | .text-h2 { 695 | font-size: 1.5em; 696 | font-weight: 400; 697 | } 698 | 699 | .text-h3 { 700 | font-size: 1.25em; 701 | font-weight: 300; 702 | } 703 | 704 | .text-h4 { 705 | font-size: 1.15em; 706 | font-weight: 300; 707 | } 708 | 709 | .text-h5 { 710 | font-size: 1em; 711 | font-weight: 200; 712 | } 713 | 714 | .text-h6 { 715 | font-size: 0.85em; 716 | font-weight: 200; 717 | } 718 | 719 | .text-caption { 720 | font-size: 0.85em; 721 | font-weight: 200; 722 | } 723 | 724 | .text-code { 725 | font-size: 1em; 726 | font-weight: 300; 727 | } 728 | 729 | .text-bold { 730 | font-weight: bold; 731 | } 732 | 733 | .text-p { 734 | font-size: 1em; 735 | font-weight: 400; 736 | } 737 | 738 | .text-small { 739 | font-size: 0.85em; 740 | font-weight: 400; 741 | } 742 | 743 | .text-muted { 744 | font-size: 0.85em; 745 | font-weight: 400; 746 | color: #999; 747 | } 748 | 749 | .text-primary { 750 | color: #3CAFCE; 751 | } 752 | 753 | .text-secondary { 754 | color: #999; 755 | } 756 | 757 | 758 | .text-center { 759 | text-align: center; 760 | } 761 | 762 | .text-right { 763 | text-align: right; 764 | } 765 | 766 | .text-left { 767 | text-align: left; 768 | } 769 | 770 | .text-justify { 771 | text-align: justify; 772 | } 773 | 774 | .text-nowrap { 775 | white-space: nowrap; 776 | } 777 | 778 | .text-truncate { 779 | overflow: hidden; 780 | text-overflow: ellipsis; 781 | white-space: nowrap; 782 | } 783 | 784 | .text-break { 785 | word-break: break-all; 786 | } 787 | 788 | .text-lowercase { 789 | text-transform: lowercase; 790 | } 791 | 792 | .text-uppercase { 793 | text-transform: uppercase; 794 | } 795 | 796 | .text-capitalize { 797 | text-transform: capitalize; 798 | } 799 | 800 | .text-wrap { 801 | word-wrap: break-word; 802 | } 803 | 804 | .text-nowrap { 805 | white-space: nowrap; 806 | } 807 | 808 | .full-width{ 809 | width: 100%; 810 | } 811 | .flex,.row,.column{ 812 | display: flex; 813 | } 814 | .column{ 815 | flex-direction: column; 816 | } 817 | .row{ 818 | flex-direction: row; 819 | } 820 | 821 | .self-start{ 822 | align-self: flex-start; 823 | } 824 | .self-center{ 825 | align-self: center; 826 | } 827 | .self-end{ 828 | align-self: flex-end; 829 | } 830 | 831 | .justify-start{ 832 | justify-content: flex-start; 833 | } 834 | .justify-center{ 835 | justify-content: center; 836 | } 837 | .justify-end{ 838 | justify-content: flex-end; 839 | } 840 | .justify-between{ 841 | justify-content: space-between; 842 | } 843 | .justify-around{ 844 | justify-content: space-around; 845 | } 846 | 847 | .items-start{ 848 | align-items: flex-start; 849 | } 850 | .items-center{ 851 | align-items: center; 852 | } 853 | .items-end{ 854 | align-items: flex-end; 855 | } 856 | .items-baseline{ 857 | align-items: baseline; 858 | } 859 | .items-stretch{ 860 | align-items: stretch; 861 | } 862 | 863 | .flex-grow{ 864 | flex-grow: 1; 865 | } 866 | 867 | .flex-wrap{ 868 | flex-wrap: wrap; 869 | } 870 | .nowrap{ 871 | flex-wrap: nowrap; 872 | } 873 | 874 | .modal-overlay { 875 | position: fixed; 876 | top: 0; 877 | left: 0; 878 | width: 100%; 879 | height: 100%; 880 | background-color: rgba(0, 0, 0, 0.5); 881 | z-index: 999; 882 | } 883 | 884 | .modal{ 885 | position: fixed; 886 | top: 50%; 887 | left: 50%; 888 | transform: translate(-50%, -50%); 889 | width: 80%; 890 | max-width: 500px; 891 | background-color: #fff; 892 | border-radius: 4px; 893 | box-shadow: 0 0 8px rgba(0, 0, 0, 0.5); 894 | z-index: 1000; 895 | 896 | } 897 | 898 | .modal .modal-header{ 899 | border-bottom: 1px solid #e5e5e5; 900 | padding: 8px; 901 | } 902 | .modal .modal-content{ 903 | height: 100%; 904 | padding: 16px; 905 | 906 | } 907 | .modal .modal-actions{ 908 | border-top: 1px solid #e5e5e5; 909 | display: flex; 910 | justify-content: flex-end; 911 | padding: 16px; 912 | flex-direction: row; 913 | flex-wrap: nowrap; 914 | 915 | } 916 | .form-group { 917 | 918 | } 919 | .form-group > * { 920 | margin-right: 4px; 921 | margin-bottom: 4px; 922 | } 923 | 924 | input[type=text].input-size-md{ 925 | height: 1.5em; 926 | font-size: 1.3em; 927 | } 928 | 929 | .field>label { 930 | width: 120px; 931 | } -------------------------------------------------------------------------------- /template/services/api.service.ts: -------------------------------------------------------------------------------- 1 | import type { Context, ServiceSchema } from "moleculer"; 2 | import type { ApiSettingsSchema, GatewayResponse, IncomingRequest, Route } from "moleculer-web"; 3 | import ApiGateway from "moleculer-web"; 4 | 5 | interface Meta { 6 | userAgent?: string | null | undefined; 7 | user?: object | null | undefined; 8 | } 9 | 10 | const ApiService: ServiceSchema = { 11 | name: "api", 12 | mixins: [ApiGateway], 13 | 14 | // More info about settings: https://moleculer.services/docs/0.14/moleculer-web.html 15 | settings: { 16 | // Exposed port 17 | port: process.env.PORT != null ? Number(process.env.PORT) : 3000, 18 | 19 | // Exposed IP 20 | ip: "0.0.0.0", 21 | 22 | // Global Express middlewares. More info: https://moleculer.services/docs/0.14/moleculer-web.html#Middlewares 23 | use: [], 24 | 25 | routes: [ 26 | { 27 | path: "/api", 28 | 29 | whitelist: ["**"], 30 | 31 | // Route-level Express middlewares. More info: https://moleculer.services/docs/0.14/moleculer-web.html#Middlewares 32 | use: [], 33 | 34 | // Enable/disable parameter merging method. More info: https://moleculer.services/docs/0.14/moleculer-web.html#Disable-merging 35 | mergeParams: true, 36 | 37 | // Enable authentication. Implement the logic into `authenticate` method. More info: https://moleculer.services/docs/0.14/moleculer-web.html#Authentication 38 | authentication: false, 39 | 40 | // Enable authorization. Implement the logic into `authorize` method. More info: https://moleculer.services/docs/0.14/moleculer-web.html#Authorization 41 | authorization: false, 42 | 43 | // The auto-alias feature allows you to declare your route alias directly in your services. 44 | // The gateway will dynamically build the full routes from service schema. 45 | autoAliases: true, 46 | 47 | aliases: {}, 48 | 49 | /** 50 | * Before call hook. You can check the request. 51 | * 52 | onBeforeCall( 53 | ctx: Context, 54 | route: Route, 55 | req: IncomingRequest, 56 | res: GatewayResponse, 57 | ): void { 58 | // Set request headers to context meta 59 | ctx.meta.userAgent = req.headers["user-agent"]; 60 | }, */ 61 | 62 | /** 63 | * After call hook. You can modify the data. 64 | * 65 | onAfterCall( 66 | ctx: Context, 67 | route: Route, 68 | req: IncomingRequest, 69 | res: GatewayResponse, 70 | data: unknown, 71 | ): unknown { 72 | // Async function which return with Promise 73 | // return this.doSomething(ctx, res, data); 74 | return data; 75 | }, */ 76 | 77 | // Calling options. More info: https://moleculer.services/docs/0.14/moleculer-web.html#Calling-options 78 | callOptions: {}, 79 | 80 | bodyParsers: { 81 | json: { 82 | strict: false, 83 | limit: "1MB", 84 | }, 85 | urlencoded: { 86 | extended: true, 87 | limit: "1MB", 88 | }, 89 | }, 90 | 91 | // Mapping policy setting. More info: https://moleculer.services/docs/0.14/moleculer-web.html#Mapping-policy 92 | mappingPolicy: "all", // Available values: "all", "restrict" 93 | 94 | // Enable/disable logging 95 | logging: true, 96 | }, 97 | ], 98 | 99 | // Do not log client side errors (does not log an error response when the error.code is 400<=X<500) 100 | log4XXResponses: false, 101 | // Logging the request parameters. Set to any log level to enable it. E.g. "info" 102 | logRequestParams: null, 103 | // Logging the response data. Set to any log level to enable it. E.g. "info" 104 | logResponseData: null, 105 | 106 | // Serve assets from "public" folder. More info: https://moleculer.services/docs/0.14/moleculer-web.html#Serve-static-files 107 | assets: { 108 | folder: "public", 109 | 110 | // Options to `server-static` module 111 | options: {}, 112 | }, 113 | }, 114 | 115 | methods: { 116 | /** 117 | * Authenticate the request. It check the `Authorization` token value in the request header. 118 | * Check the token value & resolve the user by the token. 119 | * The resolved user will be available in `ctx.meta.user` 120 | * 121 | * PLEASE NOTE, IT'S JUST AN EXAMPLE IMPLEMENTATION. DO NOT USE IN PRODUCTION! 122 | */ 123 | authenticate( 124 | ctx: Context, 125 | route: Route, 126 | req: IncomingRequest, 127 | ): Record | null { 128 | // Read the token from header 129 | const auth = req.headers.authorization; 130 | 131 | if (auth && auth.startsWith("Bearer")) { 132 | const token = auth.slice(7); 133 | 134 | // Check the token. Tip: call a service which verify the token. E.g. `accounts.resolveToken` 135 | if (token === "123456") { 136 | // Returns the resolved user. It will be set to the `ctx.meta.user` 137 | return { id: 1, name: "John Doe" }; 138 | } 139 | // Invalid token 140 | throw new ApiGateway.Errors.UnAuthorizedError( 141 | ApiGateway.Errors.ERR_INVALID_TOKEN, 142 | null, 143 | ); 144 | } else { 145 | // No token. Throw an error or do nothing if anonymous access is allowed. 146 | // throw new E.UnAuthorizedError(E.ERR_NO_TOKEN); 147 | return null; 148 | } 149 | }, 150 | 151 | /** 152 | * Authorize the request. Check that the authenticated user has right to access the resource. 153 | * 154 | * PLEASE NOTE, IT'S JUST AN EXAMPLE IMPLEMENTATION. DO NOT USE IN PRODUCTION! 155 | */ 156 | authorize(ctx: Context, route: Route, req: IncomingRequest) { 157 | // Get the authenticated user. 158 | const { user } = ctx.meta; 159 | 160 | // It check the `auth` property in action schema. 161 | if (req.$action.auth === "required" && !user) { 162 | throw new ApiGateway.Errors.UnAuthorizedError("NO_RIGHTS", null); 163 | } 164 | }, 165 | }, 166 | }; 167 | 168 | export default ApiService; 169 | -------------------------------------------------------------------------------- /template/services/greeter.service.ts: -------------------------------------------------------------------------------- 1 | import type { Context, Service, ServiceSchema, ServiceSettingSchema } from "moleculer"; 2 | 3 | export interface ActionHelloParams { 4 | name: string; 5 | } 6 | 7 | interface GreeterSettings extends ServiceSettingSchema { 8 | defaultName: string; 9 | } 10 | 11 | interface GreeterMethods { 12 | uppercase(str: string): string; 13 | } 14 | 15 | interface GreeterLocalVars { 16 | myVar: string; 17 | } 18 | 19 | type GreeterThis = Service & GreeterMethods & GreeterLocalVars; 20 | 21 | const GreeterService: ServiceSchema = { 22 | name: "greeter", 23 | 24 | /** 25 | * Settings 26 | */ 27 | settings: { 28 | defaultName: "Moleculer", 29 | }, 30 | 31 | /** 32 | * Dependencies 33 | */ 34 | dependencies: [], 35 | 36 | /** 37 | * Actions 38 | */ 39 | actions: { 40 | hello: { 41 | rest: { 42 | method: "GET", 43 | path: "/hello", 44 | }, 45 | handler(this: GreeterThis/* , ctx: Context */): string { 46 | return `Hello ${this.settings.defaultName}`; 47 | }, 48 | }, 49 | 50 | welcome: { 51 | rest: "GET /welcome/:name", 52 | params: { 53 | name: "string", 54 | }, 55 | handler(this: GreeterThis, ctx: Context): string { 56 | return `Welcome, ${ctx.params.name}`; 57 | }, 58 | }, 59 | }, 60 | 61 | /** 62 | * Events 63 | */ 64 | events: {}, 65 | 66 | /** 67 | * Methods 68 | */ 69 | methods: {}, 70 | 71 | /** 72 | * Service created lifecycle event handler 73 | */ 74 | created() {}, 75 | 76 | /** 77 | * Service started lifecycle event handler 78 | */ 79 | async started() {}, 80 | 81 | /** 82 | * Service stopped lifecycle event handler 83 | */ 84 | async stopped() {}, 85 | }; 86 | 87 | export default GreeterService; 88 | -------------------------------------------------------------------------------- /template/services/products.service.ts: -------------------------------------------------------------------------------- 1 | import type { Context, Service, ServiceSchema } from "moleculer"; 2 | import type { DbAdapter, DbServiceSettings, MoleculerDbMethods } from "moleculer-db"; 3 | import type MongoDbAdapter from "moleculer-db-adapter-mongo"; 4 | import type { DbServiceMethods } from "../mixins/db.mixin"; 5 | import DbMixin from "../mixins/db.mixin"; 6 | 7 | export interface ProductEntity { 8 | _id: string; 9 | name: string; 10 | price: number; 11 | quantity: number; 12 | } 13 | 14 | export type ActionCreateParams = Partial; 15 | 16 | export interface ActionQuantityParams { 17 | id: string; 18 | value: number; 19 | } 20 | 21 | interface ProductSettings extends DbServiceSettings { 22 | indexes?: Record[]; 23 | } 24 | 25 | interface ProductsThis extends Service, MoleculerDbMethods { 26 | adapter: DbAdapter | MongoDbAdapter; 27 | } 28 | 29 | const ProductsService: ServiceSchema & { methods: DbServiceMethods } = { 30 | name: "products", 31 | // version: 1 32 | 33 | /** 34 | * Mixins 35 | */ 36 | mixins: [DbMixin("products")], 37 | 38 | /** 39 | * Settings 40 | */ 41 | settings: { 42 | // Available fields in the responses 43 | fields: ["_id", "name", "quantity", "price"], 44 | 45 | // Validator for the `create` & `insert` actions. 46 | entityValidator: { 47 | name: "string|min:3", 48 | price: "number|positive", 49 | }, 50 | 51 | indexes: [{ name: 1 }], 52 | }, 53 | 54 | /** 55 | * Action Hooks 56 | */ 57 | hooks: { 58 | before: { 59 | /** 60 | * Register a before hook for the `create` action. 61 | * It sets a default value for the quantity field. 62 | */ 63 | create(ctx: Context) { 64 | ctx.params.quantity = 0; 65 | }, 66 | }, 67 | }, 68 | 69 | /** 70 | * Actions 71 | */ 72 | actions: { 73 | /** 74 | * The "moleculer-db" mixin registers the following actions: 75 | * - list 76 | * - find 77 | * - count 78 | * - create 79 | * - insert 80 | * - update 81 | * - remove 82 | */ 83 | 84 | // --- ADDITIONAL ACTIONS --- 85 | 86 | /** 87 | * Increase the quantity of the product item. 88 | */ 89 | increaseQuantity: { 90 | rest: "PUT /:id/quantity/increase", 91 | params: { 92 | id: "string", 93 | value: "number|integer|positive", 94 | }, 95 | async handler(this: ProductsThis, ctx: Context): Promise { 96 | const doc = await this.adapter.updateById(ctx.params.id, { 97 | $inc: { quantity: ctx.params.value }, 98 | }); 99 | const json = await this.transformDocuments(ctx, ctx.params, doc); 100 | await this.entityChanged("updated", json, ctx); 101 | 102 | return json; 103 | }, 104 | }, 105 | 106 | /** 107 | * Decrease the quantity of the product item. 108 | */ 109 | decreaseQuantity: { 110 | rest: "PUT /:id/quantity/decrease", 111 | params: { 112 | id: "string", 113 | value: "number|integer|positive", 114 | }, 115 | async handler(this: ProductsThis, ctx: Context): Promise { 116 | const doc = await this.adapter.updateById(ctx.params.id, { 117 | $inc: { quantity: -ctx.params.value }, 118 | }); 119 | const json = await this.transformDocuments(ctx, ctx.params, doc); 120 | await this.entityChanged("updated", json, ctx); 121 | 122 | return json; 123 | }, 124 | }, 125 | }, 126 | 127 | /** 128 | * Methods 129 | */ 130 | methods: { 131 | /** 132 | * Loading sample data to the collection. 133 | * It is called in the DB.mixin after the database 134 | * connection establishing & the collection is empty. 135 | */ 136 | async seedDB(this: ProductsThis) { 137 | await this.adapter.insertMany([ 138 | { name: "Samsung Galaxy S10 Plus", quantity: 10, price: 704 }, 139 | { name: "iPhone 11 Pro", quantity: 25, price: 999 }, 140 | { name: "Huawei P30 Pro", quantity: 15, price: 679 }, 141 | ]); 142 | }, 143 | }, 144 | 145 | /** 146 | * Fired after database connection establishing. 147 | */ 148 | async afterConnected(this: ProductsThis) { 149 | if ("collection" in this.adapter) { 150 | if (this.settings.indexes) { 151 | await Promise.all( 152 | this.settings.indexes.map((index) => 153 | (this.adapter).collection.createIndex(index), 154 | ), 155 | ); 156 | } 157 | } 158 | }, 159 | }; 160 | 161 | export default ProductsService; 162 | -------------------------------------------------------------------------------- /template/test/integration/products.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { afterAll, beforeAll, describe, expect, test } from "@jest/globals"; 2 | import { ServiceBroker } from "moleculer"; 3 | import type { ServiceSchema } from "moleculer"; 4 | import type { ProductEntity } from "../../services/products.service"; 5 | import TestService from "../../services/products.service"; 6 | 7 | describe("Test 'products' service", () => { 8 | describe("Test actions", () => { 9 | const broker = new ServiceBroker({ logger: false }); 10 | const service = broker.createService(TestService as unknown as ServiceSchema); 11 | service.seedDB = null; // Disable seeding 12 | 13 | beforeAll(() => broker.start()); 14 | afterAll(() => broker.stop()); 15 | 16 | const record = { 17 | name: "Awesome item", 18 | price: 999, 19 | }; 20 | let newID: string; 21 | 22 | test("should contains the seeded items", async () => { 23 | const res = await broker.call("products.list"); 24 | expect(res).toEqual({ page: 1, pageSize: 10, rows: [], total: 0, totalPages: 0 }); 25 | }); 26 | 27 | test("should add the new item", async () => { 28 | const res: ProductEntity = await broker.call("products.create", record); 29 | expect(res).toEqual({ 30 | _id: expect.any(String), 31 | name: "Awesome item", 32 | price: 999, 33 | quantity: 0, 34 | }); 35 | newID = res._id; 36 | 37 | const res2 = await broker.call("products.count"); 38 | expect(res2).toBe(1); 39 | }); 40 | 41 | test("should get the saved item", async () => { 42 | const res: ProductEntity = await broker.call("products.get", { id: newID }); 43 | expect(res).toEqual({ 44 | _id: expect.any(String), 45 | name: "Awesome item", 46 | price: 999, 47 | quantity: 0, 48 | }); 49 | 50 | const res2 = await broker.call("products.list"); 51 | expect(res2).toEqual({ 52 | page: 1, 53 | pageSize: 10, 54 | rows: [{ _id: newID, name: "Awesome item", price: 999, quantity: 0 }], 55 | total: 1, 56 | totalPages: 1, 57 | }); 58 | }); 59 | 60 | test("should update an item", async () => { 61 | const res = await broker.call("products.update", { id: newID, price: 499 }); 62 | expect(res).toEqual({ 63 | _id: expect.any(String), 64 | name: "Awesome item", 65 | price: 499, 66 | quantity: 0, 67 | }); 68 | }); 69 | 70 | test("should get the updated item", async () => { 71 | const res = await broker.call("products.get", { id: newID }); 72 | expect(res).toEqual({ 73 | _id: expect.any(String), 74 | name: "Awesome item", 75 | price: 499, 76 | quantity: 0, 77 | }); 78 | }); 79 | 80 | test("should increase the quantity", async () => { 81 | const res = await broker.call("products.increaseQuantity", { id: newID, value: 5 }); 82 | expect(res).toEqual({ 83 | _id: expect.any(String), 84 | name: "Awesome item", 85 | price: 499, 86 | quantity: 5, 87 | }); 88 | }); 89 | 90 | test("should decrease the quantity", async () => { 91 | const res = await broker.call("products.decreaseQuantity", { id: newID, value: 2 }); 92 | expect(res).toEqual({ 93 | _id: expect.any(String), 94 | name: "Awesome item", 95 | price: 499, 96 | quantity: 3, 97 | }); 98 | }); 99 | 100 | test("should remove the updated item", async () => { 101 | const res = await broker.call("products.remove", { id: newID }); 102 | expect(res).toBe(1); 103 | 104 | const res2 = await broker.call("products.count"); 105 | expect(res2).toBe(0); 106 | 107 | const res3 = await broker.call("products.list"); 108 | expect(res3).toEqual({ page: 1, pageSize: 10, rows: [], total: 0, totalPages: 0 }); 109 | }); 110 | }); 111 | }); 112 | -------------------------------------------------------------------------------- /template/test/unit/mixins/db.mixin.spec.ts: -------------------------------------------------------------------------------- 1 | import { Context, ServiceBroker } from "moleculer"; 2 | import type { Service, ServiceAsyncLifecycleHandler, ServiceEventHandler } from "moleculer"; 3 | import DbService from "moleculer-db"; 4 | import DbMixin from "../../../mixins/db.mixin"; 5 | 6 | describe("Test DB mixin", () => { 7 | describe("Test schema generator", () => { 8 | const broker = new ServiceBroker({ logger: false, cacher: "Memory" }); 9 | 10 | beforeAll(() => broker.start()); 11 | afterAll(() => broker.stop()); 12 | 13 | test("check schema properties", () => { 14 | const schema = DbMixin("my-collection"); 15 | 16 | expect(schema.mixins).toEqual([DbService]); 17 | expect(schema.adapter).toBeInstanceOf(DbService.MemoryAdapter); 18 | expect(schema.started).toBeDefined(); 19 | expect(schema.events!["cache.clean.my-collection"]).toBeInstanceOf(Function); 20 | }); 21 | 22 | test("check cache event handler", async () => { 23 | jest.spyOn(broker.cacher!, "clean"); 24 | 25 | const schema = DbMixin("my-collection"); 26 | 27 | await (schema.events!["cache.clean.my-collection"] as ServiceEventHandler).call( 28 | { 29 | broker, 30 | fullName: "my-service", 31 | }, 32 | Context.create(broker), 33 | ); 34 | 35 | expect(broker.cacher!.clean).toHaveBeenCalledTimes(1); 36 | expect(broker.cacher!.clean).toHaveBeenCalledWith("my-service.*"); 37 | }); 38 | 39 | describe("Check service started handler", () => { 40 | test("should not call seedDB method", async () => { 41 | const schema = DbMixin("my-collection"); 42 | 43 | schema.adapter!.count = jest.fn(() => Promise.resolve(10)); 44 | const seedDBFn = jest.fn(); 45 | 46 | await (schema.started as ServiceAsyncLifecycleHandler).call({ 47 | broker, 48 | logger: broker.logger, 49 | adapter: schema.adapter, 50 | seedDB: seedDBFn, 51 | } as unknown as Service); 52 | 53 | expect(schema.adapter!.count).toHaveBeenCalledTimes(1); 54 | expect(schema.adapter!.count).toHaveBeenCalledWith(); 55 | 56 | expect(seedDBFn).toHaveBeenCalledTimes(0); 57 | }); 58 | 59 | test("should call seedDB method", async () => { 60 | const schema = DbMixin("my-collection"); 61 | 62 | schema.adapter!.count = jest.fn(() => Promise.resolve(0)); 63 | const seedDBFn = jest.fn(); 64 | 65 | await (schema.started as ServiceAsyncLifecycleHandler).call({ 66 | broker, 67 | logger: broker.logger, 68 | adapter: schema.adapter, 69 | seedDB: seedDBFn, 70 | } as unknown as Service); 71 | 72 | expect(schema.adapter!.count).toHaveBeenCalledTimes(2); 73 | expect(schema.adapter!.count).toHaveBeenCalledWith(); 74 | 75 | expect(seedDBFn).toHaveBeenCalledTimes(1); 76 | expect(seedDBFn).toHaveBeenCalledWith(); 77 | }); 78 | }); 79 | 80 | test("should broadcast a cache clear event", async () => { 81 | const schema = DbMixin("my-collection"); 82 | 83 | const ctx = Context.create(broker); 84 | 85 | jest.spyOn(ctx, "broadcast"); 86 | 87 | await schema.methods!.entityChanged!("update", null, ctx); 88 | 89 | expect(ctx.broadcast).toHaveBeenCalledTimes(1); 90 | expect(ctx.broadcast).toHaveBeenCalledWith("cache.clean.my-collection"); 91 | }); 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /template/test/unit/services/greeter.spec.ts: -------------------------------------------------------------------------------- 1 | import { Errors, ServiceBroker } from "moleculer"; 2 | import type { ServiceSchema } from "moleculer"; 3 | import TestService from "../../../services/greeter.service"; 4 | 5 | describe("Test 'greeter' service", () => { 6 | const broker = new ServiceBroker({ logger: false }); 7 | broker.createService(TestService as unknown as ServiceSchema); 8 | 9 | beforeAll(() => broker.start()); 10 | afterAll(() => broker.stop()); 11 | 12 | describe("Test 'greeter.hello' action", () => { 13 | test("should return with 'Hello Moleculer'", async () => { 14 | const res = await broker.call("greeter.hello"); 15 | expect(res).toBe("Hello Moleculer"); 16 | }); 17 | }); 18 | 19 | describe("Test 'greeter.welcome' action", () => { 20 | test("should return with 'Welcome'", async () => { 21 | const res = await broker.call("greeter.welcome", { name: "Adam" }); 22 | expect(res).toBe("Welcome, Adam"); 23 | }); 24 | 25 | test("should reject an ValidationError", async () => { 26 | await expect(broker.call("greeter.welcome")).rejects.toThrow(Errors.ValidationError); 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /template/test/unit/services/products.spec.ts: -------------------------------------------------------------------------------- 1 | import { Context, Errors, ServiceBroker } from "moleculer"; 2 | import type { ServiceSchema } from "moleculer"; 3 | import TestService from "../../../services/products.service"; 4 | 5 | describe("Test 'products' service", () => { 6 | describe("Test actions", () => { 7 | const broker = new ServiceBroker({ logger: false }); 8 | const service = broker.createService(TestService as unknown as ServiceSchema); 9 | 10 | jest.spyOn(service.adapter, "updateById"); 11 | jest.spyOn(service, "transformDocuments"); 12 | jest.spyOn(service, "entityChanged"); 13 | 14 | beforeAll(() => broker.start()); 15 | afterAll(() => broker.stop()); 16 | 17 | const record = { 18 | _id: "123", 19 | name: "Awesome thing", 20 | price: 999, 21 | quantity: 25, 22 | createdAt: Date.now(), 23 | }; 24 | 25 | describe("Test 'products.increaseQuantity'", () => { 26 | test("should call the adapter updateById method & transform result", async () => { 27 | service.adapter.updateById.mockImplementation(() => Promise.resolve(record)); 28 | service.transformDocuments.mockClear(); 29 | service.entityChanged.mockClear(); 30 | 31 | const res = await broker.call("products.increaseQuantity", { 32 | id: "123", 33 | value: 10, 34 | }); 35 | expect(res).toEqual({ 36 | _id: "123", 37 | name: "Awesome thing", 38 | price: 999, 39 | quantity: 25, 40 | }); 41 | 42 | expect(service.adapter.updateById).toHaveBeenCalledTimes(1); 43 | expect(service.adapter.updateById).toHaveBeenCalledWith("123", { 44 | $inc: { quantity: 10 }, 45 | }); 46 | 47 | expect(service.transformDocuments).toHaveBeenCalledTimes(1); 48 | expect(service.transformDocuments).toHaveBeenCalledWith( 49 | expect.any(Context), 50 | { id: "123", value: 10 }, 51 | record, 52 | ); 53 | 54 | expect(service.entityChanged).toHaveBeenCalledTimes(1); 55 | expect(service.entityChanged).toHaveBeenCalledWith( 56 | "updated", 57 | { _id: "123", name: "Awesome thing", price: 999, quantity: 25 }, 58 | expect.any(Context), 59 | ); 60 | }); 61 | }); 62 | 63 | describe("Test 'products.decreaseQuantity'", () => { 64 | test("should call the adapter updateById method & transform result", async () => { 65 | service.adapter.updateById.mockClear(); 66 | service.transformDocuments.mockClear(); 67 | service.entityChanged.mockClear(); 68 | 69 | const res = await broker.call("products.decreaseQuantity", { 70 | id: "123", 71 | value: 10, 72 | }); 73 | expect(res).toEqual({ 74 | _id: "123", 75 | name: "Awesome thing", 76 | price: 999, 77 | quantity: 25, 78 | }); 79 | 80 | expect(service.adapter.updateById).toHaveBeenCalledTimes(1); 81 | expect(service.adapter.updateById).toHaveBeenCalledWith("123", { 82 | $inc: { quantity: -10 }, 83 | }); 84 | 85 | expect(service.transformDocuments).toHaveBeenCalledTimes(1); 86 | expect(service.transformDocuments).toHaveBeenCalledWith( 87 | expect.any(Context), 88 | { id: "123", value: 10 }, 89 | record, 90 | ); 91 | 92 | expect(service.entityChanged).toHaveBeenCalledTimes(1); 93 | expect(service.entityChanged).toHaveBeenCalledWith( 94 | "updated", 95 | { _id: "123", name: "Awesome thing", price: 999, quantity: 25 }, 96 | expect.any(Context), 97 | ); 98 | }); 99 | 100 | test("should throw error if params is not valid", async () => { 101 | service.adapter.updateById.mockClear(); 102 | service.transformDocuments.mockClear(); 103 | service.entityChanged.mockClear(); 104 | 105 | expect.assertions(2); 106 | try { 107 | await broker.call("products.decreaseQuantity", { 108 | id: "123", 109 | value: -5, 110 | }); 111 | } catch (err) { 112 | expect(err).toBeInstanceOf(Errors.ValidationError); 113 | expect(err.data).toEqual([ 114 | { 115 | action: "products.decreaseQuantity", 116 | actual: -5, 117 | field: "value", 118 | message: "The 'value' field must be a positive number.", 119 | nodeID: broker.nodeID, 120 | type: "numberPositive", 121 | }, 122 | ]); 123 | } 124 | }); 125 | }); 126 | }); 127 | 128 | describe("Test methods", () => { 129 | const broker = new ServiceBroker({ logger: false }); 130 | const service = broker.createService(TestService as unknown as ServiceSchema); 131 | 132 | jest.spyOn(service.adapter, "insertMany"); 133 | jest.spyOn(service, "seedDB"); 134 | 135 | beforeAll(() => broker.start()); 136 | afterAll(() => broker.stop()); 137 | 138 | describe("Test 'seedDB'", () => { 139 | test("should be called after service started & DB connected", () => { 140 | expect(service.seedDB).toHaveBeenCalledTimes(1); 141 | expect(service.seedDB).toHaveBeenCalledWith(); 142 | }); 143 | 144 | test("should insert 3 documents", () => { 145 | expect(service.adapter.insertMany).toHaveBeenCalledTimes(1); 146 | expect(service.adapter.insertMany).toHaveBeenCalledWith([ 147 | { name: "Samsung Galaxy S10 Plus", quantity: 10, price: 704 }, 148 | { name: "iPhone 11 Pro", quantity: 25, price: 999 }, 149 | { name: "Huawei P30 Pro", quantity: 15, price: 679 }, 150 | ]); 151 | }); 152 | }); 153 | }); 154 | 155 | describe("Test hooks", () => { 156 | const broker = new ServiceBroker({ logger: false }); 157 | const createActionFn = jest.fn(); 158 | broker.createService({ 159 | name: "products", 160 | mixins: [TestService as unknown as ServiceSchema], 161 | actions: { 162 | create: { 163 | handler: createActionFn, 164 | }, 165 | }, 166 | }); 167 | 168 | beforeAll(() => broker.start()); 169 | afterAll(() => broker.stop()); 170 | 171 | describe("Test before 'create' hook", () => { 172 | test("should add quantity with zero", async () => { 173 | await broker.call("products.create", { 174 | id: "111", 175 | name: "Test product", 176 | price: 100, 177 | }); 178 | 179 | expect(createActionFn).toHaveBeenCalledTimes(1); 180 | expect(createActionFn.mock.calls[0][0].params).toEqual({ 181 | id: "111", 182 | name: "Test product", 183 | price: 100, 184 | quantity: 0, 185 | }); 186 | }); 187 | }); 188 | }); 189 | }); 190 | -------------------------------------------------------------------------------- /template/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["**/*.spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /template/tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true 5 | }, 6 | "include": [ 7 | "./.*.cjs", // root commonjs files 8 | "./.*.js", // root javascript config files 9 | "**/*.js", // javascript files 10 | "**/*.ts" // typescript files 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /template/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2021", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "commonjs", /* Specify what module code is generated. */ 29 | // "rootDir": "./", /* Specify the root folder within your source files. */ 30 | "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 38 | // "resolveJsonModule": true, /* Enable importing .json files. */ 39 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 40 | 41 | /* JavaScript Support */ 42 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 43 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 44 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 45 | 46 | /* Emit */ 47 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 48 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 49 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 50 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 51 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 52 | "outDir": "./dist", /* Specify an output folder for all emitted files. */ 53 | // "removeComments": true, /* Disable emitting comments. */ 54 | // "noEmit": true, /* Disable emitting files from a compilation. */ 55 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 56 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 57 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 58 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 59 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 60 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 61 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 62 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 63 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 64 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 65 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 66 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 67 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 68 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 69 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 70 | 71 | /* Interop Constraints */ 72 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 73 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 74 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 75 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 76 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 77 | 78 | /* Type Checking */ 79 | "strict": true, /* Enable all strict type-checking options. */ 80 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 81 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 82 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 83 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 84 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 85 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 86 | "useUnknownInCatchVariables": false, /* Default catch clause variables as 'unknown' instead of 'any'. */ 87 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 88 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 89 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 90 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 91 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 92 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 93 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 94 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 95 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 96 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 97 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 98 | 99 | /* Completeness */ 100 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 101 | "skipLibCheck": true, /* Skip type checking all .d.ts files. */ 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /test/ci/answers.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiGW": true, 3 | "needTransporter": true, 4 | "transporter": "NATS", 5 | "needCacher": true, 6 | "cacher": "Redis", 7 | "dbService": true, 8 | "metrics": false, 9 | "reporter": "Prometheus", 10 | "tracing": false, 11 | "exporter": "Console", 12 | "docker": true, 13 | "lint": true 14 | } 15 | -------------------------------------------------------------------------------- /test/ci/kind-config.yaml: -------------------------------------------------------------------------------- 1 | kind: Cluster 2 | apiVersion: kind.x-k8s.io/v1alpha4 3 | nodes: 4 | - role: control-plane 5 | kubeadmConfigPatches: 6 | - | 7 | kind: InitConfiguration 8 | nodeRegistration: 9 | kubeletExtraArgs: 10 | node-labels: "ingress-ready=true" 11 | authorization-mode: "AlwaysAllow" 12 | extraPortMappings: 13 | - containerPort: 80 14 | hostPort: 80 15 | protocol: TCP 16 | - containerPort: 443 17 | hostPort: 443 18 | protocol: TCP -------------------------------------------------------------------------------- /test/ci/update-answers.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const answers = require("./answers.json"); 3 | 4 | if (process.env.TRANSPORTER) { 5 | answers.transporter = process.env.TRANSPORTER 6 | } 7 | 8 | fs.writeFileSync("./answers.json", JSON.stringify(answers, null, 4), "utf8"); 9 | 10 | console.log("Done", answers); -------------------------------------------------------------------------------- /test/demo/answers.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiGW": true, 3 | "needTransporter": true, 4 | "transporter": "NATS", 5 | "needCacher": false, 6 | "dbService": true, 7 | "metrics": true, 8 | "reporter": "Prometheus", 9 | "tracing": true, 10 | "exporter": "Console", 11 | "docker": true, 12 | "lint": true 13 | } --------------------------------------------------------------------------------