├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── _proto ├── comments.proto ├── commons.proto ├── organizations.proto └── users.proto ├── api-gateway ├── .env.example ├── .eslintrc.yaml ├── Dockerfile ├── nest-cli.json ├── package-lock.json ├── package.json ├── src │ ├── _proto │ │ ├── comments.proto │ │ ├── commons.proto │ │ ├── organizations.proto │ │ └── users.proto │ ├── app.module.ts │ ├── comments │ │ ├── comment.dto.ts │ │ ├── comments-svc.options.ts │ │ └── comments.interface.ts │ ├── commons │ │ └── interfaces │ │ │ ├── commons.interface.ts │ │ │ └── request-response.interface.ts │ ├── health-check │ │ ├── health-check.controller.ts │ │ └── health-check.module.ts │ ├── main.ts │ ├── organizations │ │ ├── organization-svc.options.ts │ │ ├── organizations.controller.ts │ │ ├── organizations.interface.ts │ │ └── organizations.module.ts │ ├── users │ │ ├── users-svc.options.ts │ │ └── users.interface.ts │ └── utils │ │ ├── query.utils.ts │ │ └── utils.module.ts ├── tsconfig.build.json └── tsconfig.json ├── docker-compose.yaml ├── docs ├── img │ ├── archi-diagram.png │ └── rest-ui.png ├── openapi-spec.json ├── openapi-spec.yaml └── proto-docs.md ├── microservices ├── comments-svc │ ├── .env.example │ ├── .eslintrc.yaml │ ├── Dockerfile │ ├── nest-cli.json │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── _proto │ │ │ ├── comments.proto │ │ │ └── commons.proto │ │ ├── app.module.ts │ │ ├── comments │ │ │ ├── comment.dto.ts │ │ │ ├── comment.entity.ts │ │ │ ├── comments.controller.ts │ │ │ ├── comments.interface.ts │ │ │ ├── comments.module.ts │ │ │ ├── comments.seeder.ts │ │ │ └── comments.service.ts │ │ ├── commons │ │ │ └── interfaces │ │ │ │ └── commons.interface.ts │ │ ├── database │ │ │ ├── database.module.ts │ │ │ └── database.providers.ts │ │ └── main.ts │ ├── tsconfig.build.json │ └── tsconfig.json ├── organizations-svc │ ├── .env.example │ ├── .eslintrc.yaml │ ├── Dockerfile │ ├── nest-cli.json │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── _proto │ │ │ ├── commons.proto │ │ │ └── organizations.proto │ │ ├── app.module.ts │ │ ├── commons │ │ │ └── interfaces │ │ │ │ └── commons.interface.ts │ │ ├── database │ │ │ ├── database.module.ts │ │ │ └── database.providers.ts │ │ ├── main.ts │ │ └── organizations │ │ │ ├── organization.dto.ts │ │ │ ├── organization.entity.ts │ │ │ ├── organizations.controller.ts │ │ │ ├── organizations.interface.ts │ │ │ ├── organizations.module.ts │ │ │ ├── organizations.seeder.ts │ │ │ └── organizations.service.ts │ ├── tsconfig.build.json │ └── tsconfig.json └── users-svc │ ├── .env.example │ ├── .eslintrc.yaml │ ├── Dockerfile │ ├── nest-cli.json │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── _proto │ │ ├── commons.proto │ │ └── users.proto │ ├── app.module.ts │ ├── commons │ │ └── interfaces │ │ │ └── commons.interface.ts │ ├── database │ │ ├── database.module.ts │ │ └── database.providers.ts │ ├── main.ts │ └── users │ │ ├── user.dto.ts │ │ ├── user.entity.ts │ │ ├── users.controller.ts │ │ ├── users.interface.ts │ │ ├── users.module.ts │ │ ├── users.seeder.ts │ │ └── users.service.ts │ ├── tsconfig.build.json │ └── tsconfig.json ├── package.json └── scripts ├── build.sh ├── generate-proto-docs.sh ├── install.sh └── lint.sh /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | dist 3 | /dist 4 | /node_modules 5 | 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # Diagnostic reports (https://nodejs.org/api/report.html) 15 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 16 | 17 | # Runtime data 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | 23 | # Directory for instrumented libs generated by jscoverage/JSCover 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | coverage 28 | *.lcov 29 | 30 | # nyc test coverage 31 | .nyc_output 32 | 33 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 34 | .grunt 35 | 36 | # Bower dependency directory (https://bower.io/) 37 | bower_components 38 | 39 | # node-waf configuration 40 | .lock-wscript 41 | 42 | # Compiled binary addons (https://nodejs.org/api/addons.html) 43 | build/Release 44 | 45 | # Dependency directories 46 | node_modules/ 47 | jspm_packages/ 48 | 49 | # TypeScript v1 declaration files 50 | typings/ 51 | 52 | # TypeScript cache 53 | *.tsbuildinfo 54 | 55 | # Optional npm cache directory 56 | .npm 57 | 58 | # Optional eslint cache 59 | .eslintcache 60 | 61 | # Microbundle cache 62 | .rpt2_cache/ 63 | .rts2_cache_cjs/ 64 | .rts2_cache_es/ 65 | .rts2_cache_umd/ 66 | 67 | # Optional REPL history 68 | .node_repl_history 69 | 70 | # Output of 'npm pack' 71 | *.tgz 72 | 73 | # Yarn Integrity file 74 | .yarn-integrity 75 | 76 | # dotenv environment variables file 77 | .env 78 | .env.test 79 | 80 | # parcel-bundler cache (https://parceljs.org/) 81 | .cache 82 | 83 | # Next.js build output 84 | .next 85 | 86 | # Nuxt.js build / generate output 87 | .nuxt 88 | 89 | # Gatsby files 90 | .cache/ 91 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 92 | # https://nextjs.org/blog/next-9-1#public-directory-support 93 | # public 94 | 95 | # vuepress build output 96 | .vuepress/dist 97 | 98 | # Serverless directories 99 | .serverless/ 100 | 101 | # FuseBox cache 102 | .fusebox/ 103 | 104 | # DynamoDB Local files 105 | .dynamodb/ 106 | 107 | # TernJS port file 108 | .tern-port 109 | 110 | # Tests 111 | /coverage 112 | /.nyc_output 113 | 114 | # IDEs and editors 115 | /.idea 116 | .project 117 | .classpath 118 | .c9/ 119 | *.launch 120 | .settings/ 121 | *.sublime-workspace 122 | 123 | # IDE - VSCode 124 | .vscode/* 125 | !.vscode/settings.json 126 | !.vscode/tasks.json 127 | !.vscode/launch.json 128 | !.vscode/extensions.json 129 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Benj Sicam 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NestJS REST API Gateway + gRPC microservices 2 | 3 | This project is a [monorepo](https://gomonorepo.org/) containing a REST API gateway with [gRPC](https://grpc.io/) back-end microservices all written using the NestJS Framework and TypeScript. This project is mainly used for learning/trial purposes only. 4 | 5 | ## Architecture Overview 6 | 7 | The REST API acts as a gateway/proxy for the different microservices it exposes. The controllers of the REST API make calls to the gRPC servers/microservices in the back-end. The gRPC microservices then handles the request to connect to databases or any other service it needs to serve requests. 8 | 9 | ### Diagram 10 | 11 | A diagram of the architecture is shown below. 12 | 13 | ![Architecture Diagram](https://raw.githubusercontent.com/benjsicam/nestjs-rest-microservices/master/docs/img/archi-diagram.png) 14 | 15 | ### Design Patterns 16 | 17 | This architecture implements the following Microservice Design Patterns: 18 | 19 | 1. [Microservice Architecture](https://microservices.io/patterns/microservices.html) 20 | 2. [Subdomain Decomposition](https://microservices.io/patterns/decomposition/decompose-by-subdomain.html) 21 | 3. [Externalized Configuration](https://microservices.io/patterns/externalized-configuration.html) 22 | 4. [Remote Procedure Invocation](https://microservices.io/patterns/communication-style/rpi.html) 23 | 5. [API Gateway](https://microservices.io/patterns/apigateway.html) 24 | 6. [Database per Service](https://microservices.io/patterns/data/database-per-service.html) 25 | 26 | ## Layers 27 | 28 | ### API Layer 29 | 30 | [NestJS + Express](https://nestjs.com/) acts as the API Layer for the architecture. It takes care of listening for client requests and calling the appropriate back-end microservice to fulfill them. 31 | 32 | ### Microservice Layer 33 | 34 | [gRPC](https://grpc.io/) was chosen as the framework to do the microservices. [Protocol buffers](https://developers.google.com/protocol-buffers/) was used as the data interchange format between the client (REST API) and the server (gRPC microservices). NestJS is still the framework used to create the gRPC Microservices. 35 | 36 | ### Persistence Layer 37 | 38 | PostgreSQL is used as the database and Sequelize is used as the Object-Relational Mapper (ORM). 39 | 40 | ## Deployment 41 | 42 | Deployment is done with containers in mind. A Docker Compose file along with Dockerfiles for each project are given to run the whole thing on any machine. For production, it's always recommended to use [Kubernetes](https://kubernetes.io/) for these kinds of microservices architecture to deploy in production. [Istio](https://istio.io/) takes care of service discovery, distributed tracing and other observability requirements. 43 | 44 | ## Project Structure 45 | 46 | ``` 47 | . 48 | ├── _proto 49 | ├── api-gateway 50 | │ └── src 51 | │ ├── _proto 52 | │ ├── comments 53 | │ ├── commons 54 | │ ├── health-check 55 | │ ├── organizations 56 | │ ├── users 57 | │ └── utils 58 | ├── docs 59 | ├── microservices 60 | │ ├── comments-svc 61 | │ │ └── src 62 | │ │ ├── _proto 63 | │ │ ├── comments 64 | │ │ ├── commons 65 | │ │ └── database 66 | │ ├── organizations-svc 67 | │ │ └── src 68 | │ │ ├── _proto 69 | │ │ ├── commons 70 | │ │ ├── database 71 | │ │ └── organizations 72 | │ └── users-svc 73 | │ └── src 74 | │ ├── _proto 75 | │ ├── commons 76 | │ ├── database 77 | │ └── users 78 | └── scripts 79 | ``` 80 | 81 | ### Project Organization 82 | 83 | 1. `_proto` - This directory consists of all the gRPC Service Definitions/Protocol Buffers. 84 | 85 | 2. `api-gateway` - This directory consists of the API Gateway project. All code relating to the API Gateway resides here. 86 | 87 | 3. `docs` - This directory consists of all files relating to documentation. The OpenAPI Specification for the REST API and the gRPC Service Definitions/Protocol Buffers documentation can be found here. 88 | 89 | 4. `microservices` - This directory consists of all microservice projects. 90 | 91 | 5. `microservices/comments-svc` - This directory consists of all files/code relating to the Comments Microservice project. 92 | 93 | 6. `microservices/organizations-svc` - This directory consists of all files/code relating to the Organizations Microservice project. 94 | 95 | 7. `microservices/users-svc` - This directory consists of all files/code relating to the Users Microservice project. 96 | 97 | 8. `scripts` - This directory consists of shell scripts that automates building and running the whole project. 98 | 99 | ## How to Run 100 | 101 | 1. System Requirements - must be Linux/Mac 102 | - [Node.js](https://nodejs.org/en/) - v12 Recommended 103 | - [Docker](https://docs.docker.com/install/) - latest 104 | - [Docker Compose](https://docs.docker.com/compose/install/) - latest 105 | 106 | 2. On the Terminal, go into the project's root folder (`cd /project/root/folder`) and execute `npm start`. The start script will install all npm dependencies for all projects, lint the code, compile the code, build the artifacts (Docker images) and run them via `docker-compose`. 107 | 108 | 3. Once the start script is done, the API Gateway will listening on [http://localhost:3000](http://localhost:3000) 109 | 110 | 4. To test the API, head to the Swagger UI running at [http://localhost:8080](http://localhost:3000) 111 | 112 | ![REST UI](https://raw.githubusercontent.com/benjsicam/nestjs-rest-microservices/master/docs/img/rest-ui.png) 113 | 114 | ## Roadmap 115 | 116 | ### General 117 | 118 | - [ ] Use RxJS Observables instead of Promises 119 | - [ ] Add Integration Tests 120 | - [ ] Add CI/CD Pipeline 121 | - [ ] Add Kubernetes Manifests 122 | - [x] Pre-populate DBs 123 | - [ ] Distributed Tracing 124 | 125 | ### API Gateway 126 | 127 | - [ ] Add authentication 128 | - [ ] Add authorization 129 | - [ ] Add event sourcing 130 | - [ ] Add request/input data validation 131 | - [ ] Improve error handling 132 | 133 | ### Microservices 134 | 135 | - [ ] Add health checks 136 | - [ ] Add caching 137 | - [ ] Improve error handling 138 | -------------------------------------------------------------------------------- /_proto/comments.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package comments; 4 | 5 | import "commons.proto"; 6 | 7 | message Comment { 8 | string id = 1; 9 | string organization = 2; 10 | string comment = 3; 11 | string createdAt = 4; 12 | string updatedAt = 5; 13 | int32 version = 6; 14 | } 15 | 16 | message CreateCommentInput { 17 | string organization = 1; 18 | string comment = 2; 19 | } 20 | 21 | message CommentsList { 22 | repeated Comment data = 5; 23 | } 24 | 25 | service CommentsService { 26 | rpc findAll (commons.Query) returns (CommentsList) {} 27 | rpc count (commons.Query) returns (commons.Count) {} 28 | rpc create (CreateCommentInput) returns (Comment) {} 29 | rpc destroy (commons.Query) returns (commons.Count) {} 30 | } 31 | -------------------------------------------------------------------------------- /_proto/commons.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package commons; 4 | 5 | message Id { 6 | string id = 1; 7 | } 8 | 9 | message Name { 10 | string name = 1; 11 | } 12 | 13 | message Query { 14 | repeated string attributes = 1; 15 | string where = 2; 16 | string order = 3; 17 | int32 offset = 4; 18 | int32 limit = 5; 19 | } 20 | 21 | message Count { 22 | int32 count = 1; 23 | } 24 | -------------------------------------------------------------------------------- /_proto/organizations.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package organizations; 4 | 5 | import "commons.proto"; 6 | 7 | message Organization { 8 | string id = 1; 9 | string name = 2; 10 | string createdAt = 3; 11 | string updatedAt = 4; 12 | int32 version = 5; 13 | } 14 | 15 | message OrganizationsList { 16 | repeated Organization data = 5; 17 | } 18 | 19 | service OrganizationsService { 20 | rpc findAll (commons.Query) returns (OrganizationsList) {} 21 | rpc findByName (commons.Name) returns (Organization) {} 22 | rpc count (commons.Query) returns (commons.Count) {} 23 | } 24 | -------------------------------------------------------------------------------- /_proto/users.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package users; 4 | 5 | import "commons.proto"; 6 | 7 | message User { 8 | string id = 1; 9 | string organization = 2; 10 | string loginId = 3; 11 | string avatar = 4; 12 | int32 followers = 5; 13 | int32 following = 6; 14 | string createdAt = 7; 15 | string updatedAt = 8; 16 | int32 version = 9; 17 | } 18 | 19 | message UsersList { 20 | repeated User data = 5; 21 | } 22 | 23 | service UsersService { 24 | rpc findAll (commons.Query) returns (UsersList) {} 25 | rpc count (commons.Query) returns (commons.Count) {} 26 | } 27 | -------------------------------------------------------------------------------- /api-gateway/.env.example: -------------------------------------------------------------------------------- 1 | # Application Options 2 | NODE_ENV=development 3 | PORT=3000 4 | 5 | COMMENTS_SVC_URL=localhost 6 | COMMENTS_SVC_PORT=50051 7 | 8 | ORGANIZATIONS_SVC_URL=localhost 9 | ORGANIZATIONS_SVC_PORT=50052 10 | 11 | USERS_SVC_URL=localhost 12 | USERS_SVC_PORT=50053 13 | -------------------------------------------------------------------------------- /api-gateway/.eslintrc.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | parser: "@typescript-eslint/parser" 3 | parserOptions: 4 | project: "tsconfig.json" 5 | sourceType: "module" 6 | plugins: 7 | - "@typescript-eslint/eslint-plugin" 8 | - "prettier" 9 | - "import" 10 | extends: 11 | - "airbnb-base" 12 | - "plugin:@typescript-eslint/eslint-recommended" 13 | - "plugin:@typescript-eslint/recommended" 14 | - "prettier" 15 | - "prettier/@typescript-eslint" 16 | - "plugin:prettier/recommended" 17 | - "plugin:jest/recommended" 18 | - "plugin:import/errors" 19 | - "plugin:import/warnings" 20 | - "plugin:import/typescript" 21 | root: true 22 | env: 23 | node: true 24 | jest: true 25 | rules: 26 | "@typescript-eslint/interface-name-prefix": "off" 27 | "@typescript-eslint/explicit-function-return-type": "off" 28 | "@typescript-eslint/no-explicit-any": "off" 29 | "class-methods-use-this": 0 30 | import/prefer-default-export: 0 31 | import/extensions: 32 | - "error" 33 | - "ignorePackages" 34 | - js: "never" 35 | jsx: "never" 36 | ts: "never" 37 | tsx: "never" 38 | prettier/prettier: 39 | - "error" 40 | - parser: "typescript" 41 | printWidth: 200 42 | semi: false 43 | singleQuote: true 44 | endOfline: "lf" 45 | -------------------------------------------------------------------------------- /api-gateway/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12-alpine as build 2 | 3 | WORKDIR /usr/local/api-gateway 4 | 5 | COPY dist package.json ./ 6 | 7 | RUN npm install --production 8 | 9 | FROM node:12-alpine 10 | 11 | WORKDIR /usr/local/api-gateway 12 | 13 | COPY --from=build /usr/local/api-gateway . 14 | 15 | EXPOSE 3000 16 | 17 | CMD ["node", "main.js"] 18 | -------------------------------------------------------------------------------- /api-gateway/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src", 4 | "compilerOptions": { 5 | "assets": ["**/*.proto"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /api-gateway/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api-gateway", 3 | "version": "1.0.0", 4 | "description": "REST API Gateway", 5 | "scripts": { 6 | "prebuild": "rimraf dist", 7 | "build": "nest build", 8 | "start": "nest start", 9 | "start:dev": "nest start --watch", 10 | "start:debug": "nest start --debug --watch", 11 | "start:prod": "node dist/main", 12 | "lint": "eslint '{src,apps,libs,test}/**/*.ts' --fix", 13 | "test": "jest", 14 | "test:watch": "jest --watch", 15 | "test:cov": "jest --coverage", 16 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 17 | "test:e2e": "jest --config ./test/jest-e2e.json" 18 | }, 19 | "dependencies": { 20 | "@grpc/proto-loader": "^0.5.3", 21 | "@nestjs/common": "6.11.6", 22 | "@nestjs/config": "0.2.2", 23 | "@nestjs/core": "6.11.6", 24 | "@nestjs/microservices": "6.11.6", 25 | "@nestjs/platform-express": "6.11.6", 26 | "express": "4.17.1", 27 | "grpc": "^1.24.2", 28 | "lodash": "4.17.15", 29 | "nestjs-pino": "1.1.3", 30 | "pino": "5.16.0", 31 | "reflect-metadata": "0.1.13", 32 | "rimraf": "3.0.2", 33 | "rxjs": "6.5.4" 34 | }, 35 | "devDependencies": { 36 | "@nestjs/cli": "6.14.2", 37 | "@nestjs/schematics": "6.9.3", 38 | "@nestjs/testing": "6.11.6", 39 | "@types/express": "4.17.2", 40 | "@types/faker": "4.1.9", 41 | "@types/jest": "25.1.2", 42 | "@types/lodash": "4.14.149", 43 | "@types/node": "13.7.0", 44 | "@types/supertest": "2.0.8", 45 | "@typescript-eslint/eslint-plugin": "2.19.0", 46 | "@typescript-eslint/parser": "2.19.0", 47 | "eslint": "6.8.0", 48 | "eslint-config-airbnb-base": "14.0.0", 49 | "eslint-config-prettier": "6.10.0", 50 | "eslint-plugin-import": "2.20.1", 51 | "eslint-plugin-jest": "23.7.0", 52 | "eslint-plugin-prettier": "3.1.2", 53 | "faker": "4.1.0", 54 | "jest": "25.1.0", 55 | "jest-extended": "0.11.5", 56 | "pino-pretty": "3.5.0", 57 | "prettier": "1.19.1", 58 | "supertest": "4.0.2", 59 | "ts-jest": "25.2.0", 60 | "ts-loader": "6.2.1", 61 | "ts-node": "8.6.2", 62 | "tsconfig-paths": "3.9.0", 63 | "typescript": "3.7.5" 64 | }, 65 | "repository": { 66 | "type": "git", 67 | "url": "git+ssh://git@github.com:benjsicam/nestjs-rest-microservices.git" 68 | }, 69 | "author": "Benj Sicam", 70 | "license": "MIT", 71 | "bugs": { 72 | "url": "https://github.com/benjsicam/nestjs-rest-microservices/issues" 73 | }, 74 | "homepage": "https://github.com/benjsicam/nestjs-rest-microservices#readme", 75 | "jest": { 76 | "moduleFileExtensions": [ 77 | "js", 78 | "json", 79 | "ts" 80 | ], 81 | "rootDir": "src", 82 | "testRegex": "test/*.+(test.ts)", 83 | "transform": { 84 | ".+\\.(t|j)s$": "ts-jest" 85 | }, 86 | "collectCoverage": true, 87 | "coverageThreshold": { 88 | "global": { 89 | "branches": 50, 90 | "functions": 75, 91 | "lines": 75, 92 | "statements": 75 93 | } 94 | }, 95 | "coverageDirectory": "../coverage", 96 | "testEnvironment": "node" 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /api-gateway/src/_proto/comments.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package comments; 4 | 5 | import "commons.proto"; 6 | 7 | message Comment { 8 | string id = 1; 9 | string organization = 2; 10 | string comment = 3; 11 | string createdAt = 4; 12 | string updatedAt = 5; 13 | int32 version = 6; 14 | } 15 | 16 | message CreateCommentInput { 17 | string organization = 1; 18 | string comment = 2; 19 | } 20 | 21 | message CommentsList { 22 | repeated Comment data = 5; 23 | } 24 | 25 | service CommentsService { 26 | rpc findAll (commons.Query) returns (CommentsList) {} 27 | rpc count (commons.Query) returns (commons.Count) {} 28 | rpc create (CreateCommentInput) returns (Comment) {} 29 | rpc destroy (commons.Query) returns (commons.Count) {} 30 | } 31 | -------------------------------------------------------------------------------- /api-gateway/src/_proto/commons.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package commons; 4 | 5 | message Id { 6 | string id = 1; 7 | } 8 | 9 | message Name { 10 | string name = 1; 11 | } 12 | 13 | message Query { 14 | repeated string attributes = 1; 15 | string where = 2; 16 | string order = 3; 17 | int32 offset = 4; 18 | int32 limit = 5; 19 | } 20 | 21 | message Count { 22 | int32 count = 1; 23 | } 24 | -------------------------------------------------------------------------------- /api-gateway/src/_proto/organizations.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package organizations; 4 | 5 | import "commons.proto"; 6 | 7 | message Organization { 8 | string id = 1; 9 | string name = 2; 10 | string createdAt = 3; 11 | string updatedAt = 4; 12 | int32 version = 5; 13 | } 14 | 15 | message OrganizationsList { 16 | repeated Organization data = 5; 17 | } 18 | 19 | service OrganizationsService { 20 | rpc findAll (commons.Query) returns (OrganizationsList) {} 21 | rpc findByName (commons.Name) returns (Organization) {} 22 | rpc count (commons.Query) returns (commons.Count) {} 23 | } 24 | -------------------------------------------------------------------------------- /api-gateway/src/_proto/users.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package users; 4 | 5 | import "commons.proto"; 6 | 7 | message User { 8 | string id = 1; 9 | string organization = 2; 10 | string loginId = 3; 11 | string avatar = 4; 12 | int32 followers = 5; 13 | int32 following = 6; 14 | string createdAt = 7; 15 | string updatedAt = 8; 16 | int32 version = 9; 17 | } 18 | 19 | message UsersList { 20 | repeated User data = 5; 21 | } 22 | 23 | service UsersService { 24 | rpc findAll (commons.Query) returns (UsersList) {} 25 | rpc count (commons.Query) returns (commons.Count) {} 26 | } 27 | -------------------------------------------------------------------------------- /api-gateway/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { ConfigModule } from '@nestjs/config' 3 | import { LoggerModule } from 'nestjs-pino' 4 | 5 | import { HealthCheckModule } from './health-check/health-check.module' 6 | import { OrganizationsModule } from './organizations/organizations.module' 7 | 8 | @Module({ 9 | imports: [ 10 | ConfigModule.forRoot(), 11 | LoggerModule.forRoot({ 12 | pinoHttp: { 13 | safe: true, 14 | prettyPrint: process.env.NODE_ENV === 'development' 15 | } 16 | }), 17 | HealthCheckModule, 18 | OrganizationsModule 19 | ] 20 | }) 21 | export class AppModule {} 22 | -------------------------------------------------------------------------------- /api-gateway/src/comments/comment.dto.ts: -------------------------------------------------------------------------------- 1 | export class CommentDto { 2 | readonly id?: string 3 | 4 | readonly organization: string 5 | 6 | readonly comment: string 7 | } 8 | -------------------------------------------------------------------------------- /api-gateway/src/comments/comments-svc.options.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path' 2 | import { Transport, ClientOptions } from '@nestjs/microservices' 3 | 4 | export const CommentsServiceClientOptions: ClientOptions = { 5 | transport: Transport.GRPC, 6 | options: { 7 | url: `${process.env.COMMENTS_SVC_URL}:${process.env.COMMENTS_SVC_PORT}`, 8 | package: 'comments', 9 | protoPath: join(__dirname, '../_proto/comments.proto'), 10 | loader: { 11 | enums: String, 12 | objects: true, 13 | arrays: true 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /api-gateway/src/comments/comments.interface.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs' 2 | 3 | import { Count, Query } from '../commons/interfaces/commons.interface' 4 | import { CommentDto } from './comment.dto' 5 | 6 | export interface Comment { 7 | id: string 8 | organization: string 9 | comment: string 10 | createdAt: string 11 | updatedAt: string 12 | } 13 | 14 | export interface CommentsQueryResult { 15 | data: Array 16 | } 17 | 18 | export interface CommentsService { 19 | findAll(query?: Query): Observable 20 | count(query?: Query): Observable 21 | create(comment: CommentDto): Observable 22 | destroy(query?: Query): Observable 23 | } 24 | -------------------------------------------------------------------------------- /api-gateway/src/commons/interfaces/commons.interface.ts: -------------------------------------------------------------------------------- 1 | export interface Id { 2 | id: string 3 | } 4 | 5 | export interface Name { 6 | name: string 7 | } 8 | 9 | export interface Query { 10 | attributes?: Array 11 | where?: string 12 | order?: string 13 | offset?: number 14 | limit?: number 15 | } 16 | 17 | export interface Count { 18 | count: number 19 | } 20 | -------------------------------------------------------------------------------- /api-gateway/src/commons/interfaces/request-response.interface.ts: -------------------------------------------------------------------------------- 1 | export interface RequestQuery { 2 | q: string 3 | select: string 4 | orderBy: string 5 | page: number 6 | limit: number 7 | } 8 | 9 | export interface QueryResponse { 10 | totalRecords: number 11 | totalPages: number 12 | page: number 13 | limit: number 14 | data: Array 15 | } 16 | -------------------------------------------------------------------------------- /api-gateway/src/health-check/health-check.controller.ts: -------------------------------------------------------------------------------- 1 | import { Response } from 'express' 2 | 3 | import { Controller, Get, Res, HttpStatus } from '@nestjs/common' 4 | 5 | @Controller('/healthz') 6 | export class HealthCheckController { 7 | @Get() 8 | healthCheck(@Res() res: Response) { 9 | res.status(HttpStatus.OK).send('OK') 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /api-gateway/src/health-check/health-check.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | 3 | import { HealthCheckController } from './health-check.controller' 4 | 5 | @Module({ 6 | controllers: [HealthCheckController] 7 | }) 8 | export class HealthCheckModule {} 9 | -------------------------------------------------------------------------------- /api-gateway/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core' 2 | import { Logger } from 'nestjs-pino' 3 | 4 | import { AppModule } from './app.module' 5 | 6 | async function bootstrap() { 7 | const app = await NestFactory.create(AppModule) 8 | 9 | app.useLogger(app.get(Logger)) 10 | app.enableCors({ 11 | origin: '*' 12 | }) 13 | 14 | return app.listen(process.env.PORT) 15 | } 16 | 17 | bootstrap() 18 | -------------------------------------------------------------------------------- /api-gateway/src/organizations/organization-svc.options.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path' 2 | import { ClientOptions, Transport } from '@nestjs/microservices' 3 | 4 | export const OrganizationsServiceClientOptions: ClientOptions = { 5 | transport: Transport.GRPC, 6 | options: { 7 | url: `${process.env.ORGANIZATIONS_SVC_URL}:${process.env.ORGANIZATIONS_SVC_PORT}`, 8 | package: 'organizations', 9 | protoPath: join(__dirname, '../_proto/organizations.proto'), 10 | loader: { 11 | enums: String, 12 | objects: true, 13 | arrays: true 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /api-gateway/src/organizations/organizations.controller.ts: -------------------------------------------------------------------------------- 1 | import { PinoLogger } from 'nestjs-pino' 2 | import { ClientGrpc, Client } from '@nestjs/microservices' 3 | import { Controller, Get, Post, Delete, Query, Body, Param, Inject, OnModuleInit, NotFoundException, Header } from '@nestjs/common' 4 | import { isEmpty } from 'lodash' 5 | 6 | import { QueryUtils } from '../utils/query.utils' 7 | import { Count } from '../commons/interfaces/commons.interface' 8 | import { RequestQuery, QueryResponse } from '../commons/interfaces/request-response.interface' 9 | 10 | import { CommentsService, Comment, CommentsQueryResult } from '../comments/comments.interface' 11 | import { OrganizationsService, Organization, OrganizationsQueryResult } from './organizations.interface' 12 | import { UsersService, UsersQueryResult } from '../users/users.interface' 13 | 14 | import { CommentDto } from '../comments/comment.dto' 15 | 16 | import { CommentsServiceClientOptions } from '../comments/comments-svc.options' 17 | import { OrganizationsServiceClientOptions } from './organization-svc.options' 18 | import { UsersServiceClientOptions } from '../users/users-svc.options' 19 | 20 | @Controller('orgs') 21 | export class OrganizationController implements OnModuleInit { 22 | constructor(@Inject('QueryUtils') private readonly queryUtils: QueryUtils, private readonly logger: PinoLogger) { 23 | logger.setContext(OrganizationController.name) 24 | } 25 | 26 | @Client(CommentsServiceClientOptions) 27 | private readonly commentsServiceClient: ClientGrpc 28 | 29 | @Client(OrganizationsServiceClientOptions) 30 | private readonly organizationsServiceClient: ClientGrpc 31 | 32 | @Client(UsersServiceClientOptions) 33 | private readonly usersServiceClient: ClientGrpc 34 | 35 | private commentsService: CommentsService 36 | 37 | private organizationsService: OrganizationsService 38 | 39 | private usersService: UsersService 40 | 41 | onModuleInit() { 42 | this.commentsService = this.commentsServiceClient.getService('CommentsService') 43 | this.organizationsService = this.organizationsServiceClient.getService('OrganizationsService') 44 | this.usersService = this.usersServiceClient.getService('UsersService') 45 | } 46 | 47 | @Get() 48 | @Header('Content-Type', 'application/json') 49 | async findOrganizations(@Query() query: RequestQuery): Promise { 50 | this.logger.info('OrganizationController#findOrganizations.call', query) 51 | 52 | const args = { 53 | ...(await this.queryUtils.getQueryParams(query)) 54 | } 55 | 56 | const { count } = await this.organizationsService 57 | .count({ 58 | where: !isEmpty(query.q) ? JSON.stringify({ name: { $like: query.q } }) : undefined 59 | }) 60 | .toPromise() 61 | 62 | const data: OrganizationsQueryResult = await this.organizationsService 63 | .findAll({ 64 | attributes: args.attributes, 65 | where: !isEmpty(query.q) ? JSON.stringify({ name: { $like: query.q } }) : undefined, 66 | order: JSON.stringify(args.order), 67 | offset: args.offset, 68 | limit: args.limit 69 | }) 70 | .toPromise() 71 | 72 | const result: QueryResponse = { 73 | totalRecords: count, 74 | totalPages: Math.ceil(count / args.limit), 75 | page: args.page, 76 | limit: args.limit, 77 | ...data 78 | } 79 | 80 | this.logger.info('OrganizationController#findOrganizations.result', result) 81 | 82 | return result 83 | } 84 | 85 | @Get(':name/members') 86 | @Header('Content-Type', 'application/json') 87 | async findOrganizationMembers(@Param('name') name: string, @Query() query: RequestQuery): Promise { 88 | this.logger.info('OrganizationController#findOrganizationMembers.call', query) 89 | 90 | const organization: Organization = await this.organizationsService 91 | .findByName({ 92 | name 93 | }) 94 | .toPromise() 95 | 96 | if (!organization) throw new NotFoundException('NOT_FOUND', 'Organization not found.') 97 | 98 | const where = { organization: organization.id } 99 | 100 | if (!isEmpty(query.q)) { 101 | Object.assign(where, { 102 | name: { $like: query.q } 103 | }) 104 | } 105 | 106 | const args = { 107 | ...(await this.queryUtils.getQueryParams(query)) 108 | } 109 | 110 | const { count } = await this.usersService 111 | .count({ 112 | where: JSON.stringify(where) 113 | }) 114 | .toPromise() 115 | 116 | const data: UsersQueryResult = await this.usersService 117 | .findAll({ 118 | attributes: args.attributes, 119 | where: JSON.stringify(where), 120 | order: !isEmpty(args.order) ? JSON.stringify(args.order) : JSON.stringify([['followers', 'DESC']]), 121 | offset: args.offset, 122 | limit: args.limit 123 | }) 124 | .toPromise() 125 | 126 | const result: QueryResponse = { 127 | totalRecords: count, 128 | totalPages: Math.ceil(count / args.limit), 129 | page: args.page, 130 | limit: args.limit, 131 | ...data 132 | } 133 | 134 | this.logger.info('OrganizationController#findOrganizationMembers.result', result) 135 | 136 | return result 137 | } 138 | 139 | @Get(':name/comments') 140 | @Header('Content-Type', 'application/json') 141 | async findOrganizationComments(@Param('name') name: string, @Query() query: RequestQuery): Promise { 142 | this.logger.info('OrganizationController#findOrganizationComments.call', query) 143 | 144 | const organization: Organization = await this.organizationsService 145 | .findByName({ 146 | name 147 | }) 148 | .toPromise() 149 | 150 | if (!organization) throw new NotFoundException('NOT_FOUND', 'Organization not found.') 151 | 152 | const where = { organization: organization.id } 153 | 154 | if (!isEmpty(query.q)) { 155 | Object.assign(where, { 156 | name: { $like: query.q } 157 | }) 158 | } 159 | 160 | const args = { 161 | ...(await this.queryUtils.getQueryParams(query)) 162 | } 163 | 164 | const { count } = await this.commentsService 165 | .count({ 166 | where: JSON.stringify(where) 167 | }) 168 | .toPromise() 169 | 170 | const data: CommentsQueryResult = await this.commentsService 171 | .findAll({ 172 | attributes: args.attributes, 173 | where: JSON.stringify(where), 174 | order: JSON.stringify(args.order), 175 | offset: args.offset, 176 | limit: args.limit 177 | }) 178 | .toPromise() 179 | 180 | const result: QueryResponse = { 181 | totalRecords: count, 182 | totalPages: Math.ceil(count / args.limit), 183 | page: args.page, 184 | limit: args.limit, 185 | ...data 186 | } 187 | 188 | this.logger.info('OrganizationController#findOrganizationComments.call', result) 189 | 190 | return result 191 | } 192 | 193 | @Post(':name/comments') 194 | @Header('Content-Type', 'application/json') 195 | async createOrganizationComment(@Param('name') name: string, @Body() comment: CommentDto): Promise { 196 | this.logger.info('OrganizationController#createOrganizationComment.call', name) 197 | 198 | const organization: Organization = await this.organizationsService 199 | .findByName({ 200 | name 201 | }) 202 | .toPromise() 203 | 204 | if (!organization) throw new NotFoundException('NOT_FOUND', 'Organization not found.') 205 | 206 | const result: Comment = await this.commentsService 207 | .create({ 208 | ...comment, 209 | organization: organization.id 210 | }) 211 | .toPromise() 212 | 213 | this.logger.info('OrganizationController#createOrganizationComment.result', result) 214 | 215 | return result 216 | } 217 | 218 | @Delete(':name/comments') 219 | @Header('Content-Type', 'application/json') 220 | async deleteOrganizationComments(@Param('name') name: string): Promise { 221 | this.logger.info('OrganizationController#deleteOrganizationComments.call', name) 222 | 223 | const organization: Organization = await this.organizationsService 224 | .findByName({ 225 | name 226 | }) 227 | .toPromise() 228 | 229 | if (!organization) throw new NotFoundException('NOT_FOUND', 'Organization not found.') 230 | 231 | const result: Count = await this.commentsService 232 | .destroy({ 233 | where: JSON.stringify({ organization: organization.id }) 234 | }) 235 | .toPromise() 236 | 237 | this.logger.info('OrganizationController#deleteOrganizationComments.result', result) 238 | 239 | return result 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /api-gateway/src/organizations/organizations.interface.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs' 2 | 3 | import { Count, Query, Name } from '../commons/interfaces/commons.interface' 4 | 5 | export interface Organization { 6 | id: string 7 | name: string 8 | createdAt: string 9 | updatedAt: string 10 | } 11 | 12 | export interface OrganizationsQueryResult { 13 | data: Array 14 | } 15 | 16 | export interface OrganizationsService { 17 | findAll(query?: Query): Observable 18 | findByName(name: Name): Observable 19 | count(query?: Query): Observable 20 | } 21 | -------------------------------------------------------------------------------- /api-gateway/src/organizations/organizations.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { LoggerModule } from 'nestjs-pino' 3 | 4 | import { UtilsModule } from '../utils/utils.module' 5 | 6 | import { OrganizationController } from './organizations.controller' 7 | 8 | @Module({ 9 | imports: [ 10 | UtilsModule, 11 | LoggerModule.forRoot({ 12 | pinoHttp: { 13 | safe: true, 14 | prettyPrint: process.env.NODE_ENV === 'development' 15 | } 16 | }) 17 | ], 18 | controllers: [OrganizationController] 19 | }) 20 | export class OrganizationsModule {} 21 | -------------------------------------------------------------------------------- /api-gateway/src/users/users-svc.options.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path' 2 | import { Transport, ClientOptions } from '@nestjs/microservices' 3 | 4 | export const UsersServiceClientOptions: ClientOptions = { 5 | transport: Transport.GRPC, 6 | options: { 7 | url: `${process.env.USERS_SVC_URL}:${process.env.USERS_SVC_PORT}`, 8 | package: 'users', 9 | protoPath: join(__dirname, '../_proto/users.proto'), 10 | loader: { 11 | enums: String, 12 | objects: true, 13 | arrays: true 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /api-gateway/src/users/users.interface.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs' 2 | 3 | import { Count, Query } from '../commons/interfaces/commons.interface' 4 | 5 | export interface User { 6 | id: string 7 | organization: string 8 | loginId: string 9 | avatar: string 10 | followers: number 11 | following: number 12 | createdAt: string 13 | updatedAt: string 14 | } 15 | 16 | export interface UsersQueryResult { 17 | data: Array 18 | } 19 | 20 | export interface UsersService { 21 | findAll(query?: Query): Observable 22 | count(query?: Query): Observable 23 | } 24 | -------------------------------------------------------------------------------- /api-gateway/src/utils/query.utils.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common' 2 | import { isNil, isEmpty, map } from 'lodash' 3 | 4 | import { RequestQuery } from '../commons/interfaces/request-response.interface' 5 | 6 | @Injectable() 7 | export class QueryUtils { 8 | async getQueryParams(query: RequestQuery): Promise { 9 | return { 10 | attributes: await this.getAttributes(query.select), 11 | order: await this.getOrder(query.orderBy), 12 | offset: await this.getOffset(query.page, query.limit), 13 | page: await this.getPage(query.page), 14 | limit: await this.getLimit(query.limit) 15 | } 16 | } 17 | 18 | async getAttributes(attributes: string): Promise> { 19 | return !isEmpty(attributes) ? attributes.split(',') : undefined 20 | } 21 | 22 | async getPage(page: any): Promise { 23 | let result = 1 24 | 25 | if (isEmpty(page)) return result 26 | 27 | if (!isNil(page)) result = parseInt(page, 10) 28 | if (result < 1) result = 1 29 | 30 | return result 31 | } 32 | 33 | async getLimit(limit: any): Promise { 34 | let result = 25 35 | 36 | if (isEmpty(limit)) return result 37 | 38 | if (!isNil(limit)) result = parseInt(limit, 10) 39 | if (result < 1) result = 1 40 | 41 | return result 42 | } 43 | 44 | async getOffset(page: any, limit: any): Promise { 45 | const tmpPage = await this.getPage(page) 46 | const tmpLimit = await this.getLimit(limit) 47 | 48 | return (tmpPage - 1) * tmpLimit 49 | } 50 | 51 | async getOrder(orderBy: string): Promise>> { 52 | let result: Array> = [] 53 | 54 | if (!isEmpty(orderBy)) { 55 | const attributes: Array = orderBy.split(',') 56 | 57 | result = map(attributes, attribute => { 58 | if (attribute.trim().charAt(0) === '-') { 59 | return [attribute.trim().substr(1), 'DESC'] 60 | } 61 | return [attribute.trim(), 'ASC'] 62 | }) 63 | } 64 | 65 | return result 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /api-gateway/src/utils/utils.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | 3 | import { QueryUtils } from './query.utils' 4 | 5 | @Module({ 6 | exports: [QueryUtils], 7 | providers: [QueryUtils] 8 | }) 9 | export class UtilsModule {} 10 | -------------------------------------------------------------------------------- /api-gateway/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /api-gateway/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "target": "es2017", 9 | "sourceMap": true, 10 | "outDir": "./dist", 11 | "baseUrl": "./", 12 | "incremental": true 13 | }, 14 | "exclude": ["node_modules", "dist"] 15 | } 16 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | comments-svc: 5 | image: "comments-svc:dev" 6 | build: 7 | context: "./microservices/comments-svc" 8 | networks: 9 | - "frontend" 10 | - "backend" 11 | - "commentsdomain" 12 | expose: 13 | - "50051" 14 | depends_on: 15 | - "comments-db" 16 | environment: 17 | NODE_ENV: "test" 18 | URL: "0.0.0.0" 19 | PORT: "50051" 20 | DB_NAME: "postgres" 21 | DB_HOST: "comments-db" 22 | DB_PORT: "5432" 23 | DB_USER: "postgres" 24 | DB_PASSWORD: "postgres" 25 | restart: "on-failure" 26 | 27 | organizations-svc: 28 | image: "organizations-svc:dev" 29 | build: 30 | context: "./microservices/organizations-svc" 31 | networks: 32 | - "frontend" 33 | - "backend" 34 | - "organizationsdomain" 35 | expose: 36 | - "50051" 37 | depends_on: 38 | - "organizations-db" 39 | environment: 40 | NODE_ENV: "test" 41 | URL: "0.0.0.0" 42 | PORT: "50051" 43 | DB_NAME: "postgres" 44 | DB_HOST: "organizations-db" 45 | DB_PORT: "5432" 46 | DB_USER: "postgres" 47 | DB_PASSWORD: "postgres" 48 | restart: "on-failure" 49 | 50 | users-svc: 51 | image: "users-svc:dev" 52 | build: 53 | context: "./microservices/users-svc" 54 | networks: 55 | - "frontend" 56 | - "backend" 57 | - "usersdomain" 58 | expose: 59 | - "50051" 60 | depends_on: 61 | - "users-db" 62 | environment: 63 | NODE_ENV: "test" 64 | URL: "0.0.0.0" 65 | PORT: "50051" 66 | DB_NAME: "postgres" 67 | DB_HOST: "users-db" 68 | DB_PORT: "5432" 69 | DB_USER: "postgres" 70 | DB_PASSWORD: "postgres" 71 | restart: "on-failure" 72 | 73 | api-gateway: 74 | image: "api-gateway:dev" 75 | build: 76 | context: "./api-gateway" 77 | networks: 78 | - "frontend" 79 | ports: 80 | - "3000:3000" 81 | depends_on: 82 | - "comments-svc" 83 | - "organizations-svc" 84 | - "users-svc" 85 | environment: 86 | NODE_ENV: "test" 87 | PORT: "3000" 88 | COMMENTS_SVC_URL: "comments-svc" 89 | COMMENTS_SVC_PORT: "50051" 90 | ORGANIZATIONS_SVC_URL: "organizations-svc" 91 | ORGANIZATIONS_SVC_PORT: "50051" 92 | USERS_SVC_URL: "users-svc" 93 | USERS_SVC_PORT: "50051" 94 | healthcheck: 95 | test: ["CMD", "wget", "localhost:3000/healthz -q -O - > /dev/null 2>&1"] 96 | interval: 30s 97 | timeout: 10s 98 | retries: 5 99 | restart: "on-failure" 100 | 101 | swagger-ui: 102 | image: "swaggerapi/swagger-ui:v3.25.0" 103 | networks: 104 | - "frontend" 105 | ports: 106 | - "8080:8080" 107 | volumes: 108 | - "./docs/openapi-spec.yaml:/usr/share/spec/openapi-spec.yaml" 109 | environment: 110 | SWAGGER_JSON: "/usr/share/spec/openapi-spec.yaml" 111 | healthcheck: 112 | test: ["CMD", "wget", "localhost:8080 -q -O - > /dev/null 2>&1"] 113 | interval: 30s 114 | timeout: 10s 115 | retries: 5 116 | 117 | comments-db: 118 | image: "postgres:12.1-alpine" 119 | networks: 120 | - "commentsdomain" 121 | expose: 122 | - "5432" 123 | environment: 124 | POSTGRES_USER: "postgres" 125 | POSTGRES_PASSWORD: "postgres" 126 | healthcheck: 127 | test: ["CMD-SHELL", "su -c 'pg_isready -U postgres' postgres"] 128 | interval: 30s 129 | timeout: 30s 130 | retries: 3 131 | restart: "on-failure" 132 | 133 | organizations-db: 134 | image: "postgres:12.1-alpine" 135 | networks: 136 | - "organizationsdomain" 137 | expose: 138 | - "5432" 139 | environment: 140 | POSTGRES_USER: "postgres" 141 | POSTGRES_PASSWORD: "postgres" 142 | healthcheck: 143 | test: ["CMD-SHELL", "su -c 'pg_isready -U postgres' postgres"] 144 | interval: 30s 145 | timeout: 30s 146 | retries: 3 147 | restart: "on-failure" 148 | 149 | users-db: 150 | image: "postgres:12.1-alpine" 151 | networks: 152 | - "usersdomain" 153 | expose: 154 | - "5432" 155 | environment: 156 | POSTGRES_USER: "postgres" 157 | POSTGRES_PASSWORD: "postgres" 158 | healthcheck: 159 | test: ["CMD-SHELL", "su -c 'pg_isready -U postgres' postgres"] 160 | interval: 30s 161 | timeout: 30s 162 | retries: 3 163 | restart: "on-failure" 164 | 165 | networks: 166 | frontend: 167 | backend: 168 | commentsdomain: 169 | organizationsdomain: 170 | usersdomain: 171 | -------------------------------------------------------------------------------- /docs/img/archi-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benjsicam/nestjs-rest-microservices/3ab9494556bc8f7e4fffd07aa41671965037d6b6/docs/img/archi-diagram.png -------------------------------------------------------------------------------- /docs/img/rest-ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benjsicam/nestjs-rest-microservices/3ab9494556bc8f7e4fffd07aa41671965037d6b6/docs/img/rest-ui.png -------------------------------------------------------------------------------- /docs/openapi-spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.2", 3 | "info": { 4 | "title": "REST API Gateway", 5 | "description": "REST API Gateway Documentation", 6 | "version": "1.0.0" 7 | }, 8 | "servers": [ 9 | { 10 | "url": "http://localhost:3000", 11 | "description": "Local Server" 12 | } 13 | ], 14 | "paths": { 15 | "/orgs": { 16 | "get": { 17 | "tags": [ 18 | "organizations" 19 | ], 20 | "summary": "Query or search organizations", 21 | "description": "Retrieves a collection of organizations", 22 | "operationId": "findOrganizations", 23 | "parameters": [ 24 | { 25 | "name": "q", 26 | "in": "query", 27 | "description": "The parameter to filter the query results using the name of the organization", 28 | "schema": { 29 | "type": "string", 30 | "default": "", 31 | "nullable": true 32 | }, 33 | "example": "acme" 34 | }, 35 | { 36 | "name": "select", 37 | "in": "query", 38 | "description": "The parameter to control the projection of query results. Can be a comma delimited set of model attributes", 39 | "schema": { 40 | "type": "string", 41 | "default": "", 42 | "nullable": true 43 | }, 44 | "example": "id,name" 45 | }, 46 | { 47 | "name": "orderBy", 48 | "in": "query", 49 | "description": "The parameter to control the sorting for the query results. Can be a comma delimited set of model attributes. Default sort order is ascending. Use the minus (-) sign before the field to change sort order", 50 | "schema": { 51 | "type": "string", 52 | "nullable": true 53 | }, 54 | "example": "name,-createdAt" 55 | }, 56 | { 57 | "name": "page", 58 | "in": "query", 59 | "description": "The pagination parameter to control the page number", 60 | "schema": { 61 | "type": "integer", 62 | "default": 1, 63 | "nullable": true 64 | } 65 | }, 66 | { 67 | "name": "limit", 68 | "in": "query", 69 | "description": "The pagination parameter to control the number of returned records per page", 70 | "schema": { 71 | "type": "integer", 72 | "default": 25, 73 | "nullable": true 74 | } 75 | } 76 | ], 77 | "responses": { 78 | "200": { 79 | "description": "Successfully queried resources", 80 | "content": { 81 | "application/json": { 82 | "schema": { 83 | "$ref": "#/components/responses/OrganizationQueryResponse" 84 | }, 85 | "example": { 86 | "totalRecords": 2, 87 | "totalPages": 1, 88 | "page": 1, 89 | "limit": 25, 90 | "data": [ 91 | { 92 | "id": "7fe5b86f-b30d-40ee-9b8b-8619edf0fb18", 93 | "name": "acme", 94 | "createdAt": "2019-08-05T10:02:18.047Z", 95 | "updatedAt": "2020-01-05T15:22:03.020Z", 96 | "version": 2 97 | }, 98 | { 99 | "id": "6696c3fa-ff8a-4b23-9997-27b434352d32", 100 | "name": "acme2", 101 | "createdAt": "2019-08-05T10:02:18.047Z", 102 | "updatedAt": "2020-01-05T15:22:03.020Z", 103 | "version": 9 104 | } 105 | ] 106 | } 107 | } 108 | } 109 | }, 110 | "400": { 111 | "$ref": "#/components/responses/BadRequestError" 112 | }, 113 | "401": { 114 | "$ref": "#/components/responses/InvalidCredentialsError" 115 | }, 116 | "403": { 117 | "$ref": "#/components/responses/NotAuthorizedError" 118 | }, 119 | "500": { 120 | "$ref": "#/components/responses/InternalError" 121 | } 122 | }, 123 | "security": [ 124 | { 125 | "bearerAuth": [ 126 | 127 | ] 128 | } 129 | ] 130 | } 131 | }, 132 | "/orgs/{name}/comments": { 133 | "get": { 134 | "tags": [ 135 | "organizations" 136 | ], 137 | "summary": "Query or search comments associated to an organization", 138 | "description": "Retrieves a collection of comments associated to an organization", 139 | "operationId": "findOrganizationComments", 140 | "parameters": [ 141 | { 142 | "name": "name", 143 | "in": "path", 144 | "description": "The name of the organization", 145 | "required": true, 146 | "schema": { 147 | "type": "string", 148 | "default": "" 149 | } 150 | }, 151 | { 152 | "name": "q", 153 | "in": "query", 154 | "description": "The parameter to filter the query results using the contents of a comment", 155 | "schema": { 156 | "type": "string", 157 | "default": "", 158 | "nullable": true 159 | }, 160 | "example": "Lorem Ipsum" 161 | }, 162 | { 163 | "name": "select", 164 | "in": "query", 165 | "description": "The parameter to control the projection of query results. Can be a comma delimited set of model attributes", 166 | "schema": { 167 | "type": "string", 168 | "default": "", 169 | "nullable": true 170 | }, 171 | "example": "id,comment" 172 | }, 173 | { 174 | "name": "orderBy", 175 | "in": "query", 176 | "description": "The parameter to control the sorting for the query results. Can be a comma delimited set of model attributes. Default sort order is ascending. Use the minus (-) sign before the field to change sort order", 177 | "schema": { 178 | "type": "string", 179 | "nullable": true 180 | }, 181 | "example": "organization,-createdAt" 182 | }, 183 | { 184 | "name": "page", 185 | "in": "query", 186 | "description": "The pagination parameter to control the page number", 187 | "schema": { 188 | "type": "integer", 189 | "default": 1, 190 | "nullable": true 191 | } 192 | }, 193 | { 194 | "name": "limit", 195 | "in": "query", 196 | "description": "The pagination parameter to control the number of returned records per page", 197 | "schema": { 198 | "type": "integer", 199 | "default": 25, 200 | "nullable": true 201 | } 202 | } 203 | ], 204 | "responses": { 205 | "200": { 206 | "description": "Successfully queried resources", 207 | "content": { 208 | "application/json": { 209 | "schema": { 210 | "$ref": "#/components/responses/CommentQueryResponse" 211 | }, 212 | "example": { 213 | "totalRecords": 2, 214 | "totalPages": 1, 215 | "page": 1, 216 | "limit": 25, 217 | "data": [ 218 | { 219 | "id": "a64b3121-8d27-43f9-9138-2e4121972720", 220 | "organization": "7fe5b86f-b30d-40ee-9b8b-8619edf0fb18", 221 | "comment": "Nulla condimentum ornare nisi auctor euismod. Phasellus vel tincidunt lectus. Nulla porttitor consequat augue, ut molestie mauris ultricies vel.", 222 | "createdAt": "2019-08-05T10:02:18.047Z", 223 | "updatedAt": "2020-01-05T15:22:03.020Z", 224 | "version": 2 225 | }, 226 | { 227 | "id": "9607a6a4-db32-4f6e-b600-78f6b7f67799", 228 | "organization": "7fe5b86f-b30d-40ee-9b8b-8619edf0fb18", 229 | "comment": "Interdum et malesuada fames ac ante ipsum primis in faucibus. Phasellus molestie, arcu in rutrum euismod, leo justo aliquam ante, molestie gravida odio quam nec urna.", 230 | "createdAt": "2019-08-05T10:02:18.047Z", 231 | "updatedAt": "2020-01-05T15:22:03.020Z", 232 | "version": 8 233 | } 234 | ] 235 | } 236 | } 237 | } 238 | }, 239 | "400": { 240 | "$ref": "#/components/responses/BadRequestError" 241 | }, 242 | "401": { 243 | "$ref": "#/components/responses/InvalidCredentialsError" 244 | }, 245 | "403": { 246 | "$ref": "#/components/responses/NotAuthorizedError" 247 | }, 248 | "500": { 249 | "$ref": "#/components/responses/InternalError" 250 | } 251 | }, 252 | "security": [ 253 | { 254 | "bearerAuth": [ 255 | 256 | ] 257 | } 258 | ] 259 | }, 260 | "post": { 261 | "tags": [ 262 | "organizations" 263 | ], 264 | "summary": "Post a new comment for an organization", 265 | "description": "Post a new comment for an organization", 266 | "operationId": "createOrganizationComment", 267 | "parameters": [ 268 | { 269 | "name": "name", 270 | "in": "path", 271 | "description": "The name of the organization", 272 | "required": true, 273 | "schema": { 274 | "type": "string", 275 | "default": "" 276 | }, 277 | "example": "acme" 278 | } 279 | ], 280 | "requestBody": { 281 | "description": "New comment to create", 282 | "content": { 283 | "application/json": { 284 | "schema": { 285 | "type": "object", 286 | "required": [ 287 | "comment" 288 | ], 289 | "properties": { 290 | "comment": { 291 | "type": "string", 292 | "description": "The comment text" 293 | } 294 | } 295 | }, 296 | "example": { 297 | "comment": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas gravida velit nec semper congue. Etiam in orci nec lorem laoreet sodales id eget magna." 298 | } 299 | } 300 | } 301 | }, 302 | "responses": { 303 | "201": { 304 | "description": "Successfully created comment", 305 | "content": { 306 | "application/json": { 307 | "schema": { 308 | "$ref": "#/components/schemas/Comment" 309 | } 310 | } 311 | } 312 | }, 313 | "400": { 314 | "$ref": "#/components/responses/BadRequestError" 315 | }, 316 | "401": { 317 | "$ref": "#/components/responses/InvalidCredentialsError" 318 | }, 319 | "403": { 320 | "$ref": "#/components/responses/NotAuthorizedError" 321 | }, 322 | "409": { 323 | "$ref": "#/components/responses/ConflictError" 324 | }, 325 | "500": { 326 | "$ref": "#/components/responses/InternalError" 327 | } 328 | }, 329 | "security": [ 330 | { 331 | "bearerAuth": [ 332 | 333 | ] 334 | } 335 | ] 336 | }, 337 | "delete": { 338 | "tags": [ 339 | "organizations" 340 | ], 341 | "summary": "Soft deletes all comments associated to an organization", 342 | "description": "Soft deletes all comments associated to an organization", 343 | "operationId": "deleteOrganizationComments", 344 | "parameters": [ 345 | { 346 | "name": "name", 347 | "in": "path", 348 | "description": "The name of the organization", 349 | "required": true, 350 | "schema": { 351 | "type": "string", 352 | "default": "" 353 | }, 354 | "example": "acme" 355 | } 356 | ], 357 | "responses": { 358 | "200": { 359 | "$ref": "#/components/responses/DeleteResponse" 360 | }, 361 | "400": { 362 | "$ref": "#/components/responses/BadRequestError" 363 | }, 364 | "401": { 365 | "$ref": "#/components/responses/InvalidCredentialsError" 366 | }, 367 | "403": { 368 | "$ref": "#/components/responses/NotAuthorizedError" 369 | }, 370 | "404": { 371 | "$ref": "#/components/responses/ResourceNotFoundError" 372 | }, 373 | "500": { 374 | "$ref": "#/components/responses/InternalError" 375 | } 376 | }, 377 | "security": [ 378 | { 379 | "bearerAuth": [ 380 | 381 | ] 382 | } 383 | ] 384 | } 385 | }, 386 | "/orgs/{name}/members": { 387 | "get": { 388 | "tags": [ 389 | "organizations" 390 | ], 391 | "summary": "Query or search the members of the organization", 392 | "description": "Query or search the members of the organization", 393 | "operationId": "findOrganizationMembers", 394 | "parameters": [ 395 | { 396 | "name": "name", 397 | "in": "path", 398 | "description": "The name of the organization", 399 | "required": true, 400 | "schema": { 401 | "type": "string", 402 | "default": "" 403 | }, 404 | "example": "acme" 405 | }, 406 | { 407 | "name": "q", 408 | "in": "query", 409 | "description": "The parameter to filter the query results using the member's name", 410 | "schema": { 411 | "type": "string", 412 | "default": "", 413 | "nullable": true 414 | }, 415 | "example": "user1" 416 | }, 417 | { 418 | "name": "select", 419 | "in": "query", 420 | "description": "The parameter to control the projection of query results. Can be a comma delimited set of model attributes", 421 | "schema": { 422 | "type": "string", 423 | "default": "", 424 | "nullable": true 425 | }, 426 | "example": "id,loginId" 427 | }, 428 | { 429 | "name": "orderBy", 430 | "in": "query", 431 | "description": "The parameter to control the sorting for the query results. Can be a comma delimited set of model attributes. Default sort order is ascending. Use the minus (-) sign before the field to change sort order", 432 | "schema": { 433 | "type": "string", 434 | "nullable": true 435 | }, 436 | "example": "-followers" 437 | }, 438 | { 439 | "name": "page", 440 | "in": "query", 441 | "description": "The pagination parameter to control the page number", 442 | "schema": { 443 | "type": "integer", 444 | "default": 1, 445 | "nullable": true 446 | } 447 | }, 448 | { 449 | "name": "limit", 450 | "in": "query", 451 | "description": "The pagination parameter to control the number of returned records per page", 452 | "schema": { 453 | "type": "integer", 454 | "default": 25, 455 | "nullable": true 456 | } 457 | } 458 | ], 459 | "responses": { 460 | "200": { 461 | "description": "Successfully queried members", 462 | "content": { 463 | "application/json": { 464 | "schema": { 465 | "$ref": "#/components/responses/UserQueryResponse" 466 | }, 467 | "example": { 468 | "totalRecords": 2, 469 | "totalPages": 1, 470 | "page": 1, 471 | "limit": 25, 472 | "data": [ 473 | { 474 | "id": "331fe8f5-7c1c-48d2-8890-b1df8a1853a1", 475 | "organization": "7fe5b86f-b30d-40ee-9b8b-8619edf0fb18", 476 | "loginId": "user1", 477 | "avatar": "https://gravatar.com/avatar/d044a58b427b7d3845dc59e131634a91", 478 | "followers": 2, 479 | "following": 5, 480 | "createdAt": "2019-08-05T10:02:18.047Z", 481 | "updatedAt": "2020-01-05T15:22:03.020Z", 482 | "version": 2 483 | }, 484 | { 485 | "id": "1129438e-0d15-42eb-a7e8-1b7c1ed258c0", 486 | "organization": "7fe5b86f-b30d-40ee-9b8b-8619edf0fb18", 487 | "loginId": "user1", 488 | "avatar": "https://gravatar.com/avatar/3d8fd0ddb69827ce84ff637889ff7cd0", 489 | "followers": 6, 490 | "following": 38, 491 | "createdAt": "2019-08-05T10:02:18.047Z", 492 | "updatedAt": "2020-01-05T15:22:03.020Z", 493 | "version": 7 494 | } 495 | ] 496 | } 497 | } 498 | } 499 | }, 500 | "400": { 501 | "$ref": "#/components/responses/BadRequestError" 502 | }, 503 | "401": { 504 | "$ref": "#/components/responses/InvalidCredentialsError" 505 | }, 506 | "403": { 507 | "$ref": "#/components/responses/NotAuthorizedError" 508 | }, 509 | "500": { 510 | "$ref": "#/components/responses/InternalError" 511 | } 512 | }, 513 | "security": [ 514 | { 515 | "bearerAuth": [ 516 | 517 | ] 518 | } 519 | ] 520 | } 521 | } 522 | }, 523 | "components": { 524 | "schemas": { 525 | "User": { 526 | "type": "object", 527 | "description": "User model", 528 | "properties": { 529 | "id": { 530 | "description": "The identifier for the user record", 531 | "type": "string", 532 | "format": "uuid", 533 | "readOnly": true 534 | }, 535 | "organization": { 536 | "description": "Ref: Organization. The organization the user is associated with.", 537 | "type": "string", 538 | "format": "uuid" 539 | }, 540 | "loginId": { 541 | "description": "The login id of the user", 542 | "type": "string" 543 | }, 544 | "avatar": { 545 | "description": "The avatar url of the user", 546 | "type": "string", 547 | "format": "uri" 548 | }, 549 | "followers": { 550 | "description": "The number of followers of the user.", 551 | "type": "integer", 552 | "format": "int32", 553 | "default": 0 554 | }, 555 | "following": { 556 | "description": "The number of people being followed by the user.", 557 | "type": "integer", 558 | "format": "int32", 559 | "default": 0 560 | }, 561 | "createdAt": { 562 | "description": "The timestamp for when the record has been created. For auditing purposes only", 563 | "type": "string", 564 | "format": "date-time", 565 | "readOnly": true 566 | }, 567 | "updatedAt": { 568 | "description": "The timestamp for when the record has been last updated. For auditing purposes only", 569 | "type": "string", 570 | "format": "date-time", 571 | "readOnly": true 572 | }, 573 | "version": { 574 | "description": "The record's version in the database.", 575 | "type": "integer", 576 | "format": "int32", 577 | "readOnly": true 578 | } 579 | }, 580 | "example": { 581 | "id": "331fe8f5-7c1c-48d2-8890-b1df8a1853a1", 582 | "organization": "7fe5b86f-b30d-40ee-9b8b-8619edf0fb18", 583 | "loginId": "user1", 584 | "avatar": "https://gravatar.com/avatar/d044a58b427b7d3845dc59e131634a91", 585 | "followers": 2, 586 | "following": 5, 587 | "createdAt": "2019-08-05T10:02:18.047Z", 588 | "updatedAt": "2020-01-05T15:22:03.020Z", 589 | "version": 2 590 | } 591 | }, 592 | "Comment": { 593 | "type": "object", 594 | "description": "Comment model", 595 | "required": [ 596 | "comment" 597 | ], 598 | "properties": { 599 | "id": { 600 | "description": "The identifier for the comment record", 601 | "type": "string", 602 | "format": "uuid", 603 | "readOnly": true 604 | }, 605 | "organization": { 606 | "description": "Ref: Organization. The organization the comment is associated with.", 607 | "type": "string", 608 | "format": "uuid" 609 | }, 610 | "comment": { 611 | "description": "The comment text", 612 | "type": "string" 613 | }, 614 | "createdAt": { 615 | "description": "The timestamp for when the record has been created. For auditing purposes only", 616 | "type": "string", 617 | "format": "date-time", 618 | "readOnly": true 619 | }, 620 | "updatedAt": { 621 | "description": "The timestamp for when the record has been last updated. For auditing purposes only", 622 | "type": "string", 623 | "format": "date-time", 624 | "readOnly": true 625 | }, 626 | "version": { 627 | "description": "The record's version in the database.", 628 | "type": "integer", 629 | "format": "int32", 630 | "readOnly": true 631 | } 632 | }, 633 | "example": { 634 | "id": "74d0f515-6b4d-4112-9439-ed10752d0bc9", 635 | "organization": "7fe5b86f-b30d-40ee-9b8b-8619edf0fb18", 636 | "comment": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras vel consequat ipsum, et posuere lorem.", 637 | "createdAt": "2019-08-05T10:02:18.047Z", 638 | "updatedAt": "2020-01-05T15:22:03.020Z", 639 | "version": 2 640 | } 641 | }, 642 | "Organization": { 643 | "type": "object", 644 | "description": "Organization model", 645 | "properties": { 646 | "id": { 647 | "description": "The identifier for the organization record", 648 | "type": "string", 649 | "format": "uuid", 650 | "readOnly": true 651 | }, 652 | "name": { 653 | "description": "The name of the organization", 654 | "type": "string" 655 | }, 656 | "createdAt": { 657 | "description": "The timestamp for when the record has been created. For auditing purposes only", 658 | "type": "string", 659 | "format": "date-time", 660 | "readOnly": true 661 | }, 662 | "updatedAt": { 663 | "description": "The timestamp for when the record has been last updated. For auditing purposes only", 664 | "type": "string", 665 | "format": "date-time", 666 | "readOnly": true 667 | }, 668 | "version": { 669 | "description": "The record's version in the database.", 670 | "type": "integer", 671 | "format": "int32", 672 | "readOnly": true 673 | } 674 | }, 675 | "example": { 676 | "id": "7fe5b86f-b30d-40ee-9b8b-8619edf0fb18", 677 | "name": "acme", 678 | "createdAt": "2019-08-05T10:02:18.047Z", 679 | "updatedAt": "2020-01-05T15:22:03.020Z", 680 | "version": 2 681 | } 682 | } 683 | }, 684 | "responses": { 685 | "UserQueryResponse": { 686 | "description": "Successfully queried user", 687 | "content": { 688 | "application/json": { 689 | "schema": { 690 | "allOf": [ 691 | { 692 | "$ref": "#/components/responses/QueryResponse" 693 | }, 694 | { 695 | "type": "object", 696 | "properties": { 697 | "data": { 698 | "type": "array", 699 | "items": { 700 | "$ref": "#/components/schemas/User" 701 | } 702 | } 703 | } 704 | } 705 | ] 706 | } 707 | } 708 | } 709 | }, 710 | "CommentQueryResponse": { 711 | "description": "Successfully queried comments", 712 | "content": { 713 | "application/json": { 714 | "schema": { 715 | "allOf": [ 716 | { 717 | "$ref": "#/components/responses/QueryResponse" 718 | }, 719 | { 720 | "type": "object", 721 | "properties": { 722 | "data": { 723 | "type": "array", 724 | "items": { 725 | "$ref": "#/components/schemas/Comment" 726 | } 727 | } 728 | } 729 | } 730 | ] 731 | } 732 | } 733 | } 734 | }, 735 | "OrganizationQueryResponse": { 736 | "description": "Successfully queried organizations", 737 | "content": { 738 | "application/json": { 739 | "schema": { 740 | "allOf": [ 741 | { 742 | "$ref": "#/components/responses/QueryResponse" 743 | }, 744 | { 745 | "type": "object", 746 | "properties": { 747 | "data": { 748 | "type": "array", 749 | "items": { 750 | "$ref": "#/components/schemas/Organization" 751 | } 752 | } 753 | } 754 | } 755 | ] 756 | } 757 | } 758 | } 759 | }, 760 | "QueryResponse": { 761 | "description": "Successfully queried resources", 762 | "content": { 763 | "application/json": { 764 | "schema": { 765 | "type": "object", 766 | "description": "Generic response schema for queries or search", 767 | "properties": { 768 | "totalRecords": { 769 | "description": "The total number of records or records returned by the query", 770 | "type": "integer", 771 | "default": 0, 772 | "readOnly": true 773 | }, 774 | "totalPages": { 775 | "description": "The total number of pages returned by the query", 776 | "type": "integer", 777 | "default": 0, 778 | "readOnly": true 779 | }, 780 | "page": { 781 | "description": "The current page when navigating the query results", 782 | "type": "integer", 783 | "default": 1, 784 | "readOnly": true 785 | }, 786 | "limit": { 787 | "description": "The limit, size or number of records returned per page by the query", 788 | "type": "integer", 789 | "default": 25, 790 | "readOnly": true 791 | }, 792 | "data": { 793 | "type": "array", 794 | "items": { 795 | "default": [ 796 | 797 | ] 798 | } 799 | } 800 | } 801 | } 802 | } 803 | } 804 | }, 805 | "GenericResponse": { 806 | "description": "Generic response", 807 | "content": { 808 | "application/json": { 809 | "schema": { 810 | "type": "object", 811 | "description": "Generic response message", 812 | "required": [ 813 | "message" 814 | ], 815 | "properties": { 816 | "message": { 817 | "description": "A descriptive message for the response", 818 | "type": "string" 819 | } 820 | } 821 | }, 822 | "example": { 823 | "message": "Generic message" 824 | } 825 | } 826 | } 827 | }, 828 | "DeleteResponse": { 829 | "description": "Successfully deleted resource/s", 830 | "content": { 831 | "application/json": { 832 | "schema": { 833 | "type": "object", 834 | "properties": { 835 | "count": { 836 | "description": "The number of records deleted from the database", 837 | "type": "integer", 838 | "format": "int32" 839 | } 840 | } 841 | }, 842 | "example": { 843 | "count": 8 844 | } 845 | } 846 | } 847 | }, 848 | "ErrorResponse": { 849 | "description": "Error response", 850 | "content": { 851 | "application/json": { 852 | "schema": { 853 | "type": "object", 854 | "properties": { 855 | "code": { 856 | "description": "The error or response code", 857 | "type": "integer", 858 | "format": "int32", 859 | "readOnly": true 860 | }, 861 | "type": { 862 | "description": "The error name or type", 863 | "type": "string", 864 | "readOnly": true 865 | }, 866 | "message": { 867 | "description": "A high level descriptive message for the error", 868 | "type": "string", 869 | "readOnly": true 870 | }, 871 | "data": { 872 | "description": "Detailed description of the errors in the request", 873 | "type": "array", 874 | "readOnly": true, 875 | "items": { 876 | "type": "object", 877 | "properties": { 878 | "type": { 879 | "description": "The error name or type", 880 | "type": "string", 881 | "default": "" 882 | }, 883 | "field": { 884 | "description": "The name of the field with the error", 885 | "type": "string", 886 | "default": "" 887 | }, 888 | "message": { 889 | "description": "The detailed error message", 890 | "type": "string", 891 | "default": "" 892 | } 893 | } 894 | } 895 | } 896 | } 897 | }, 898 | "example": { 899 | "code": 404, 900 | "type": "NOT_FOUND", 901 | "message": "Resource not found", 902 | "data": [ 903 | 904 | ] 905 | } 906 | } 907 | } 908 | }, 909 | "BadRequestError": { 910 | "description": "Invalid input or validation error", 911 | "content": { 912 | "application/json": { 913 | "schema": { 914 | "$ref": "#/components/responses/ErrorResponse" 915 | }, 916 | "example": { 917 | "code": 400, 918 | "type": "VALIDATION_ERROR", 919 | "message": "Validation has failed", 920 | "data": [ 921 | { 922 | "type": "VALIDATION_ERROR", 923 | "field": "url", 924 | "message": "URL must be a valid URL" 925 | } 926 | ] 927 | } 928 | } 929 | } 930 | }, 931 | "ConflictError": { 932 | "description": "Unique constraint validation error", 933 | "content": { 934 | "application/json": { 935 | "schema": { 936 | "$ref": "#/components/responses/ErrorResponse" 937 | }, 938 | "example": { 939 | "code": 409, 940 | "type": "CONFLICT_ERROR", 941 | "message": "A record with the same name exists", 942 | "data": [ 943 | 944 | ] 945 | } 946 | } 947 | } 948 | }, 949 | "InvalidCredentialsError": { 950 | "description": "Authentication has failed", 951 | "content": { 952 | "application/json": { 953 | "schema": { 954 | "$ref": "#/components/responses/ErrorResponse" 955 | }, 956 | "example": { 957 | "code": 401, 958 | "type": "AUTHENTICATION_REQUIRED", 959 | "message": "Authentication is required", 960 | "data": [ 961 | 962 | ] 963 | } 964 | } 965 | } 966 | }, 967 | "NotAuthorizedError": { 968 | "description": "Method is not allowed or access has been denied", 969 | "content": { 970 | "application/json": { 971 | "schema": { 972 | "$ref": "#/components/responses/ErrorResponse" 973 | }, 974 | "example": { 975 | "code": 403, 976 | "type": "FORBIDDEN", 977 | "message": "Access denied", 978 | "data": [ 979 | 980 | ] 981 | } 982 | } 983 | } 984 | }, 985 | "ResourceNotFoundError": { 986 | "description": "Resource not found error", 987 | "content": { 988 | "application/json": { 989 | "schema": { 990 | "$ref": "#/components/responses/ErrorResponse" 991 | }, 992 | "example": { 993 | "code": 404, 994 | "type": "NOT_FOUND", 995 | "message": "Resource not found", 996 | "data": [ 997 | 998 | ] 999 | } 1000 | } 1001 | } 1002 | }, 1003 | "InternalError": { 1004 | "description": "Unexpected Error", 1005 | "content": { 1006 | "application/json": { 1007 | "schema": { 1008 | "$ref": "#/components/responses/ErrorResponse" 1009 | }, 1010 | "example": { 1011 | "code": 500, 1012 | "type": "INTERNAL_SERVER_ERROR", 1013 | "message": "An unexpected error has occurred", 1014 | "data": [ 1015 | 1016 | ] 1017 | } 1018 | } 1019 | } 1020 | } 1021 | }, 1022 | "securitySchemes": { 1023 | "bearerAuth": { 1024 | "type": "http", 1025 | "scheme": "bearer", 1026 | "bearerFormat": "JWT" 1027 | } 1028 | } 1029 | } 1030 | } 1031 | -------------------------------------------------------------------------------- /docs/openapi-spec.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.2" 2 | info: 3 | title: "REST API Gateway" 4 | description: "REST API Gateway Documentation" 5 | version: "1.0.0" 6 | servers: 7 | - url: "http://localhost:3000" 8 | description: "Local Server" 9 | paths: 10 | /orgs: 11 | get: 12 | tags: 13 | - "organizations" 14 | summary: "Query or search organizations" 15 | description: "Retrieves a collection of organizations" 16 | operationId: "findOrganizations" 17 | parameters: 18 | - name: "q" 19 | in: "query" 20 | description: "The parameter to filter the query results using the name of the organization" 21 | schema: 22 | type: "string" 23 | default: "" 24 | nullable: true 25 | example: "acme" 26 | - name: "select" 27 | in: "query" 28 | description: "The parameter to control the projection of query results. Can be a comma delimited set of model attributes" 29 | schema: 30 | type: "string" 31 | default: "" 32 | nullable: true 33 | example: "id,name" 34 | - name: "orderBy" 35 | in: "query" 36 | description: "The parameter to control the sorting for the query results. Can be a comma delimited set of model attributes. Default sort order is ascending. Use the minus (-) sign before the field to change sort order" 37 | schema: 38 | type: "string" 39 | nullable: true 40 | example: "name,-createdAt" 41 | - name: "page" 42 | in: "query" 43 | description: "The pagination parameter to control the page number" 44 | schema: 45 | type: "integer" 46 | default: 1 47 | nullable: true 48 | - name: "limit" 49 | in: "query" 50 | description: "The pagination parameter to control the number of returned records per page" 51 | schema: 52 | type: "integer" 53 | default: 25 54 | nullable: true 55 | responses: 56 | 200: 57 | description: "Successfully queried resources" 58 | content: 59 | application/json: 60 | schema: 61 | $ref: "#/components/responses/OrganizationQueryResponse" 62 | example: 63 | totalRecords: 2 64 | totalPages: 1 65 | page: 1 66 | limit: 25 67 | data: 68 | - id: "7fe5b86f-b30d-40ee-9b8b-8619edf0fb18" 69 | name: "acme" 70 | createdAt: "2019-08-05T10:02:18.047Z" 71 | updatedAt: "2020-01-05T15:22:03.020Z" 72 | version: 2 73 | - id: "6696c3fa-ff8a-4b23-9997-27b434352d32" 74 | name: "acme2" 75 | createdAt: "2019-08-05T10:02:18.047Z" 76 | updatedAt: "2020-01-05T15:22:03.020Z" 77 | version: 9 78 | 400: 79 | $ref: "#/components/responses/BadRequestError" 80 | 401: 81 | $ref: "#/components/responses/InvalidCredentialsError" 82 | 403: 83 | $ref: "#/components/responses/NotAuthorizedError" 84 | 500: 85 | $ref: "#/components/responses/InternalError" 86 | security: 87 | - bearerAuth: [] 88 | /orgs/{name}/comments: 89 | get: 90 | tags: 91 | - "organizations" 92 | summary: "Query or search comments associated to an organization" 93 | description: "Retrieves a collection of comments associated to an organization" 94 | operationId: "findOrganizationComments" 95 | parameters: 96 | - name: "name" 97 | in: "path" 98 | description: "The name of the organization" 99 | required: true 100 | schema: 101 | type: "string" 102 | default: "" 103 | - name: "q" 104 | in: "query" 105 | description: "The parameter to filter the query results using the contents of a comment" 106 | schema: 107 | type: "string" 108 | default: "" 109 | nullable: true 110 | example: "Lorem Ipsum" 111 | - name: "select" 112 | in: "query" 113 | description: "The parameter to control the projection of query results. Can be a comma delimited set of model attributes" 114 | schema: 115 | type: "string" 116 | default: "" 117 | nullable: true 118 | example: "id,comment" 119 | - name: "orderBy" 120 | in: "query" 121 | description: "The parameter to control the sorting for the query results. Can be a comma delimited set of model attributes. Default sort order is ascending. Use the minus (-) sign before the field to change sort order" 122 | schema: 123 | type: "string" 124 | nullable: true 125 | example: "organization,-createdAt" 126 | - name: "page" 127 | in: "query" 128 | description: "The pagination parameter to control the page number" 129 | schema: 130 | type: "integer" 131 | default: 1 132 | nullable: true 133 | - name: "limit" 134 | in: "query" 135 | description: "The pagination parameter to control the number of returned records per page" 136 | schema: 137 | type: "integer" 138 | default: 25 139 | nullable: true 140 | responses: 141 | 200: 142 | description: "Successfully queried resources" 143 | content: 144 | application/json: 145 | schema: 146 | $ref: "#/components/responses/CommentQueryResponse" 147 | example: 148 | totalRecords: 2 149 | totalPages: 1 150 | page: 1 151 | limit: 25 152 | data: 153 | - id: "a64b3121-8d27-43f9-9138-2e4121972720" 154 | organization: "7fe5b86f-b30d-40ee-9b8b-8619edf0fb18" 155 | comment: "Nulla condimentum ornare nisi auctor euismod. Phasellus vel tincidunt lectus. Nulla porttitor consequat augue, ut molestie mauris ultricies vel." 156 | createdAt: "2019-08-05T10:02:18.047Z" 157 | updatedAt: "2020-01-05T15:22:03.020Z" 158 | version: 2 159 | - id: "9607a6a4-db32-4f6e-b600-78f6b7f67799" 160 | organization: "7fe5b86f-b30d-40ee-9b8b-8619edf0fb18" 161 | comment: "Interdum et malesuada fames ac ante ipsum primis in faucibus. Phasellus molestie, arcu in rutrum euismod, leo justo aliquam ante, molestie gravida odio quam nec urna." 162 | createdAt: "2019-08-05T10:02:18.047Z" 163 | updatedAt: "2020-01-05T15:22:03.020Z" 164 | version: 8 165 | 400: 166 | $ref: "#/components/responses/BadRequestError" 167 | 401: 168 | $ref: "#/components/responses/InvalidCredentialsError" 169 | 403: 170 | $ref: "#/components/responses/NotAuthorizedError" 171 | 500: 172 | $ref: "#/components/responses/InternalError" 173 | security: 174 | - bearerAuth: [] 175 | post: 176 | tags: 177 | - "organizations" 178 | summary: "Post a new comment for an organization" 179 | description: "Post a new comment for an organization" 180 | operationId: "createOrganizationComment" 181 | parameters: 182 | - name: "name" 183 | in: "path" 184 | description: "The name of the organization" 185 | required: true 186 | schema: 187 | type: "string" 188 | default: "" 189 | example: "acme" 190 | requestBody: 191 | description: "New comment to create" 192 | content: 193 | application/json: 194 | schema: 195 | type: "object" 196 | required: 197 | - "comment" 198 | properties: 199 | comment: 200 | type: "string" 201 | description: "The comment text" 202 | example: 203 | comment: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas gravida velit nec semper congue. Etiam in orci nec lorem laoreet sodales id eget magna." 204 | responses: 205 | 201: 206 | description: "Successfully created comment" 207 | content: 208 | application/json: 209 | schema: 210 | $ref: "#/components/schemas/Comment" 211 | 400: 212 | $ref: "#/components/responses/BadRequestError" 213 | 401: 214 | $ref: "#/components/responses/InvalidCredentialsError" 215 | 403: 216 | $ref: "#/components/responses/NotAuthorizedError" 217 | 409: 218 | $ref: "#/components/responses/ConflictError" 219 | 500: 220 | $ref: "#/components/responses/InternalError" 221 | security: 222 | - bearerAuth: [] 223 | delete: 224 | tags: 225 | - "organizations" 226 | summary: "Soft deletes all comments associated to an organization" 227 | description: "Soft deletes all comments associated to an organization" 228 | operationId: "deleteOrganizationComments" 229 | parameters: 230 | - name: "name" 231 | in: "path" 232 | description: "The name of the organization" 233 | required: true 234 | schema: 235 | type: "string" 236 | default: "" 237 | example: "acme" 238 | responses: 239 | 200: 240 | $ref: "#/components/responses/DeleteResponse" 241 | 400: 242 | $ref: "#/components/responses/BadRequestError" 243 | 401: 244 | $ref: "#/components/responses/InvalidCredentialsError" 245 | 403: 246 | $ref: "#/components/responses/NotAuthorizedError" 247 | 404: 248 | $ref: "#/components/responses/ResourceNotFoundError" 249 | 500: 250 | $ref: "#/components/responses/InternalError" 251 | security: 252 | - bearerAuth: [] 253 | /orgs/{name}/members: 254 | get: 255 | tags: 256 | - "organizations" 257 | summary: "Query or search the members of the organization" 258 | description: "Query or search the members of the organization" 259 | operationId: "findOrganizationMembers" 260 | parameters: 261 | - name: "name" 262 | in: "path" 263 | description: "The name of the organization" 264 | required: true 265 | schema: 266 | type: "string" 267 | default: "" 268 | example: "acme" 269 | - name: "q" 270 | in: "query" 271 | description: "The parameter to filter the query results using the member's name" 272 | schema: 273 | type: "string" 274 | default: "" 275 | nullable: true 276 | example: "user1" 277 | - name: "select" 278 | in: "query" 279 | description: "The parameter to control the projection of query results. Can be a comma delimited set of model attributes" 280 | schema: 281 | type: "string" 282 | default: "" 283 | nullable: true 284 | example: "id,loginId" 285 | - name: "orderBy" 286 | in: "query" 287 | description: "The parameter to control the sorting for the query results. Can be a comma delimited set of model attributes. Default sort order is ascending. Use the minus (-) sign before the field to change sort order" 288 | schema: 289 | type: "string" 290 | nullable: true 291 | example: "-followers" 292 | - name: "page" 293 | in: "query" 294 | description: "The pagination parameter to control the page number" 295 | schema: 296 | type: "integer" 297 | default: 1 298 | nullable: true 299 | - name: "limit" 300 | in: "query" 301 | description: "The pagination parameter to control the number of returned records per page" 302 | schema: 303 | type: "integer" 304 | default: 25 305 | nullable: true 306 | responses: 307 | 200: 308 | description: "Successfully queried members" 309 | content: 310 | application/json: 311 | schema: 312 | $ref: "#/components/responses/UserQueryResponse" 313 | example: 314 | totalRecords: 2 315 | totalPages: 1 316 | page: 1 317 | limit: 25 318 | data: 319 | - id: "331fe8f5-7c1c-48d2-8890-b1df8a1853a1" 320 | organization: "7fe5b86f-b30d-40ee-9b8b-8619edf0fb18" 321 | loginId: "user1" 322 | avatar: "https://gravatar.com/avatar/d044a58b427b7d3845dc59e131634a91" 323 | followers: 2 324 | following: 5 325 | createdAt: "2019-08-05T10:02:18.047Z" 326 | updatedAt: "2020-01-05T15:22:03.020Z" 327 | version: 2 328 | - id: "1129438e-0d15-42eb-a7e8-1b7c1ed258c0" 329 | organization: "7fe5b86f-b30d-40ee-9b8b-8619edf0fb18" 330 | loginId: "user1" 331 | avatar: "https://gravatar.com/avatar/3d8fd0ddb69827ce84ff637889ff7cd0" 332 | followers: 6 333 | following: 38 334 | createdAt: "2019-08-05T10:02:18.047Z" 335 | updatedAt: "2020-01-05T15:22:03.020Z" 336 | version: 7 337 | 400: 338 | $ref: "#/components/responses/BadRequestError" 339 | 401: 340 | $ref: "#/components/responses/InvalidCredentialsError" 341 | 403: 342 | $ref: "#/components/responses/NotAuthorizedError" 343 | 500: 344 | $ref: "#/components/responses/InternalError" 345 | security: 346 | - bearerAuth: [] 347 | components: 348 | schemas: 349 | User: 350 | type: "object" 351 | description: "User model" 352 | properties: 353 | id: 354 | description: "The identifier for the user record" 355 | type: "string" 356 | format: "uuid" 357 | readOnly: true 358 | organization: 359 | description: "Ref: Organization. The organization the user is associated with." 360 | type: "string" 361 | format: "uuid" 362 | loginId: 363 | description: "The login id of the user" 364 | type: "string" 365 | avatar: 366 | description: "The avatar url of the user" 367 | type: "string" 368 | format: "uri" 369 | followers: 370 | description: "The number of followers of the user." 371 | type: "integer" 372 | format: "int32" 373 | default: 0 374 | following: 375 | description: "The number of people being followed by the user." 376 | type: "integer" 377 | format: "int32" 378 | default: 0 379 | createdAt: 380 | description: "The timestamp for when the record has been created. For auditing purposes only" 381 | type: "string" 382 | format: "date-time" 383 | readOnly: true 384 | updatedAt: 385 | description: "The timestamp for when the record has been last updated. For auditing purposes only" 386 | type: "string" 387 | format: "date-time" 388 | readOnly: true 389 | version: 390 | description: "The record's version in the database." 391 | type: "integer" 392 | format: "int32" 393 | readOnly: true 394 | example: 395 | id: "331fe8f5-7c1c-48d2-8890-b1df8a1853a1" 396 | organization: "7fe5b86f-b30d-40ee-9b8b-8619edf0fb18" 397 | loginId: "user1" 398 | avatar: "https://gravatar.com/avatar/d044a58b427b7d3845dc59e131634a91" 399 | followers: 2 400 | following: 5 401 | createdAt: "2019-08-05T10:02:18.047Z" 402 | updatedAt: "2020-01-05T15:22:03.020Z" 403 | version: 2 404 | Comment: 405 | type: "object" 406 | description: "Comment model" 407 | required: 408 | - "comment" 409 | properties: 410 | id: 411 | description: "The identifier for the comment record" 412 | type: "string" 413 | format: "uuid" 414 | readOnly: true 415 | organization: 416 | description: "Ref: Organization. The organization the comment is associated with." 417 | type: "string" 418 | format: "uuid" 419 | comment: 420 | description: "The comment text" 421 | type: "string" 422 | createdAt: 423 | description: "The timestamp for when the record has been created. For auditing purposes only" 424 | type: "string" 425 | format: "date-time" 426 | readOnly: true 427 | updatedAt: 428 | description: "The timestamp for when the record has been last updated. For auditing purposes only" 429 | type: "string" 430 | format: "date-time" 431 | readOnly: true 432 | version: 433 | description: "The record's version in the database." 434 | type: "integer" 435 | format: "int32" 436 | readOnly: true 437 | example: 438 | id: "74d0f515-6b4d-4112-9439-ed10752d0bc9" 439 | organization: "7fe5b86f-b30d-40ee-9b8b-8619edf0fb18" 440 | comment: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras vel consequat ipsum, et posuere lorem." 441 | createdAt: "2019-08-05T10:02:18.047Z" 442 | updatedAt: "2020-01-05T15:22:03.020Z" 443 | version: 2 444 | Organization: 445 | type: "object" 446 | description: "Organization model" 447 | properties: 448 | id: 449 | description: "The identifier for the organization record" 450 | type: "string" 451 | format: "uuid" 452 | readOnly: true 453 | name: 454 | description: "The name of the organization" 455 | type: "string" 456 | createdAt: 457 | description: "The timestamp for when the record has been created. For auditing purposes only" 458 | type: "string" 459 | format: "date-time" 460 | readOnly: true 461 | updatedAt: 462 | description: "The timestamp for when the record has been last updated. For auditing purposes only" 463 | type: "string" 464 | format: "date-time" 465 | readOnly: true 466 | version: 467 | description: "The record's version in the database." 468 | type: "integer" 469 | format: "int32" 470 | readOnly: true 471 | example: 472 | id: "7fe5b86f-b30d-40ee-9b8b-8619edf0fb18" 473 | name: "acme" 474 | createdAt: "2019-08-05T10:02:18.047Z" 475 | updatedAt: "2020-01-05T15:22:03.020Z" 476 | version: 2 477 | responses: 478 | UserQueryResponse: 479 | description: "Successfully queried user" 480 | content: 481 | application/json: 482 | schema: 483 | allOf: 484 | - $ref: "#/components/responses/QueryResponse" 485 | - type: "object" 486 | properties: 487 | data: 488 | type: "array" 489 | items: 490 | $ref: "#/components/schemas/User" 491 | CommentQueryResponse: 492 | description: "Successfully queried comments" 493 | content: 494 | application/json: 495 | schema: 496 | allOf: 497 | - $ref: "#/components/responses/QueryResponse" 498 | - type: "object" 499 | properties: 500 | data: 501 | type: "array" 502 | items: 503 | $ref: "#/components/schemas/Comment" 504 | OrganizationQueryResponse: 505 | description: "Successfully queried organizations" 506 | content: 507 | application/json: 508 | schema: 509 | allOf: 510 | - $ref: "#/components/responses/QueryResponse" 511 | - type: "object" 512 | properties: 513 | data: 514 | type: "array" 515 | items: 516 | $ref: "#/components/schemas/Organization" 517 | QueryResponse: 518 | description: "Successfully queried resources" 519 | content: 520 | application/json: 521 | schema: 522 | type: "object" 523 | description: "Generic response schema for queries or search" 524 | properties: 525 | totalRecords: 526 | description: "The total number of records or records returned by the query" 527 | type: "integer" 528 | default: 0 529 | readOnly: true 530 | totalPages: 531 | description: "The total number of pages returned by the query" 532 | type: "integer" 533 | default: 0 534 | readOnly: true 535 | page: 536 | description: "The current page when navigating the query results" 537 | type: "integer" 538 | default: 1 539 | readOnly: true 540 | limit: 541 | description: "The limit, size or number of records returned per page by the query" 542 | type: "integer" 543 | default: 25 544 | readOnly: true 545 | data: 546 | type: "array" 547 | items: 548 | default: [] 549 | GenericResponse: 550 | description: "Generic response" 551 | content: 552 | application/json: 553 | schema: 554 | type: "object" 555 | description: "Generic response message" 556 | required: 557 | - "message" 558 | properties: 559 | message: 560 | description: "A descriptive message for the response" 561 | type: "string" 562 | example: 563 | message: "Generic message" 564 | DeleteResponse: 565 | description: "Successfully deleted resource/s" 566 | content: 567 | application/json: 568 | schema: 569 | type: "object" 570 | properties: 571 | count: 572 | description: "The number of records deleted from the database" 573 | type: "integer" 574 | format: "int32" 575 | example: 576 | count: 8 577 | ErrorResponse: 578 | description: "Error response" 579 | content: 580 | application/json: 581 | schema: 582 | type: "object" 583 | properties: 584 | code: 585 | description: "The error or response code" 586 | type: "integer" 587 | format: "int32" 588 | readOnly: true 589 | type: 590 | description: "The error name or type" 591 | type: "string" 592 | readOnly: true 593 | message: 594 | description: "A high level descriptive message for the error" 595 | type: "string" 596 | readOnly: true 597 | data: 598 | description: "Detailed description of the errors in the request" 599 | type: "array" 600 | readOnly: true 601 | items: 602 | type: "object" 603 | properties: 604 | type: 605 | description: "The error name or type" 606 | type: "string" 607 | default: "" 608 | field: 609 | description: "The name of the field with the error" 610 | type: "string" 611 | default: "" 612 | message: 613 | description: "The detailed error message" 614 | type: "string" 615 | default: "" 616 | example: 617 | code: 404 618 | type: "NOT_FOUND" 619 | message: "Resource not found" 620 | data: [] 621 | BadRequestError: 622 | description: "Invalid input or validation error" 623 | content: 624 | application/json: 625 | schema: 626 | $ref: "#/components/responses/ErrorResponse" 627 | example: 628 | code: 400 629 | type: "VALIDATION_ERROR" 630 | message: "Validation has failed" 631 | data: 632 | - type: "VALIDATION_ERROR" 633 | field: "url" 634 | message: "URL must be a valid URL" 635 | ConflictError: 636 | description: "Unique constraint validation error" 637 | content: 638 | application/json: 639 | schema: 640 | $ref: "#/components/responses/ErrorResponse" 641 | example: 642 | code: 409 643 | type: "CONFLICT_ERROR" 644 | message: "A record with the same name exists" 645 | data: [] 646 | InvalidCredentialsError: 647 | description: "Authentication has failed" 648 | content: 649 | application/json: 650 | schema: 651 | $ref: "#/components/responses/ErrorResponse" 652 | example: 653 | code: 401 654 | type: "AUTHENTICATION_REQUIRED" 655 | message: "Authentication is required" 656 | data: [] 657 | NotAuthorizedError: 658 | description: "Method is not allowed or access has been denied" 659 | content: 660 | application/json: 661 | schema: 662 | $ref: "#/components/responses/ErrorResponse" 663 | example: 664 | code: 403 665 | type: "FORBIDDEN" 666 | message: "Access denied" 667 | data: [] 668 | ResourceNotFoundError: 669 | description: "Resource not found error" 670 | content: 671 | application/json: 672 | schema: 673 | $ref: "#/components/responses/ErrorResponse" 674 | example: 675 | code: 404 676 | type: "NOT_FOUND" 677 | message: "Resource not found" 678 | data: [] 679 | InternalError: 680 | description: "Unexpected Error" 681 | content: 682 | application/json: 683 | schema: 684 | $ref: "#/components/responses/ErrorResponse" 685 | example: 686 | code: 500 687 | type: "INTERNAL_SERVER_ERROR" 688 | message: "An unexpected error has occurred" 689 | data: [] 690 | securitySchemes: 691 | bearerAuth: 692 | type: "http" 693 | scheme: "bearer" 694 | bearerFormat: "JWT" 695 | -------------------------------------------------------------------------------- /docs/proto-docs.md: -------------------------------------------------------------------------------- 1 | # Protocol Documentation 2 | 3 | 4 | ## Table of Contents 5 | 6 | - [comments.proto](#comments.proto) 7 | - [Comment](#comments.Comment) 8 | - [CommentsList](#comments.CommentsList) 9 | - [CreateCommentInput](#comments.CreateCommentInput) 10 | 11 | 12 | 13 | - [CommentsService](#comments.CommentsService) 14 | 15 | 16 | - [commons.proto](#commons.proto) 17 | - [Count](#commons.Count) 18 | - [Id](#commons.Id) 19 | - [Name](#commons.Name) 20 | - [Query](#commons.Query) 21 | 22 | 23 | 24 | 25 | 26 | - [organizations.proto](#organizations.proto) 27 | - [Organization](#organizations.Organization) 28 | - [OrganizationsList](#organizations.OrganizationsList) 29 | 30 | 31 | 32 | - [OrganizationsService](#organizations.OrganizationsService) 33 | 34 | 35 | - [users.proto](#users.proto) 36 | - [User](#users.User) 37 | - [UsersList](#users.UsersList) 38 | 39 | 40 | 41 | - [UsersService](#users.UsersService) 42 | 43 | 44 | - [Scalar Value Types](#scalar-value-types) 45 | 46 | 47 | 48 | 49 |

Top

50 | 51 | ## comments.proto 52 | 53 | 54 | 55 | 56 | 57 | ### Comment 58 | 59 | 60 | 61 | | Field | Type | Label | Description | 62 | | ----- | ---- | ----- | ----------- | 63 | | id | [string](#string) | | | 64 | | organization | [string](#string) | | | 65 | | comment | [string](#string) | | | 66 | | createdAt | [string](#string) | | | 67 | | updatedAt | [string](#string) | | | 68 | | version | [int32](#int32) | | | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | ### CommentsList 78 | 79 | 80 | 81 | | Field | Type | Label | Description | 82 | | ----- | ---- | ----- | ----------- | 83 | | data | [Comment](#comments.Comment) | repeated | | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | ### CreateCommentInput 93 | 94 | 95 | 96 | | Field | Type | Label | Description | 97 | | ----- | ---- | ----- | ----------- | 98 | | organization | [string](#string) | | | 99 | | comment | [string](#string) | | | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | ### CommentsService 115 | 116 | 117 | | Method Name | Request Type | Response Type | Description | 118 | | ----------- | ------------ | ------------- | ------------| 119 | | findAll | [.commons.Query](#commons.Query) | [CommentsList](#comments.CommentsList) | | 120 | | count | [.commons.Query](#commons.Query) | [.commons.Count](#commons.Count) | | 121 | | create | [CreateCommentInput](#comments.CreateCommentInput) | [Comment](#comments.Comment) | | 122 | | destroy | [.commons.Query](#commons.Query) | [.commons.Count](#commons.Count) | | 123 | 124 | 125 | 126 | 127 | 128 | 129 |

Top

130 | 131 | ## commons.proto 132 | 133 | 134 | 135 | 136 | 137 | ### Count 138 | 139 | 140 | 141 | | Field | Type | Label | Description | 142 | | ----- | ---- | ----- | ----------- | 143 | | count | [int32](#int32) | | | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | ### Id 153 | 154 | 155 | 156 | | Field | Type | Label | Description | 157 | | ----- | ---- | ----- | ----------- | 158 | | id | [string](#string) | | | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | ### Name 168 | 169 | 170 | 171 | | Field | Type | Label | Description | 172 | | ----- | ---- | ----- | ----------- | 173 | | name | [string](#string) | | | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | ### Query 183 | 184 | 185 | 186 | | Field | Type | Label | Description | 187 | | ----- | ---- | ----- | ----------- | 188 | | attributes | [string](#string) | repeated | | 189 | | where | [string](#string) | | | 190 | | order | [string](#string) | | | 191 | | offset | [int32](#int32) | | | 192 | | limit | [int32](#int32) | | | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 |

Top

210 | 211 | ## organizations.proto 212 | 213 | 214 | 215 | 216 | 217 | ### Organization 218 | 219 | 220 | 221 | | Field | Type | Label | Description | 222 | | ----- | ---- | ----- | ----------- | 223 | | id | [string](#string) | | | 224 | | name | [string](#string) | | | 225 | | createdAt | [string](#string) | | | 226 | | updatedAt | [string](#string) | | | 227 | | version | [int32](#int32) | | | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | ### OrganizationsList 237 | 238 | 239 | 240 | | Field | Type | Label | Description | 241 | | ----- | ---- | ----- | ----------- | 242 | | data | [Organization](#organizations.Organization) | repeated | | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | ### OrganizationsService 258 | 259 | 260 | | Method Name | Request Type | Response Type | Description | 261 | | ----------- | ------------ | ------------- | ------------| 262 | | findAll | [.commons.Query](#commons.Query) | [OrganizationsList](#organizations.OrganizationsList) | | 263 | | findByName | [.commons.Name](#commons.Name) | [Organization](#organizations.Organization) | | 264 | | count | [.commons.Query](#commons.Query) | [.commons.Count](#commons.Count) | | 265 | 266 | 267 | 268 | 269 | 270 | 271 |

Top

272 | 273 | ## users.proto 274 | 275 | 276 | 277 | 278 | 279 | ### User 280 | 281 | 282 | 283 | | Field | Type | Label | Description | 284 | | ----- | ---- | ----- | ----------- | 285 | | id | [string](#string) | | | 286 | | organization | [string](#string) | | | 287 | | loginId | [string](#string) | | | 288 | | avatar | [string](#string) | | | 289 | | followers | [int32](#int32) | | | 290 | | following | [int32](#int32) | | | 291 | | createdAt | [string](#string) | | | 292 | | updatedAt | [string](#string) | | | 293 | | version | [int32](#int32) | | | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | ### UsersList 303 | 304 | 305 | 306 | | Field | Type | Label | Description | 307 | | ----- | ---- | ----- | ----------- | 308 | | data | [User](#users.User) | repeated | | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | ### UsersService 324 | 325 | 326 | | Method Name | Request Type | Response Type | Description | 327 | | ----------- | ------------ | ------------- | ------------| 328 | | findAll | [.commons.Query](#commons.Query) | [UsersList](#users.UsersList) | | 329 | | count | [.commons.Query](#commons.Query) | [.commons.Count](#commons.Count) | | 330 | 331 | 332 | 333 | 334 | 335 | ## Scalar Value Types 336 | 337 | | .proto Type | Notes | C++ Type | Java Type | Python Type | 338 | | ----------- | ----- | -------- | --------- | ----------- | 339 | | double | | double | double | float | 340 | | float | | float | float | float | 341 | | int32 | Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint32 instead. | int32 | int | int | 342 | | int64 | Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint64 instead. | int64 | long | int/long | 343 | | uint32 | Uses variable-length encoding. | uint32 | int | int/long | 344 | | uint64 | Uses variable-length encoding. | uint64 | long | int/long | 345 | | sint32 | Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int32s. | int32 | int | int | 346 | | sint64 | Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int64s. | int64 | long | int/long | 347 | | fixed32 | Always four bytes. More efficient than uint32 if values are often greater than 2^28. | uint32 | int | int | 348 | | fixed64 | Always eight bytes. More efficient than uint64 if values are often greater than 2^56. | uint64 | long | int/long | 349 | | sfixed32 | Always four bytes. | int32 | int | int | 350 | | sfixed64 | Always eight bytes. | int64 | long | int/long | 351 | | bool | | bool | boolean | boolean | 352 | | string | A string must always contain UTF-8 encoded or 7-bit ASCII text. | string | String | str/unicode | 353 | | bytes | May contain any arbitrary sequence of bytes. | string | ByteString | str | 354 | 355 | -------------------------------------------------------------------------------- /microservices/comments-svc/.env.example: -------------------------------------------------------------------------------- 1 | # Application Options 2 | NODE_ENV=development 3 | URL=0.0.0.0 4 | PORT=50051 5 | 6 | # DB 7 | DB_NAME=postgres 8 | DB_USER=postgres 9 | DB_PASSWORD=postgres 10 | DB_HOST=localhost 11 | DB_PORT=5432 12 | -------------------------------------------------------------------------------- /microservices/comments-svc/.eslintrc.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | parser: "@typescript-eslint/parser" 3 | parserOptions: 4 | project: "tsconfig.json" 5 | sourceType: "module" 6 | plugins: 7 | - "@typescript-eslint/eslint-plugin" 8 | - "prettier" 9 | - "import" 10 | extends: 11 | - "airbnb-base" 12 | - "plugin:@typescript-eslint/eslint-recommended" 13 | - "plugin:@typescript-eslint/recommended" 14 | - "prettier" 15 | - "prettier/@typescript-eslint" 16 | - "plugin:prettier/recommended" 17 | - "plugin:jest/recommended" 18 | - "plugin:import/errors" 19 | - "plugin:import/warnings" 20 | - "plugin:import/typescript" 21 | root: true 22 | env: 23 | node: true 24 | jest: true 25 | rules: 26 | "@typescript-eslint/interface-name-prefix": "off" 27 | "@typescript-eslint/explicit-function-return-type": "off" 28 | "@typescript-eslint/no-explicit-any": "off" 29 | "class-methods-use-this": 0 30 | import/prefer-default-export: 0 31 | import/extensions: 32 | - "error" 33 | - "ignorePackages" 34 | - js: "never" 35 | jsx: "never" 36 | ts: "never" 37 | tsx: "never" 38 | prettier/prettier: 39 | - "error" 40 | - parser: "typescript" 41 | printWidth: 200 42 | semi: false 43 | singleQuote: true 44 | endOfline: "lf" 45 | -------------------------------------------------------------------------------- /microservices/comments-svc/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12-alpine as build 2 | 3 | WORKDIR /usr/local/comments-svc 4 | 5 | COPY dist package.json ./ 6 | 7 | RUN npm install --production 8 | 9 | FROM node:12-alpine 10 | 11 | WORKDIR /usr/local/comments-svc 12 | 13 | COPY --from=build /usr/local/comments-svc . 14 | 15 | EXPOSE 50051 16 | 17 | CMD ["node", "main.js"] 18 | -------------------------------------------------------------------------------- /microservices/comments-svc/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src", 4 | "compilerOptions": { 5 | "assets": ["**/*.proto"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /microservices/comments-svc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "comments-svc", 3 | "version": "1.0.0", 4 | "description": "gRPC microservice back-end for comments. Used for learning/trial purposes only.", 5 | "scripts": { 6 | "prebuild": "rimraf dist", 7 | "build": "nest build", 8 | "start": "nest start", 9 | "start:dev": "nest start --watch", 10 | "start:debug": "nest start --debug --watch", 11 | "start:prod": "node dist/main", 12 | "lint": "eslint '{src,apps,libs,test}/**/*.ts' --fix", 13 | "test": "jest", 14 | "test:watch": "jest --watch", 15 | "test:cov": "jest --coverage", 16 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 17 | "test:e2e": "jest --config ./test/jest-e2e.json" 18 | }, 19 | "dependencies": { 20 | "@grpc/proto-loader": "0.5.3", 21 | "@nestjs/common": "6.11.6", 22 | "@nestjs/config": "0.2.2", 23 | "@nestjs/core": "6.11.6", 24 | "@nestjs/microservices": "6.11.6", 25 | "faker": "4.1.0", 26 | "grpc": "1.24.2", 27 | "lodash": "4.17.15", 28 | "nestjs-pino": "1.1.3", 29 | "pg": "7.18.1", 30 | "pg-hstore": "2.3.3", 31 | "pino": "5.16.0", 32 | "reflect-metadata": "0.1.13", 33 | "rimraf": "3.0.2", 34 | "rxjs": "6.5.4", 35 | "sequelize": "5.21.4", 36 | "sequelize-typescript": "1.1.0" 37 | }, 38 | "devDependencies": { 39 | "@nestjs/cli": "6.14.2", 40 | "@nestjs/schematics": "6.9.3", 41 | "@nestjs/testing": "6.11.6", 42 | "@types/bluebird": "3.5.29", 43 | "@types/faker": "4.1.9", 44 | "@types/jest": "25.1.2", 45 | "@types/lodash": "4.14.149", 46 | "@types/node": "13.7.0", 47 | "@types/validator": "12.0.1", 48 | "@typescript-eslint/eslint-plugin": "2.19.0", 49 | "@typescript-eslint/parser": "2.19.0", 50 | "eslint": "6.8.0", 51 | "eslint-config-airbnb-base": "14.0.0", 52 | "eslint-config-prettier": "6.10.0", 53 | "eslint-plugin-import": "2.20.1", 54 | "eslint-plugin-jest": "23.7.0", 55 | "eslint-plugin-prettier": "3.1.2", 56 | "jest": "25.1.0", 57 | "jest-extended": "0.11.5", 58 | "pino-pretty": "3.5.0", 59 | "prettier": "1.19.1", 60 | "ts-jest": "25.2.0", 61 | "ts-loader": "6.2.1", 62 | "ts-node": "8.6.2", 63 | "tsconfig-paths": "3.9.0", 64 | "typescript": "3.7.5" 65 | }, 66 | "repository": { 67 | "type": "git", 68 | "url": "git+ssh://git@github.com:benjsicam/nestjs-rest-microservices.git" 69 | }, 70 | "author": "Benj Sicam", 71 | "license": "MIT", 72 | "bugs": { 73 | "url": "https://github.com/benjsicam/nestjs-rest-microservices/issues" 74 | }, 75 | "homepage": "https://github.com/benjsicam/nestjs-rest-microservices#readme", 76 | "jest": { 77 | "moduleFileExtensions": [ 78 | "js", 79 | "json", 80 | "ts" 81 | ], 82 | "rootDir": "src", 83 | "testRegex": "tests/*.+(test.ts)", 84 | "transform": { 85 | ".+\\.(t|j)s$": "ts-jest" 86 | }, 87 | "collectCoverage": true, 88 | "coverageThreshold": { 89 | "global": { 90 | "branches": 50, 91 | "functions": 75, 92 | "lines": 75, 93 | "statements": 75 94 | } 95 | }, 96 | "coverageDirectory": "../coverage", 97 | "testEnvironment": "node" 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /microservices/comments-svc/src/_proto/comments.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package comments; 4 | 5 | import "commons.proto"; 6 | 7 | message Comment { 8 | string id = 1; 9 | string organization = 2; 10 | string comment = 3; 11 | string createdAt = 4; 12 | string updatedAt = 5; 13 | int32 version = 6; 14 | } 15 | 16 | message CreateCommentInput { 17 | string organization = 1; 18 | string comment = 2; 19 | } 20 | 21 | message CommentsList { 22 | repeated Comment data = 5; 23 | } 24 | 25 | service CommentsService { 26 | rpc findAll (commons.Query) returns (CommentsList) {} 27 | rpc count (commons.Query) returns (commons.Count) {} 28 | rpc create (CreateCommentInput) returns (Comment) {} 29 | rpc destroy (commons.Query) returns (commons.Count) {} 30 | } 31 | -------------------------------------------------------------------------------- /microservices/comments-svc/src/_proto/commons.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package commons; 4 | 5 | message Id { 6 | string id = 1; 7 | } 8 | 9 | message Name { 10 | string name = 1; 11 | } 12 | 13 | message Query { 14 | repeated string attributes = 1; 15 | string where = 2; 16 | string order = 3; 17 | int32 offset = 4; 18 | int32 limit = 5; 19 | } 20 | 21 | message Count { 22 | int32 count = 1; 23 | } 24 | -------------------------------------------------------------------------------- /microservices/comments-svc/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { ConfigModule } from '@nestjs/config' 3 | import { LoggerModule } from 'nestjs-pino' 4 | 5 | import { DatabaseModule } from './database/database.module' 6 | import { CommentsModule } from './comments/comments.module' 7 | 8 | @Module({ 9 | imports: [ 10 | ConfigModule.forRoot(), 11 | LoggerModule.forRoot({ 12 | pinoHttp: { 13 | safe: true, 14 | prettyPrint: process.env.NODE_ENV === 'development' 15 | } 16 | }), 17 | DatabaseModule, 18 | CommentsModule 19 | ] 20 | }) 21 | export class AppModule {} 22 | -------------------------------------------------------------------------------- /microservices/comments-svc/src/comments/comment.dto.ts: -------------------------------------------------------------------------------- 1 | export class CommentDto { 2 | readonly id?: string 3 | 4 | readonly organization: string 5 | 6 | readonly comment: string 7 | } 8 | -------------------------------------------------------------------------------- /microservices/comments-svc/src/comments/comment.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, Model, Table, DataType } from 'sequelize-typescript' 2 | 3 | @Table({ 4 | modelName: 'comment', 5 | tableName: 'comments', 6 | underscored: true, 7 | paranoid: true, 8 | timestamps: true, 9 | version: true 10 | }) 11 | export class Comment extends Model { 12 | @Column({ 13 | primaryKey: true, 14 | type: DataType.UUID, 15 | defaultValue: DataType.UUIDV4, 16 | comment: 'The identifier for the comment record' 17 | }) 18 | id: string 19 | 20 | @Column({ 21 | type: DataType.UUID, 22 | comment: 'Ref: Organization. The organization the comment is associated with' 23 | }) 24 | organization: string 25 | 26 | @Column({ 27 | type: DataType.TEXT, 28 | comment: 'The comment text' 29 | }) 30 | comment: string 31 | } 32 | -------------------------------------------------------------------------------- /microservices/comments-svc/src/comments/comments.controller.ts: -------------------------------------------------------------------------------- 1 | import { PinoLogger } from 'nestjs-pino' 2 | import { Controller, Inject } from '@nestjs/common' 3 | import { GrpcMethod } from '@nestjs/microservices' 4 | import { isEmpty } from 'lodash' 5 | 6 | import { Count, Query } from '../commons/interfaces/commons.interface' 7 | import { CommentsService, CommentsQueryResult } from './comments.interface' 8 | 9 | import { Comment } from './comment.entity' 10 | import { CommentDto } from './comment.dto' 11 | 12 | @Controller() 13 | export class CommentsController { 14 | constructor(@Inject('CommentsService') private readonly commentsService: CommentsService, private readonly logger: PinoLogger) { 15 | logger.setContext(CommentsController.name) 16 | } 17 | 18 | @GrpcMethod('CommentsService', 'findAll') 19 | async findAll(query: Query): Promise { 20 | this.logger.info('CommentsController#findAll.call', query) 21 | 22 | const result: Array = await this.commentsService.findAll({ 23 | attributes: !isEmpty(query.attributes) ? query.attributes : undefined, 24 | where: !isEmpty(query.where) ? JSON.parse(query.where) : undefined, 25 | order: !isEmpty(query.order) ? JSON.parse(query.order) : undefined, 26 | offset: query.offset ? query.offset : 0, 27 | limit: query.limit ? query.limit : 25 28 | }) 29 | 30 | this.logger.info('CommentsController#findAll.result', result) 31 | 32 | return { data: result } 33 | } 34 | 35 | @GrpcMethod('CommentsService', 'count') 36 | async count(query: Query): Promise { 37 | this.logger.info('CommentsController#count.call', query) 38 | 39 | const count: number = await this.commentsService.count({ 40 | where: !isEmpty(query.where) ? JSON.parse(query.where) : undefined 41 | }) 42 | 43 | this.logger.info('CommentsController#count.result', count) 44 | 45 | return { count } 46 | } 47 | 48 | @GrpcMethod('CommentsService', 'create') 49 | async create(data: CommentDto): Promise { 50 | this.logger.info('CommentsController#create.call', data) 51 | 52 | const result: Comment = await this.commentsService.create(data) 53 | 54 | this.logger.info('CommentsController#create.result', result) 55 | 56 | return result 57 | } 58 | 59 | @GrpcMethod('CommentsService', 'destroy') 60 | async destroy(query: Query): Promise { 61 | this.logger.info('CommentsController#destroy.call', query) 62 | 63 | const count: number = await this.commentsService.destroy({ 64 | where: !isEmpty(query.where) ? JSON.parse(query.where) : undefined 65 | }) 66 | 67 | this.logger.info('CommentsController#destroy.result', count) 68 | 69 | return { count } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /microservices/comments-svc/src/comments/comments.interface.ts: -------------------------------------------------------------------------------- 1 | import { FindOptions } from 'sequelize/types' 2 | 3 | import { Comment } from './comment.entity' 4 | import { CommentDto } from './comment.dto' 5 | 6 | export interface CommentsQueryResult { 7 | data: Array 8 | } 9 | 10 | export interface CommentsService { 11 | findAll(query?: FindOptions): Promise> 12 | count(query?: FindOptions): Promise 13 | create(comment: CommentDto): Promise 14 | destroy(query?: FindOptions): Promise 15 | } 16 | -------------------------------------------------------------------------------- /microservices/comments-svc/src/comments/comments.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { LoggerModule } from 'nestjs-pino' 3 | 4 | import { Comment } from './comment.entity' 5 | import { CommentsController } from './comments.controller' 6 | import { CommentsServiceImpl } from './comments.service' 7 | import { CommentsSeeder } from './comments.seeder' 8 | 9 | @Module({ 10 | imports: [ 11 | LoggerModule.forRoot({ 12 | pinoHttp: { 13 | safe: true, 14 | prettyPrint: process.env.NODE_ENV === 'development' 15 | } 16 | }) 17 | ], 18 | controllers: [CommentsController], 19 | providers: [CommentsSeeder, { provide: 'CommentsService', useClass: CommentsServiceImpl }, { provide: 'CommentsRepository', useValue: Comment }] 20 | }) 21 | export class CommentsModule {} 22 | -------------------------------------------------------------------------------- /microservices/comments-svc/src/comments/comments.seeder.ts: -------------------------------------------------------------------------------- 1 | import { PinoLogger } from 'nestjs-pino' 2 | import { Inject, Injectable } from '@nestjs/common' 3 | import { lorem, random } from 'faker' 4 | import { times } from 'lodash' 5 | 6 | import { CommentsService } from './comments.interface' 7 | import { Comment } from './comment.entity' 8 | 9 | @Injectable() 10 | export class CommentsSeeder { 11 | private readonly ORGS: Array = ['62a1c874-1f3f-4e24-a553-05289eea6332', 'f891fa17-d33f-49cb-baea-ced2539fa574'] 12 | 13 | constructor(@Inject('CommentsService') private readonly service: CommentsService, private readonly logger: PinoLogger) { 14 | logger.setContext(CommentsSeeder.name) 15 | } 16 | 17 | async seedDatabase(): Promise { 18 | const recordCount: number = await this.service.count() 19 | 20 | if (recordCount > 0) { 21 | this.logger.info('CommentsSeeder#seedDatabase', 'Aborting...') 22 | 23 | return recordCount 24 | } 25 | 26 | const numOfRecords: number = random.number({ min: 10, max: 30 }) 27 | 28 | this.logger.info('CommentsSeeder#seedDatabase.numOfRecords', numOfRecords) 29 | 30 | times(numOfRecords, async () => { 31 | const comment: Comment = await this.service.create({ 32 | organization: random.arrayElement(this.ORGS), 33 | comment: lorem.sentence() 34 | }) 35 | 36 | this.logger.info('CommentsSeeder#seedDatabase.newRecord', comment) 37 | }) 38 | 39 | return numOfRecords 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /microservices/comments-svc/src/comments/comments.service.ts: -------------------------------------------------------------------------------- 1 | import { PinoLogger } from 'nestjs-pino' 2 | import { Inject, Injectable } from '@nestjs/common' 3 | import { FindOptions } from 'sequelize/types' 4 | 5 | import { CommentsService } from './comments.interface' 6 | 7 | import { Comment } from './comment.entity' 8 | import { CommentDto } from './comment.dto' 9 | 10 | @Injectable() 11 | export class CommentsServiceImpl implements CommentsService { 12 | constructor(@Inject('CommentsRepository') private readonly repo: typeof Comment, private readonly logger: PinoLogger) { 13 | logger.setContext(CommentsServiceImpl.name) 14 | } 15 | 16 | async findAll(query?: FindOptions): Promise> { 17 | this.logger.info('CommentsService#findAll.call', query) 18 | 19 | const result = await this.repo.findAll(query) 20 | 21 | this.logger.info('CommentsService#findAll.result', result) 22 | 23 | return result 24 | } 25 | 26 | async count(query?: FindOptions): Promise { 27 | this.logger.info('CommentsService#count.call', query) 28 | 29 | const result = await this.repo.count(query) 30 | 31 | this.logger.info('CommentsService#count.result', result) 32 | 33 | return result 34 | } 35 | 36 | async create(commentDto: CommentDto): Promise { 37 | this.logger.info('CommentsService#create.call', commentDto) 38 | 39 | const comment = new Comment(commentDto) 40 | 41 | const result = await comment.save() 42 | 43 | this.logger.info('CommentsService#create.result', result) 44 | 45 | return result 46 | } 47 | 48 | async destroy(query?: FindOptions): Promise { 49 | this.logger.info('CommentsService#destroy.call', query) 50 | 51 | const result = await Comment.destroy(query) 52 | 53 | this.logger.info('CommentsService#destroy.result', result) 54 | 55 | return result 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /microservices/comments-svc/src/commons/interfaces/commons.interface.ts: -------------------------------------------------------------------------------- 1 | export interface Id { 2 | id: string 3 | } 4 | 5 | export interface Name { 6 | name: string 7 | } 8 | 9 | export interface Query { 10 | attributes?: Array 11 | where?: string 12 | order?: string 13 | offset?: number 14 | limit?: number 15 | } 16 | 17 | export interface Count { 18 | count: number 19 | } 20 | -------------------------------------------------------------------------------- /microservices/comments-svc/src/database/database.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { LoggerModule } from 'nestjs-pino' 3 | 4 | import { DatabaseProvider } from './database.providers' 5 | 6 | @Module({ 7 | imports: [ 8 | LoggerModule.forRoot({ 9 | pinoHttp: { 10 | safe: true, 11 | prettyPrint: process.env.NODE_ENV === 'development' 12 | } 13 | }) 14 | ], 15 | providers: [DatabaseProvider], 16 | exports: [DatabaseProvider] 17 | }) 18 | export class DatabaseModule {} 19 | -------------------------------------------------------------------------------- /microservices/comments-svc/src/database/database.providers.ts: -------------------------------------------------------------------------------- 1 | import { Provider } from '@nestjs/common' 2 | import { Sequelize } from 'sequelize-typescript' 3 | 4 | import { PinoLogger } from 'nestjs-pino' 5 | import { Comment } from '../comments/comment.entity' 6 | 7 | export const DatabaseProvider: Provider = { 8 | provide: 'SEQUELIZE', 9 | useFactory: async (logger: PinoLogger) => { 10 | logger.setContext('Sequelize') 11 | 12 | const db: Sequelize = new Sequelize(process.env.DB_NAME, process.env.DB_USER, process.env.DB_PASSWORD, { 13 | dialect: 'postgres', 14 | host: process.env.DB_HOST, 15 | port: parseInt(process.env.DB_PORT || '5432', 10), 16 | logging: logger.info.bind(logger), 17 | benchmark: true, 18 | retry: { 19 | max: 3 20 | } 21 | }) 22 | 23 | db.addModels([Comment]) 24 | 25 | await db.sync() 26 | 27 | return db 28 | }, 29 | inject: [PinoLogger] 30 | } 31 | -------------------------------------------------------------------------------- /microservices/comments-svc/src/main.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path' 2 | 3 | import { NestFactory } from '@nestjs/core' 4 | import { Transport } from '@nestjs/microservices' 5 | import { Logger } from 'nestjs-pino' 6 | 7 | import { AppModule } from './app.module' 8 | import { CommentsSeeder } from './comments/comments.seeder' 9 | 10 | async function bootstrap() { 11 | const app = await NestFactory.createMicroservice(AppModule, { 12 | transport: Transport.GRPC, 13 | options: { 14 | url: `${process.env.URL}:${process.env.PORT}`, 15 | package: 'comments', 16 | protoPath: join(__dirname, './_proto/comments.proto'), 17 | loader: { 18 | enums: String, 19 | objects: true, 20 | arrays: true 21 | } 22 | } 23 | }) 24 | 25 | app.useLogger(app.get(Logger)) 26 | 27 | const seeder: CommentsSeeder = app.get(CommentsSeeder) 28 | 29 | await seeder.seedDatabase() 30 | 31 | return app.listenAsync() 32 | } 33 | 34 | bootstrap() 35 | -------------------------------------------------------------------------------- /microservices/comments-svc/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /microservices/comments-svc/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "target": "es2017", 9 | "sourceMap": true, 10 | "outDir": "./dist", 11 | "baseUrl": "./", 12 | "incremental": true 13 | }, 14 | "assets": ["_proto"], 15 | "exclude": ["node_modules", "dist"] 16 | } 17 | -------------------------------------------------------------------------------- /microservices/organizations-svc/.env.example: -------------------------------------------------------------------------------- 1 | # Application Options 2 | NODE_ENV=development 3 | URL=0.0.0.0 4 | PORT=50052 5 | 6 | # DB 7 | DB_NAME=postgres 8 | DB_USER=postgres 9 | DB_PASSWORD=postgres 10 | DB_HOST=localhost 11 | DB_PORT=5432 12 | -------------------------------------------------------------------------------- /microservices/organizations-svc/.eslintrc.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | parser: "@typescript-eslint/parser" 3 | parserOptions: 4 | project: "tsconfig.json" 5 | sourceType: "module" 6 | plugins: 7 | - "@typescript-eslint/eslint-plugin" 8 | - "prettier" 9 | - "import" 10 | extends: 11 | - "airbnb-base" 12 | - "plugin:@typescript-eslint/eslint-recommended" 13 | - "plugin:@typescript-eslint/recommended" 14 | - "prettier" 15 | - "prettier/@typescript-eslint" 16 | - "plugin:prettier/recommended" 17 | - "plugin:jest/recommended" 18 | - "plugin:import/errors" 19 | - "plugin:import/warnings" 20 | - "plugin:import/typescript" 21 | root: true 22 | env: 23 | node: true 24 | jest: true 25 | rules: 26 | "@typescript-eslint/interface-name-prefix": "off" 27 | "@typescript-eslint/explicit-function-return-type": "off" 28 | "@typescript-eslint/no-explicit-any": "off" 29 | "class-methods-use-this": 0 30 | import/prefer-default-export: 0 31 | import/extensions: 32 | - "error" 33 | - "ignorePackages" 34 | - js: "never" 35 | jsx: "never" 36 | ts: "never" 37 | tsx: "never" 38 | prettier/prettier: 39 | - "error" 40 | - parser: "typescript" 41 | printWidth: 200 42 | semi: false 43 | singleQuote: true 44 | endOfline: "lf" 45 | -------------------------------------------------------------------------------- /microservices/organizations-svc/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12-alpine as build 2 | 3 | WORKDIR /usr/local/organizations-svc 4 | 5 | COPY dist package.json ./ 6 | 7 | RUN npm install --production 8 | 9 | FROM node:12-alpine 10 | 11 | WORKDIR /usr/local/organizations-svc 12 | 13 | COPY --from=build /usr/local/organizations-svc . 14 | 15 | EXPOSE 50051 16 | 17 | CMD ["node", "main.js"] 18 | -------------------------------------------------------------------------------- /microservices/organizations-svc/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src", 4 | "compilerOptions": { 5 | "assets": ["**/*.proto"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /microservices/organizations-svc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "organizations-svc", 3 | "version": "1.0.0", 4 | "description": "gRPC microservice back-end for organizations. Used for learning/trial purposes only.", 5 | "scripts": { 6 | "prebuild": "rimraf dist", 7 | "build": "nest build", 8 | "start": "nest start", 9 | "start:dev": "nest start --watch", 10 | "start:debug": "nest start --debug --watch", 11 | "start:prod": "node dist/main", 12 | "lint": "eslint '{src,apps,libs,test}/**/*.ts' --fix", 13 | "test": "jest", 14 | "test:watch": "jest --watch", 15 | "test:cov": "jest --coverage", 16 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 17 | "test:e2e": "jest --config ./test/jest-e2e.json" 18 | }, 19 | "dependencies": { 20 | "@grpc/proto-loader": "0.5.3", 21 | "@nestjs/common": "6.11.6", 22 | "@nestjs/config": "0.2.2", 23 | "@nestjs/core": "6.11.6", 24 | "@nestjs/microservices": "6.11.6", 25 | "faker": "4.1.0", 26 | "grpc": "1.24.2", 27 | "lodash": "4.17.15", 28 | "nestjs-pino": "1.1.3", 29 | "pg": "7.18.1", 30 | "pg-hstore": "2.3.3", 31 | "pino": "5.16.0", 32 | "reflect-metadata": "0.1.13", 33 | "rimraf": "3.0.2", 34 | "rxjs": "6.5.4", 35 | "sequelize": "5.21.4", 36 | "sequelize-typescript": "1.1.0" 37 | }, 38 | "devDependencies": { 39 | "@nestjs/cli": "6.14.2", 40 | "@nestjs/schematics": "6.9.3", 41 | "@nestjs/testing": "6.11.6", 42 | "@types/bluebird": "3.5.29", 43 | "@types/faker": "4.1.9", 44 | "@types/jest": "25.1.2", 45 | "@types/lodash": "4.14.149", 46 | "@types/node": "13.7.0", 47 | "@types/validator": "12.0.1", 48 | "@typescript-eslint/eslint-plugin": "2.19.0", 49 | "@typescript-eslint/parser": "2.19.0", 50 | "eslint": "6.8.0", 51 | "eslint-config-airbnb-base": "14.0.0", 52 | "eslint-config-prettier": "6.10.0", 53 | "eslint-plugin-import": "2.20.1", 54 | "eslint-plugin-jest": "23.7.0", 55 | "eslint-plugin-prettier": "3.1.2", 56 | "jest": "25.1.0", 57 | "jest-extended": "0.11.5", 58 | "pino-pretty": "3.5.0", 59 | "prettier": "1.19.1", 60 | "ts-jest": "25.2.0", 61 | "ts-loader": "6.2.1", 62 | "ts-node": "8.6.2", 63 | "tsconfig-paths": "3.9.0", 64 | "typescript": "3.7.5" 65 | }, 66 | "repository": { 67 | "type": "git", 68 | "url": "git+ssh://git@github.com:benjsicam/nestjs-rest-microservices.git" 69 | }, 70 | "author": "Benj Sicam", 71 | "license": "MIT", 72 | "bugs": { 73 | "url": "https://github.com/benjsicam/nestjs-rest-microservices/issues" 74 | }, 75 | "homepage": "https://github.com/benjsicam/nestjs-rest-microservices#readme", 76 | "jest": { 77 | "moduleFileExtensions": [ 78 | "js", 79 | "json", 80 | "ts" 81 | ], 82 | "rootDir": "src", 83 | "testRegex": "tests/*.+(test.ts)", 84 | "transform": { 85 | ".+\\.(t|j)s$": "ts-jest" 86 | }, 87 | "collectCoverage": true, 88 | "coverageThreshold": { 89 | "global": { 90 | "branches": 50, 91 | "functions": 75, 92 | "lines": 75, 93 | "statements": 75 94 | } 95 | }, 96 | "coverageDirectory": "../coverage", 97 | "testEnvironment": "node" 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /microservices/organizations-svc/src/_proto/commons.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package commons; 4 | 5 | message Id { 6 | string id = 1; 7 | } 8 | 9 | message Name { 10 | string name = 1; 11 | } 12 | 13 | message Query { 14 | repeated string attributes = 1; 15 | string where = 2; 16 | string order = 3; 17 | int32 offset = 4; 18 | int32 limit = 5; 19 | } 20 | 21 | message Count { 22 | int32 count = 1; 23 | } 24 | -------------------------------------------------------------------------------- /microservices/organizations-svc/src/_proto/organizations.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package organizations; 4 | 5 | import "commons.proto"; 6 | 7 | message Organization { 8 | string id = 1; 9 | string name = 2; 10 | string createdAt = 3; 11 | string updatedAt = 4; 12 | int32 version = 5; 13 | } 14 | 15 | message OrganizationsList { 16 | repeated Organization data = 5; 17 | } 18 | 19 | service OrganizationsService { 20 | rpc findAll (commons.Query) returns (OrganizationsList) {} 21 | rpc findByName (commons.Name) returns (Organization) {} 22 | rpc count (commons.Query) returns (commons.Count) {} 23 | } 24 | -------------------------------------------------------------------------------- /microservices/organizations-svc/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { ConfigModule } from '@nestjs/config' 3 | import { LoggerModule } from 'nestjs-pino' 4 | 5 | import { DatabaseModule } from './database/database.module' 6 | import { OrganizationsModule } from './organizations/organizations.module' 7 | 8 | @Module({ 9 | imports: [ 10 | ConfigModule.forRoot(), 11 | LoggerModule.forRoot({ 12 | pinoHttp: { 13 | safe: true, 14 | prettyPrint: process.env.NODE_ENV === 'development' 15 | } 16 | }), 17 | DatabaseModule, 18 | OrganizationsModule 19 | ] 20 | }) 21 | export class AppModule {} 22 | -------------------------------------------------------------------------------- /microservices/organizations-svc/src/commons/interfaces/commons.interface.ts: -------------------------------------------------------------------------------- 1 | export interface Id { 2 | id: string 3 | } 4 | 5 | export interface Name { 6 | name: string 7 | } 8 | 9 | export interface Query { 10 | attributes?: Array 11 | where?: string 12 | order?: string 13 | offset?: number 14 | limit?: number 15 | } 16 | 17 | export interface Count { 18 | count: number 19 | } 20 | -------------------------------------------------------------------------------- /microservices/organizations-svc/src/database/database.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { LoggerModule } from 'nestjs-pino' 3 | 4 | import { DatabaseProvider } from './database.providers' 5 | 6 | @Module({ 7 | imports: [ 8 | LoggerModule.forRoot({ 9 | pinoHttp: { 10 | safe: true, 11 | prettyPrint: process.env.NODE_ENV === 'development' 12 | } 13 | }) 14 | ], 15 | providers: [DatabaseProvider], 16 | exports: [DatabaseProvider] 17 | }) 18 | export class DatabaseModule {} 19 | -------------------------------------------------------------------------------- /microservices/organizations-svc/src/database/database.providers.ts: -------------------------------------------------------------------------------- 1 | import { Provider } from '@nestjs/common' 2 | import { Sequelize } from 'sequelize-typescript' 3 | 4 | import { PinoLogger } from 'nestjs-pino' 5 | import { Organization } from '../organizations/organization.entity' 6 | 7 | export const DatabaseProvider: Provider = { 8 | provide: 'SEQUELIZE', 9 | useFactory: async (logger: PinoLogger) => { 10 | logger.setContext('Sequelize') 11 | 12 | const db: Sequelize = new Sequelize(process.env.DB_NAME, process.env.DB_USER, process.env.DB_PASSWORD, { 13 | dialect: 'postgres', 14 | host: process.env.DB_HOST, 15 | port: parseInt(process.env.DB_PORT || '5432', 10), 16 | logging: logger.info.bind(logger), 17 | benchmark: true, 18 | retry: { 19 | max: 3 20 | } 21 | }) 22 | 23 | db.addModels([Organization]) 24 | 25 | await db.sync() 26 | 27 | return db 28 | }, 29 | inject: [PinoLogger] 30 | } 31 | -------------------------------------------------------------------------------- /microservices/organizations-svc/src/main.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path' 2 | 3 | import { NestFactory } from '@nestjs/core' 4 | import { Transport } from '@nestjs/microservices' 5 | import { Logger } from 'nestjs-pino' 6 | 7 | import { AppModule } from './app.module' 8 | import { OrganizationsSeeder } from './organizations/organizations.seeder' 9 | 10 | async function bootstrap() { 11 | const app = await NestFactory.createMicroservice(AppModule, { 12 | transport: Transport.GRPC, 13 | options: { 14 | url: `${process.env.URL}:${process.env.PORT}`, 15 | package: 'organizations', 16 | protoPath: join(__dirname, './_proto/organizations.proto'), 17 | loader: { 18 | enums: String, 19 | objects: true, 20 | arrays: true 21 | } 22 | } 23 | }) 24 | 25 | app.useLogger(app.get(Logger)) 26 | 27 | const seeder: OrganizationsSeeder = app.get(OrganizationsSeeder) 28 | 29 | await seeder.seedDatabase() 30 | 31 | return app.listenAsync() 32 | } 33 | 34 | bootstrap() 35 | -------------------------------------------------------------------------------- /microservices/organizations-svc/src/organizations/organization.dto.ts: -------------------------------------------------------------------------------- 1 | export class OrganizationDto { 2 | readonly id?: string 3 | 4 | readonly name: string 5 | } 6 | -------------------------------------------------------------------------------- /microservices/organizations-svc/src/organizations/organization.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, Model, Table, DataType } from 'sequelize-typescript' 2 | 3 | @Table({ 4 | modelName: 'organization', 5 | tableName: 'organizations', 6 | underscored: true, 7 | timestamps: true, 8 | version: true 9 | }) 10 | export class Organization extends Model { 11 | @Column({ 12 | primaryKey: true, 13 | type: DataType.UUID, 14 | defaultValue: DataType.UUIDV4, 15 | comment: 'The identifier for the organization record' 16 | }) 17 | id: string 18 | 19 | @Column({ 20 | type: DataType.STRING, 21 | unique: true, 22 | comment: 'The name of the organization' 23 | }) 24 | name: string 25 | } 26 | -------------------------------------------------------------------------------- /microservices/organizations-svc/src/organizations/organizations.controller.ts: -------------------------------------------------------------------------------- 1 | import { PinoLogger } from 'nestjs-pino' 2 | import { Controller, Inject } from '@nestjs/common' 3 | import { GrpcMethod } from '@nestjs/microservices' 4 | import { isEmpty } from 'lodash' 5 | 6 | import { Query, Count, Name } from '../commons/interfaces/commons.interface' 7 | import { OrganizationsService, OrganizationsQueryResult } from './organizations.interface' 8 | 9 | import { Organization } from './organization.entity' 10 | 11 | @Controller() 12 | export class OrganizationsController { 13 | constructor(@Inject('OrganizationsService') private readonly organizationsService: OrganizationsService, private readonly logger: PinoLogger) { 14 | logger.setContext(OrganizationsController.name) 15 | } 16 | 17 | @GrpcMethod('OrganizationsService', 'findAll') 18 | async findAll(query: Query): Promise { 19 | this.logger.info('OrganizationsController#findAll.call', query) 20 | 21 | const result: Array = await this.organizationsService.findAll({ 22 | attributes: !isEmpty(query.attributes) ? query.attributes : undefined, 23 | where: !isEmpty(query.where) ? JSON.parse(query.where) : undefined, 24 | order: !isEmpty(query.order) ? JSON.parse(query.order) : undefined, 25 | offset: query.offset ? query.offset : 0, 26 | limit: query.limit ? query.limit : 25 27 | }) 28 | 29 | this.logger.info('OrganizationsController#findAll.result', result) 30 | 31 | return { data: result } 32 | } 33 | 34 | @GrpcMethod('OrganizationsService', 'findByName') 35 | async findByName(data: Name): Promise { 36 | this.logger.info('OrganizationsController#findByName.call', data) 37 | 38 | const result: Organization = await this.organizationsService.findOne({ 39 | where: { name: data.name } 40 | }) 41 | 42 | this.logger.info('OrganizationsController#findByName.result', result) 43 | 44 | return result 45 | } 46 | 47 | @GrpcMethod('OrganizationsService', 'count') 48 | async count(query: Query): Promise { 49 | this.logger.info('OrganizationsController#count.call', query) 50 | 51 | const count: number = await this.organizationsService.count({ 52 | where: !isEmpty(query.where) ? JSON.parse(query.where) : undefined 53 | }) 54 | 55 | this.logger.info('OrganizationsController#count.result', count) 56 | 57 | return { count } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /microservices/organizations-svc/src/organizations/organizations.interface.ts: -------------------------------------------------------------------------------- 1 | import { FindOptions } from 'sequelize/types' 2 | 3 | import { Organization } from './organization.entity' 4 | import { OrganizationDto } from './organization.dto' 5 | 6 | export interface OrganizationsQueryResult { 7 | data: Array 8 | } 9 | 10 | export interface OrganizationsService { 11 | findAll(query?: FindOptions): Promise> 12 | findOne(query?: FindOptions): Promise 13 | count(query?: FindOptions): Promise 14 | create(organization: OrganizationDto): Promise 15 | } 16 | -------------------------------------------------------------------------------- /microservices/organizations-svc/src/organizations/organizations.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { LoggerModule } from 'nestjs-pino' 3 | 4 | import { Organization } from './organization.entity' 5 | import { OrganizationsController } from './organizations.controller' 6 | import { OrganizationsServiceImpl } from './organizations.service' 7 | import { OrganizationsSeeder } from './organizations.seeder' 8 | 9 | @Module({ 10 | imports: [ 11 | LoggerModule.forRoot({ 12 | pinoHttp: { 13 | safe: true, 14 | prettyPrint: process.env.NODE_ENV === 'development' 15 | } 16 | }) 17 | ], 18 | controllers: [OrganizationsController], 19 | providers: [OrganizationsSeeder, { provide: 'OrganizationsService', useClass: OrganizationsServiceImpl }, { provide: 'OrganizationsRepository', useValue: Organization }] 20 | }) 21 | export class OrganizationsModule {} 22 | -------------------------------------------------------------------------------- /microservices/organizations-svc/src/organizations/organizations.seeder.ts: -------------------------------------------------------------------------------- 1 | import { PinoLogger } from 'nestjs-pino' 2 | import { Inject, Injectable } from '@nestjs/common' 3 | import { company, helpers } from 'faker' 4 | import { forEach } from 'lodash' 5 | 6 | import { OrganizationsService } from './organizations.interface' 7 | import { Organization } from './organization.entity' 8 | 9 | @Injectable() 10 | export class OrganizationsSeeder { 11 | private readonly ORGS: Array = ['62a1c874-1f3f-4e24-a553-05289eea6332', 'f891fa17-d33f-49cb-baea-ced2539fa574'] 12 | 13 | constructor(@Inject('OrganizationsService') private readonly service: OrganizationsService, private readonly logger: PinoLogger) { 14 | logger.setContext(OrganizationsSeeder.name) 15 | } 16 | 17 | async seedDatabase(): Promise { 18 | const recordCount: number = await this.service.count() 19 | 20 | if (recordCount > 0) { 21 | this.logger.info('OrganizationsSeeder#seedDatabase', 'Aborting...') 22 | 23 | return recordCount 24 | } 25 | 26 | forEach(this.ORGS, async id => { 27 | const organization: Organization = await this.service.create({ 28 | id, 29 | name: helpers.slugify(company.companyName(1)) 30 | }) 31 | 32 | this.logger.info('OrganizationsSeeder#seedDatabase.newRecord', organization) 33 | }) 34 | 35 | return this.ORGS.length 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /microservices/organizations-svc/src/organizations/organizations.service.ts: -------------------------------------------------------------------------------- 1 | import { PinoLogger } from 'nestjs-pino' 2 | import { Inject, Injectable } from '@nestjs/common' 3 | import { FindOptions } from 'sequelize/types' 4 | 5 | import { OrganizationsService } from './organizations.interface' 6 | 7 | import { Organization } from './organization.entity' 8 | import { OrganizationDto } from './organization.dto' 9 | 10 | @Injectable() 11 | export class OrganizationsServiceImpl implements OrganizationsService { 12 | constructor(@Inject('OrganizationsRepository') private readonly repo: typeof Organization, private readonly logger: PinoLogger) { 13 | logger.setContext(OrganizationsServiceImpl.name) 14 | } 15 | 16 | async findAll(query?: FindOptions): Promise> { 17 | this.logger.info('OrganizationsService#findAll.call', query) 18 | 19 | const result: Array = await this.repo.findAll(query) 20 | 21 | this.logger.info('OrganizationsService#findAll.result', result) 22 | 23 | return result 24 | } 25 | 26 | async findOne(query?: FindOptions): Promise { 27 | this.logger.info('OrganizationsService#findOne.call', query) 28 | 29 | const result: Organization = await this.repo.findOne(query) 30 | 31 | this.logger.info('OrganizationsService#findOne.result', result) 32 | 33 | return result 34 | } 35 | 36 | async count(query?: FindOptions): Promise { 37 | this.logger.info('OrganizationsService#count.call', query) 38 | 39 | const result: number = await this.repo.count(query) 40 | 41 | this.logger.info('OrganizationsService#count.result', result) 42 | 43 | return result 44 | } 45 | 46 | async create(organizationDto: OrganizationDto): Promise { 47 | this.logger.info('OrganizationsService#create.call', organizationDto) 48 | 49 | const organization: Organization = await this.repo.create(organizationDto) 50 | 51 | this.logger.info('OrganizationsService#create.result', organization) 52 | 53 | return organization 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /microservices/organizations-svc/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /microservices/organizations-svc/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "target": "es2017", 9 | "sourceMap": true, 10 | "outDir": "./dist", 11 | "baseUrl": "./", 12 | "incremental": true 13 | }, 14 | "assets": ["_proto"], 15 | "exclude": ["node_modules", "dist"] 16 | } 17 | -------------------------------------------------------------------------------- /microservices/users-svc/.env.example: -------------------------------------------------------------------------------- 1 | # Application Options 2 | NODE_ENV=development 3 | URL=0.0.0.0 4 | PORT=50053 5 | 6 | # DB 7 | DB_NAME=postgres 8 | DB_USER=postgres 9 | DB_PASSWORD=postgres 10 | DB_HOST=localhost 11 | DB_PORT=5432 12 | -------------------------------------------------------------------------------- /microservices/users-svc/.eslintrc.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | parser: "@typescript-eslint/parser" 3 | parserOptions: 4 | project: "tsconfig.json" 5 | sourceType: "module" 6 | plugins: 7 | - "@typescript-eslint/eslint-plugin" 8 | - "prettier" 9 | - "import" 10 | extends: 11 | - "airbnb-base" 12 | - "plugin:@typescript-eslint/eslint-recommended" 13 | - "plugin:@typescript-eslint/recommended" 14 | - "prettier" 15 | - "prettier/@typescript-eslint" 16 | - "plugin:prettier/recommended" 17 | - "plugin:jest/recommended" 18 | - "plugin:import/errors" 19 | - "plugin:import/warnings" 20 | - "plugin:import/typescript" 21 | root: true 22 | env: 23 | node: true 24 | jest: true 25 | rules: 26 | "@typescript-eslint/interface-name-prefix": "off" 27 | "@typescript-eslint/explicit-function-return-type": "off" 28 | "@typescript-eslint/no-explicit-any": "off" 29 | "class-methods-use-this": 0 30 | import/prefer-default-export: 0 31 | import/extensions: 32 | - "error" 33 | - "ignorePackages" 34 | - js: "never" 35 | jsx: "never" 36 | ts: "never" 37 | tsx: "never" 38 | prettier/prettier: 39 | - "error" 40 | - parser: "typescript" 41 | printWidth: 200 42 | semi: false 43 | singleQuote: true 44 | endOfline: "lf" 45 | -------------------------------------------------------------------------------- /microservices/users-svc/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12-alpine as build 2 | 3 | WORKDIR /usr/local/users-svc 4 | 5 | COPY dist package.json ./ 6 | 7 | RUN npm install --production 8 | 9 | FROM node:12-alpine 10 | 11 | WORKDIR /usr/local/users-svc 12 | 13 | COPY --from=build /usr/local/users-svc . 14 | 15 | EXPOSE 50051 16 | 17 | CMD ["node", "main.js"] 18 | -------------------------------------------------------------------------------- /microservices/users-svc/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src", 4 | "compilerOptions": { 5 | "assets": ["**/*.proto"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /microservices/users-svc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "users-svc", 3 | "version": "1.0.0", 4 | "description": "gRPC microservice back-end for users. Used for learning/trial purposes only.", 5 | "scripts": { 6 | "prebuild": "rimraf dist", 7 | "build": "nest build", 8 | "start": "nest start", 9 | "start:dev": "nest start --watch", 10 | "start:debug": "nest start --debug --watch", 11 | "start:prod": "node dist/main", 12 | "lint": "eslint '{src,apps,libs,test}/**/*.ts' --fix", 13 | "test": "jest", 14 | "test:watch": "jest --watch", 15 | "test:cov": "jest --coverage", 16 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 17 | "test:e2e": "jest --config ./test/jest-e2e.json" 18 | }, 19 | "dependencies": { 20 | "@grpc/proto-loader": "0.5.3", 21 | "@nestjs/common": "6.11.6", 22 | "@nestjs/config": "0.2.2", 23 | "@nestjs/core": "6.11.6", 24 | "@nestjs/microservices": "6.11.6", 25 | "faker": "4.1.0", 26 | "grpc": "1.24.2", 27 | "lodash": "4.17.15", 28 | "nestjs-pino": "1.1.3", 29 | "pg": "7.18.1", 30 | "pg-hstore": "2.3.3", 31 | "pino": "5.16.0", 32 | "reflect-metadata": "0.1.13", 33 | "rimraf": "3.0.2", 34 | "rxjs": "6.5.4", 35 | "sequelize": "5.21.4", 36 | "sequelize-typescript": "1.1.0" 37 | }, 38 | "devDependencies": { 39 | "@nestjs/cli": "6.14.2", 40 | "@nestjs/schematics": "6.9.3", 41 | "@nestjs/testing": "6.11.6", 42 | "@types/bluebird": "3.5.29", 43 | "@types/faker": "4.1.9", 44 | "@types/jest": "25.1.2", 45 | "@types/lodash": "4.14.149", 46 | "@types/node": "13.7.0", 47 | "@types/validator": "12.0.1", 48 | "@typescript-eslint/eslint-plugin": "2.19.0", 49 | "@typescript-eslint/parser": "2.19.0", 50 | "eslint": "6.8.0", 51 | "eslint-config-airbnb-base": "14.0.0", 52 | "eslint-config-prettier": "6.10.0", 53 | "eslint-plugin-import": "2.20.1", 54 | "eslint-plugin-jest": "23.7.0", 55 | "eslint-plugin-prettier": "3.1.2", 56 | "jest": "25.1.0", 57 | "jest-extended": "0.11.5", 58 | "pino-pretty": "3.5.0", 59 | "prettier": "1.19.1", 60 | "ts-jest": "25.2.0", 61 | "ts-loader": "6.2.1", 62 | "ts-node": "8.6.2", 63 | "tsconfig-paths": "3.9.0", 64 | "typescript": "3.7.5" 65 | }, 66 | "repository": { 67 | "type": "git", 68 | "url": "git+ssh://git@github.com:benjsicam/nestjs-rest-microservices.git" 69 | }, 70 | "author": "Benj Sicam", 71 | "license": "MIT", 72 | "bugs": { 73 | "url": "https://github.com/benjsicam/nestjs-rest-microservices/issues" 74 | }, 75 | "homepage": "https://github.com/benjsicam/nestjs-rest-microservices#readme", 76 | "jest": { 77 | "moduleFileExtensions": [ 78 | "js", 79 | "json", 80 | "ts" 81 | ], 82 | "rootDir": "src", 83 | "testRegex": "tests/*.+(test.ts)", 84 | "transform": { 85 | ".+\\.(t|j)s$": "ts-jest" 86 | }, 87 | "collectCoverage": true, 88 | "coverageThreshold": { 89 | "global": { 90 | "branches": 50, 91 | "functions": 75, 92 | "lines": 75, 93 | "statements": 75 94 | } 95 | }, 96 | "coverageDirectory": "../coverage", 97 | "testEnvironment": "node" 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /microservices/users-svc/src/_proto/commons.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package commons; 4 | 5 | message Id { 6 | string id = 1; 7 | } 8 | 9 | message Name { 10 | string name = 1; 11 | } 12 | 13 | message Query { 14 | repeated string attributes = 1; 15 | string where = 2; 16 | string order = 3; 17 | int32 offset = 4; 18 | int32 limit = 5; 19 | } 20 | 21 | message Count { 22 | int32 count = 1; 23 | } 24 | -------------------------------------------------------------------------------- /microservices/users-svc/src/_proto/users.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package users; 4 | 5 | import "commons.proto"; 6 | 7 | message User { 8 | string id = 1; 9 | string organization = 2; 10 | string loginId = 3; 11 | string avatar = 4; 12 | int32 followers = 5; 13 | int32 following = 6; 14 | string createdAt = 7; 15 | string updatedAt = 8; 16 | int32 version = 9; 17 | } 18 | 19 | message UsersList { 20 | repeated User data = 5; 21 | } 22 | 23 | service UsersService { 24 | rpc findAll (commons.Query) returns (UsersList) {} 25 | rpc count (commons.Query) returns (commons.Count) {} 26 | } 27 | -------------------------------------------------------------------------------- /microservices/users-svc/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { ConfigModule } from '@nestjs/config' 3 | import { LoggerModule } from 'nestjs-pino' 4 | 5 | import { DatabaseModule } from './database/database.module' 6 | import { UsersModule } from './users/users.module' 7 | 8 | @Module({ 9 | imports: [ 10 | ConfigModule.forRoot(), 11 | LoggerModule.forRoot({ 12 | pinoHttp: { 13 | safe: true, 14 | prettyPrint: process.env.NODE_ENV === 'development' 15 | } 16 | }), 17 | DatabaseModule, 18 | UsersModule 19 | ] 20 | }) 21 | export class AppModule {} 22 | -------------------------------------------------------------------------------- /microservices/users-svc/src/commons/interfaces/commons.interface.ts: -------------------------------------------------------------------------------- 1 | export interface Id { 2 | id: string 3 | } 4 | 5 | export interface Name { 6 | name: string 7 | } 8 | 9 | export interface Query { 10 | attributes?: Array 11 | where?: string 12 | order?: string 13 | offset?: number 14 | limit?: number 15 | } 16 | 17 | export interface Count { 18 | count: number 19 | } 20 | -------------------------------------------------------------------------------- /microservices/users-svc/src/database/database.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { LoggerModule } from 'nestjs-pino' 3 | 4 | import { DatabaseProvider } from './database.providers' 5 | 6 | @Module({ 7 | imports: [ 8 | LoggerModule.forRoot({ 9 | pinoHttp: { 10 | safe: true, 11 | prettyPrint: process.env.NODE_ENV === 'development' 12 | } 13 | }) 14 | ], 15 | providers: [DatabaseProvider], 16 | exports: [DatabaseProvider] 17 | }) 18 | export class DatabaseModule {} 19 | -------------------------------------------------------------------------------- /microservices/users-svc/src/database/database.providers.ts: -------------------------------------------------------------------------------- 1 | import { Provider } from '@nestjs/common' 2 | import { Sequelize } from 'sequelize-typescript' 3 | 4 | import { PinoLogger } from 'nestjs-pino' 5 | import { User } from '../users/user.entity' 6 | 7 | export const DatabaseProvider: Provider = { 8 | provide: 'SEQUELIZE', 9 | useFactory: async (logger: PinoLogger) => { 10 | logger.setContext('Sequelize') 11 | 12 | const db: Sequelize = new Sequelize(process.env.DB_NAME, process.env.DB_USER, process.env.DB_PASSWORD, { 13 | dialect: 'postgres', 14 | host: process.env.DB_HOST, 15 | port: parseInt(process.env.DB_PORT || '5432', 10), 16 | logging: logger.info.bind(logger), 17 | benchmark: true, 18 | retry: { 19 | max: 3 20 | } 21 | }) 22 | 23 | db.addModels([User]) 24 | 25 | await db.sync() 26 | 27 | return db 28 | }, 29 | inject: [PinoLogger] 30 | } 31 | -------------------------------------------------------------------------------- /microservices/users-svc/src/main.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path' 2 | 3 | import { NestFactory } from '@nestjs/core' 4 | import { Transport } from '@nestjs/microservices' 5 | import { Logger } from 'nestjs-pino' 6 | 7 | import { AppModule } from './app.module' 8 | import { UsersSeeder } from './users/users.seeder' 9 | 10 | async function bootstrap() { 11 | const app = await NestFactory.createMicroservice(AppModule, { 12 | transport: Transport.GRPC, 13 | options: { 14 | url: `${process.env.URL}:${process.env.PORT}`, 15 | package: 'users', 16 | protoPath: join(__dirname, './_proto/users.proto'), 17 | loader: { 18 | enums: String, 19 | objects: true, 20 | arrays: true 21 | } 22 | } 23 | }) 24 | 25 | app.useLogger(app.get(Logger)) 26 | 27 | const seeder: UsersSeeder = app.get(UsersSeeder) 28 | 29 | await seeder.seedDatabase() 30 | 31 | return app.listenAsync() 32 | } 33 | 34 | bootstrap() 35 | -------------------------------------------------------------------------------- /microservices/users-svc/src/users/user.dto.ts: -------------------------------------------------------------------------------- 1 | export class UserDto { 2 | readonly id?: string 3 | 4 | readonly organization: string 5 | 6 | readonly loginId: string 7 | 8 | readonly avatar?: string 9 | 10 | readonly followers?: number 11 | 12 | readonly following?: number 13 | } 14 | -------------------------------------------------------------------------------- /microservices/users-svc/src/users/user.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, Model, Table, DataType } from 'sequelize-typescript' 2 | 3 | @Table({ 4 | modelName: 'user', 5 | tableName: 'users', 6 | underscored: true, 7 | timestamps: true, 8 | version: true 9 | }) 10 | export class User extends Model { 11 | @Column({ 12 | primaryKey: true, 13 | type: DataType.UUID, 14 | defaultValue: DataType.UUIDV4, 15 | comment: 'The identifier for the user record' 16 | }) 17 | id: string 18 | 19 | @Column({ 20 | type: DataType.UUID, 21 | comment: 'Ref: Organization. The organization the user is associated with' 22 | }) 23 | organization: string 24 | 25 | @Column({ 26 | type: DataType.STRING, 27 | comment: 'The login id of the user' 28 | }) 29 | loginId: string 30 | 31 | @Column({ 32 | type: DataType.STRING, 33 | comment: 'The avatar url of the user' 34 | }) 35 | avatar: string 36 | 37 | @Column({ 38 | type: DataType.INTEGER, 39 | comment: 'The number of followers of the user' 40 | }) 41 | followers: number 42 | 43 | @Column({ 44 | type: DataType.INTEGER, 45 | comment: 'The number of people being followed by the user' 46 | }) 47 | following: number 48 | } 49 | -------------------------------------------------------------------------------- /microservices/users-svc/src/users/users.controller.ts: -------------------------------------------------------------------------------- 1 | import { PinoLogger } from 'nestjs-pino' 2 | import { Controller, Inject } from '@nestjs/common' 3 | import { GrpcMethod } from '@nestjs/microservices' 4 | import { isEmpty } from 'lodash' 5 | 6 | import { Count, Query } from '../commons/interfaces/commons.interface' 7 | import { UsersService, UserServiceQueryResult } from './users.interface' 8 | 9 | import { User } from './user.entity' 10 | 11 | @Controller() 12 | export class UsersController { 13 | constructor(@Inject('UsersService') private readonly usersService: UsersService, private readonly logger: PinoLogger) { 14 | logger.setContext(UsersController.name) 15 | } 16 | 17 | @GrpcMethod('UsersService', 'findAll') 18 | async findAll(query: Query): Promise { 19 | this.logger.info('UsersController#findAll.call', query) 20 | 21 | const result: Array = await this.usersService.findAll({ 22 | attributes: !isEmpty(query.attributes) ? query.attributes : undefined, 23 | where: !isEmpty(query.where) ? JSON.parse(query.where) : undefined, 24 | order: !isEmpty(query.order) ? JSON.parse(query.order) : undefined, 25 | offset: query.offset ? query.offset : 0, 26 | limit: query.limit ? query.limit : 25 27 | }) 28 | 29 | this.logger.info('UsersController#findAll.result', result) 30 | 31 | return { data: result } 32 | } 33 | 34 | @GrpcMethod('UsersService', 'count') 35 | async count(query: Query): Promise { 36 | this.logger.info('UsersController#count.call', query) 37 | 38 | const count: number = await this.usersService.count({ 39 | where: !isEmpty(query.where) ? JSON.parse(query.where) : undefined 40 | }) 41 | 42 | this.logger.info('UsersController#count.result', count) 43 | 44 | return { count } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /microservices/users-svc/src/users/users.interface.ts: -------------------------------------------------------------------------------- 1 | import { FindOptions } from 'sequelize/types' 2 | 3 | import { User } from './user.entity' 4 | import { UserDto } from './user.dto' 5 | 6 | export interface UserServiceQueryResult { 7 | data: Array 8 | } 9 | 10 | export interface UsersService { 11 | findAll(query?: FindOptions): Promise> 12 | count(query?: FindOptions): Promise 13 | create(user: UserDto): Promise 14 | } 15 | -------------------------------------------------------------------------------- /microservices/users-svc/src/users/users.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { LoggerModule } from 'nestjs-pino' 3 | 4 | import { User } from './user.entity' 5 | import { UsersController } from './users.controller' 6 | import { UsersServiceImpl } from './users.service' 7 | import { UsersSeeder } from './users.seeder' 8 | 9 | @Module({ 10 | imports: [ 11 | LoggerModule.forRoot({ 12 | pinoHttp: { 13 | safe: true, 14 | prettyPrint: process.env.NODE_ENV === 'development' 15 | } 16 | }) 17 | ], 18 | controllers: [UsersController], 19 | providers: [UsersSeeder, { provide: 'UsersService', useClass: UsersServiceImpl }, { provide: 'UsersRepository', useValue: User }] 20 | }) 21 | export class UsersModule {} 22 | -------------------------------------------------------------------------------- /microservices/users-svc/src/users/users.seeder.ts: -------------------------------------------------------------------------------- 1 | import { PinoLogger } from 'nestjs-pino' 2 | import { Inject, Injectable } from '@nestjs/common' 3 | import { image, internet, random } from 'faker' 4 | import { times } from 'lodash' 5 | 6 | import { UsersService } from './users.interface' 7 | import { User } from './user.entity' 8 | 9 | @Injectable() 10 | export class UsersSeeder { 11 | private readonly ORGS: Array = ['62a1c874-1f3f-4e24-a553-05289eea6332', 'f891fa17-d33f-49cb-baea-ced2539fa574'] 12 | 13 | constructor(@Inject('UsersService') private readonly service: UsersService, private readonly logger: PinoLogger) { 14 | logger.setContext(UsersSeeder.name) 15 | } 16 | 17 | async seedDatabase(): Promise { 18 | const recordCount: number = await this.service.count() 19 | 20 | if (recordCount > 0) { 21 | this.logger.info('UsersSeeder#seedDatabase', 'Aborting...') 22 | 23 | return recordCount 24 | } 25 | 26 | const numOfRecords: number = random.number({ min: 10, max: 30 }) 27 | 28 | this.logger.info('UsersSeeder#seedDatabase.numOfRecords', numOfRecords) 29 | 30 | times(numOfRecords, async () => { 31 | const user: User = await this.service.create({ 32 | organization: random.arrayElement(this.ORGS), 33 | loginId: internet.userName(), 34 | avatar: image.avatar(), 35 | followers: random.number({ min: 1, max: 500 }), 36 | following: random.number({ min: 1, max: 500 }) 37 | }) 38 | 39 | this.logger.info('UsersSeeder#seedDatabase.newRecord', user) 40 | }) 41 | 42 | return numOfRecords 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /microservices/users-svc/src/users/users.service.ts: -------------------------------------------------------------------------------- 1 | import { PinoLogger } from 'nestjs-pino' 2 | import { Inject, Injectable } from '@nestjs/common' 3 | import { FindOptions } from 'sequelize/types' 4 | 5 | import { UsersService } from './users.interface' 6 | 7 | import { User } from './user.entity' 8 | import { UserDto } from './user.dto' 9 | 10 | @Injectable() 11 | export class UsersServiceImpl implements UsersService { 12 | constructor(@Inject('UsersRepository') private readonly repo: typeof User, private readonly logger: PinoLogger) { 13 | logger.setContext(UsersServiceImpl.name) 14 | } 15 | 16 | async findAll(query?: FindOptions): Promise> { 17 | this.logger.info('UsersService#findAll.call', query) 18 | 19 | const result: Array = await this.repo.findAll(query) 20 | 21 | this.logger.info('UsersService#findAll.result', result) 22 | 23 | return result 24 | } 25 | 26 | async count(query?: FindOptions): Promise { 27 | this.logger.info('UsersService#count.call', query) 28 | 29 | const result: number = await this.repo.count(query) 30 | 31 | this.logger.info('UsersService#count.result', result) 32 | 33 | return result 34 | } 35 | 36 | async create(userDto: UserDto): Promise { 37 | this.logger.info('UsersService#create.call', userDto) 38 | 39 | const user = new User(userDto) 40 | 41 | const result = await user.save() 42 | 43 | this.logger.info('UsersService#create.result', result) 44 | 45 | return result 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /microservices/users-svc/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /microservices/users-svc/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "target": "es2017", 9 | "sourceMap": true, 10 | "outDir": "./dist", 11 | "baseUrl": "./", 12 | "incremental": true 13 | }, 14 | "assets": ["_proto"], 15 | "exclude": ["node_modules", "dist"] 16 | } 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nestjs-api-microservices", 3 | "version": "1.0.0", 4 | "description": "A sample REST API Gateway with gRPC back-end microservices built using the NestJS framework.", 5 | "scripts": { 6 | "docs:proto-gen": "./scripts/generate-proto-docs.sh", 7 | "install": "./scripts/install.sh", 8 | "lint": "./scripts/lint.sh", 9 | "build": "./scripts/build.sh", 10 | "docker:build": "docker-compose build", 11 | "docker:start": "docker-compose up", 12 | "docker:teardown": "docker-compose down -v", 13 | "start": "npm run install && npm run lint && npm run build && npm run docker:build && npm run docker:start" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+ssh://git@github.com:benjsicam/nestjs-rest-microservices.git" 18 | }, 19 | "author": "Benj Sicam", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/benjsicam/nestjs-rest-microservices/issues" 23 | }, 24 | "homepage": "https://github.com/benjsicam/nestjs-rest-microservices#readme" 25 | } 26 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd api-gateway && npm run build && cd - 4 | cd microservices/comments-svc && npm run build && cd - 5 | cd microservices/organizations-svc && npm run build && cd - 6 | cd microservices/users-svc && npm run build && cd - 7 | -------------------------------------------------------------------------------- /scripts/generate-proto-docs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker run --rm \ 4 | -v $PWD/docs:/out \ 5 | -v $PWD/_proto:/protos \ 6 | pseudomuto/protoc-gen-doc --doc_opt=markdown,proto-docs.md 7 | -------------------------------------------------------------------------------- /scripts/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd api-gateway && npm i && cd - 4 | cd microservices/comments-svc && npm i && cd - 5 | cd microservices/organizations-svc && npm i && cd - 6 | cd microservices/users-svc && npm i && cd - 7 | -------------------------------------------------------------------------------- /scripts/lint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd api-gateway && npm run lint && cd - 4 | cd microservices/comments-svc && npm run lint && cd - 5 | cd microservices/organizations-svc && npm run lint && cd - 6 | cd microservices/users-svc && npm run lint && cd - 7 | --------------------------------------------------------------------------------