├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── _proto ├── comment.proto ├── commons.proto ├── mailer.proto ├── post.proto └── user.proto ├── api-gateway ├── .env.example ├── .eslintrc.yaml ├── .gitlab-ci.yml ├── Dockerfile ├── jest.config.js ├── nest-cli.json ├── package-lock.json ├── package.json ├── src │ ├── _proto │ │ ├── comment.proto │ │ ├── commons.proto │ │ ├── mailer.proto │ │ ├── post.proto │ │ └── user.proto │ ├── app.module.ts │ ├── auth │ │ ├── auth.module.ts │ │ ├── auth.resolver.ts │ │ ├── auth.service.ts │ │ ├── gql-auth.guard.ts │ │ ├── jwt-refresh.strategy.ts │ │ ├── jwt.strategy.ts │ │ ├── refresh-auth.guard.ts │ │ └── user.decorator.ts │ ├── comments │ │ ├── comment.dto.ts │ │ ├── comments-mutation.resolver.ts │ │ ├── comments-query.resolver.ts │ │ ├── comments-subscription.resolver.ts │ │ ├── comments-type.resolver.ts │ │ ├── comments.interface.ts │ │ └── comments.module.ts │ ├── commons │ │ ├── commons.interface.ts │ │ └── commons.module.ts │ ├── graphql │ │ ├── generate-typings.ts │ │ ├── playground-query.ts │ │ ├── schema │ │ │ ├── _mutation.schema.graphql │ │ │ ├── _query.schema.graphql │ │ │ ├── _scalar.schema.graphql │ │ │ ├── _subscription.schema.graphql │ │ │ ├── comment.schema.graphql │ │ │ ├── commons.schema.graphql │ │ │ ├── post.schema.graphql │ │ │ └── user.schema.graphql │ │ └── typings.ts │ ├── main.ts │ ├── posts │ │ ├── post.dto.ts │ │ ├── posts-mutation.resolver.ts │ │ ├── posts-query.resolver.ts │ │ ├── posts-subscription.resolver.ts │ │ ├── posts-type.resolver.ts │ │ ├── posts.interface.ts │ │ └── posts.module.ts │ ├── users │ │ ├── user.dto.ts │ │ ├── users-mutation.resolver.ts │ │ ├── users-query.resolver.ts │ │ ├── users-type.resolver.ts │ │ ├── users.interface.ts │ │ └── users.module.ts │ └── utils │ │ ├── password.utils.ts │ │ ├── query.utils.ts │ │ └── utils.module.ts ├── tsconfig.build.json └── tsconfig.json ├── docker-compose.yaml ├── docs ├── img │ ├── archi-diagram.png │ └── graph-model.png └── proto │ └── docs.md ├── k8s ├── configuration │ ├── api-gateway.yaml │ ├── cache.yaml │ ├── comments-svc.yaml │ ├── database.yaml │ ├── jwt.yaml │ ├── posts-svc.yaml │ ├── smtp.yaml │ └── users-svc.yaml ├── services │ ├── api-gateway.yaml │ ├── comments-svc.yaml │ ├── mailer-svc.yaml │ ├── posts-svc.yaml │ └── users-svc.yaml └── workloads │ ├── api-gateway.yaml │ ├── comments-svc.yaml │ ├── mailer-svc.yaml │ ├── posts-svc.yaml │ └── users-svc.yaml ├── microservices ├── comments-svc │ ├── .env.example │ ├── .eslintrc.yaml │ ├── .gitlab-ci.yml │ ├── Dockerfile │ ├── docker-compose.yaml │ ├── jest.config.js │ ├── nest-cli.json │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── _proto │ │ │ ├── README.md │ │ │ ├── comment.proto │ │ │ ├── commons.proto │ │ │ ├── mailer.proto │ │ │ ├── post.proto │ │ │ └── user.proto │ │ ├── app.module.ts │ │ ├── comments │ │ │ ├── comment.dto.ts │ │ │ ├── comment.model.ts │ │ │ ├── comments.controller.ts │ │ │ ├── comments.interface.ts │ │ │ ├── comments.module.ts │ │ │ └── comments.service.ts │ │ ├── commons │ │ │ ├── commons.interface.ts │ │ │ ├── cursor-pagination.interface.ts │ │ │ └── find-and-paginate.interface.ts │ │ └── main.ts │ ├── tsconfig.build.json │ └── tsconfig.json ├── mailer-svc │ ├── .env.example │ ├── .eslintrc.yaml │ ├── .gitlab-ci.yml │ ├── Dockerfile │ ├── docker-compose.yaml │ ├── jest.config.js │ ├── nest-cli.json │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── _proto │ │ │ ├── README.md │ │ │ ├── comment.proto │ │ │ ├── commons.proto │ │ │ ├── mailer.proto │ │ │ ├── post.proto │ │ │ └── user.proto │ │ ├── app.module.ts │ │ ├── mailer │ │ │ ├── mailer.controller.ts │ │ │ ├── mailer.interface.ts │ │ │ └── mailer.module.ts │ │ ├── main.ts │ │ └── templates │ │ │ ├── new-comment.pug │ │ │ ├── signup.pug │ │ │ ├── update-email.pug │ │ │ └── update-password.pug │ ├── tsconfig.build.json │ └── tsconfig.json ├── posts-svc │ ├── .env.example │ ├── .eslintrc.yaml │ ├── .gitlab-ci.yml │ ├── Dockerfile │ ├── docker-compose.yaml │ ├── jest.config.js │ ├── nest-cli.json │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── _proto │ │ │ ├── README.md │ │ │ ├── comment.proto │ │ │ ├── commons.proto │ │ │ ├── mailer.proto │ │ │ ├── post.proto │ │ │ └── user.proto │ │ ├── app.module.ts │ │ ├── commons │ │ │ ├── commons.interface.ts │ │ │ ├── cursor-pagination.interface.ts │ │ │ └── find-and-paginate.interface.ts │ │ ├── main.ts │ │ └── posts │ │ │ ├── post.dto.ts │ │ │ ├── post.model.ts │ │ │ ├── posts.controller.ts │ │ │ ├── posts.interface.ts │ │ │ ├── posts.module.ts │ │ │ └── posts.service.ts │ ├── tsconfig.build.json │ └── tsconfig.json └── users-svc │ ├── .env.example │ ├── .eslintrc.yaml │ ├── .gitlab-ci.yml │ ├── Dockerfile │ ├── docker-compose.yaml │ ├── jest.config.js │ ├── nest-cli.json │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── _proto │ │ ├── README.md │ │ ├── comment.proto │ │ ├── commons.proto │ │ ├── mailer.proto │ │ ├── post.proto │ │ └── user.proto │ ├── app.module.ts │ ├── commons │ │ ├── commons.interface.ts │ │ ├── cursor-pagination.interface.ts │ │ └── find-and-paginate.interface.ts │ ├── main.ts │ └── users │ │ ├── user.dto.ts │ │ ├── user.model.ts │ │ ├── users.controller.ts │ │ ├── users.interface.ts │ │ ├── users.module.ts │ │ └── users.service.ts │ ├── tsconfig.build.json │ └── tsconfig.json ├── package.json └── scripts ├── build.sh ├── generate-jwt-secret.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 Dist 2 | dist 3 | dist-test 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | 24 | # nyc test coverage 25 | .nyc_output 26 | 27 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 28 | .grunt 29 | 30 | # Bower dependency directory (https://bower.io/) 31 | bower_components 32 | 33 | # node-waf configuration 34 | .lock-wscript 35 | 36 | # Compiled binary addons (https://nodejs.org/api/addons.html) 37 | build/Release 38 | 39 | # Dependency directories 40 | node_modules/ 41 | jspm_packages/ 42 | 43 | # TypeScript v1 declaration files 44 | typings/ 45 | 46 | # Optional npm cache directory 47 | .npm 48 | 49 | # Optional eslint cache 50 | .eslintcache 51 | 52 | # Optional REPL history 53 | .node_repl_history 54 | 55 | # Output of 'npm pack' 56 | *.tgz 57 | 58 | # Yarn Integrity file 59 | .yarn-integrity 60 | 61 | # dotenv environment variables file 62 | .env 63 | 64 | # next.js build output 65 | .next 66 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 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 | -------------------------------------------------------------------------------- /_proto/comment.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package comment; 4 | 5 | import "commons.proto"; 6 | 7 | message Comment { 8 | string id = 1; 9 | string text = 2; 10 | string author = 3; 11 | string post = 4; 12 | string createdAt = 5; 13 | string updatedAt = 6; 14 | int32 version = 7; 15 | } 16 | 17 | message CommentEdge { 18 | Comment node = 1; 19 | string cursor = 2; 20 | } 21 | 22 | message CreateCommentInput { 23 | string text = 1; 24 | string author = 2; 25 | string post = 3; 26 | } 27 | 28 | message UpdateCommentInput { 29 | string id = 1; 30 | Comment data = 2; 31 | } 32 | 33 | message FindCommentsPayload { 34 | repeated CommentEdge edges = 1; 35 | commons.PageInfo pageInfo = 2; 36 | } 37 | 38 | service CommentsService { 39 | rpc find (commons.Query) returns (FindCommentsPayload) {} 40 | rpc findById (commons.Id) returns (Comment) {} 41 | rpc findOne (commons.Query) returns (Comment) {} 42 | rpc count (commons.Query) returns (commons.Count) {} 43 | rpc create (CreateCommentInput) returns (Comment) {} 44 | rpc update (UpdateCommentInput) returns (Comment) {} 45 | rpc destroy (commons.Query) returns (commons.Count) {} 46 | } 47 | -------------------------------------------------------------------------------- /_proto/commons.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package commons; 4 | 5 | message Id { 6 | string id = 1; 7 | } 8 | 9 | message Query { 10 | repeated string select = 1; 11 | string where = 2; 12 | repeated string orderBy = 3; 13 | int32 limit = 4; 14 | string before = 5; 15 | string after = 6; 16 | } 17 | 18 | message PageInfo { 19 | string startCursor = 1; 20 | string endCursor = 2; 21 | bool hasNextPage = 3; 22 | bool hasPreviousPage = 4; 23 | } 24 | 25 | message Count { 26 | int32 count = 1; 27 | } 28 | -------------------------------------------------------------------------------- /_proto/mailer.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package mailer; 4 | 5 | message SendMailInput { 6 | string template = 1; 7 | string to = 2; 8 | bytes data = 3; 9 | } 10 | 11 | message SendMailPayload { 12 | bool isSent = 1; 13 | } 14 | 15 | service MailerService { 16 | rpc send (SendMailInput) returns (SendMailPayload) {} 17 | } 18 | -------------------------------------------------------------------------------- /_proto/post.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package post; 4 | 5 | import "commons.proto"; 6 | 7 | message Post { 8 | string id = 1; 9 | string title = 2; 10 | string body = 3; 11 | bool published = 4; 12 | string author = 5; 13 | string createdAt = 6; 14 | string updatedAt = 7; 15 | int32 version = 8; 16 | } 17 | 18 | message PostEdge { 19 | Post node = 1; 20 | string cursor = 2; 21 | } 22 | 23 | message CreatePostInput { 24 | string title = 2; 25 | string body = 3; 26 | bool published = 4; 27 | string author = 5; 28 | } 29 | 30 | message UpdatePostInput { 31 | string id = 1; 32 | Post data = 2; 33 | } 34 | 35 | message FindPostsPayload { 36 | repeated PostEdge edges = 1; 37 | commons.PageInfo pageInfo = 2; 38 | } 39 | 40 | service PostsService { 41 | rpc find (commons.Query) returns (FindPostsPayload) {} 42 | rpc findById (commons.Id) returns (Post) {} 43 | rpc findOne (commons.Query) returns (Post) {} 44 | rpc count (commons.Query) returns (commons.Count) {} 45 | rpc create (CreatePostInput) returns (Post) {} 46 | rpc update (UpdatePostInput) returns (Post) {} 47 | rpc destroy (commons.Query) returns (commons.Count) {} 48 | } 49 | -------------------------------------------------------------------------------- /_proto/user.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package user; 4 | 5 | import "commons.proto"; 6 | 7 | message User { 8 | string id = 1; 9 | string name = 2; 10 | string email = 3; 11 | string password = 4; 12 | int32 age = 5; 13 | string createdAt = 6; 14 | string updatedAt = 7; 15 | int32 version = 8; 16 | } 17 | 18 | message UserEdge { 19 | User node = 1; 20 | string cursor = 2; 21 | } 22 | 23 | message CreateUserInput { 24 | string name = 1; 25 | string email = 2; 26 | string password = 3; 27 | int32 age = 4; 28 | } 29 | 30 | message UpdateUserInput { 31 | string id = 1; 32 | User data = 2; 33 | } 34 | 35 | message FindUsersPayload { 36 | repeated UserEdge edges = 1; 37 | commons.PageInfo pageInfo = 2; 38 | } 39 | 40 | service UsersService { 41 | rpc find (commons.Query) returns (FindUsersPayload) {} 42 | rpc findById (commons.Id) returns (User) {} 43 | rpc findOne (commons.Query) returns (User) {} 44 | rpc count (commons.Query) returns (commons.Count) {} 45 | rpc create (CreateUserInput) returns (User) {} 46 | rpc update (UpdateUserInput) returns (User) {} 47 | rpc destroy (commons.Query) returns (commons.Count) {} 48 | } 49 | -------------------------------------------------------------------------------- /api-gateway/.env.example: -------------------------------------------------------------------------------- 1 | # Environment 2 | NODE_ENV= 3 | 4 | # GraphQL Server Options 5 | GRAPHQL_PORT= 6 | 7 | # JWT 8 | JWT_ACCESSTOKEN_SECRET= 9 | JWT_REFRESHTOKEN_SECRET= 10 | JWT_ISSUER= 11 | JWT_AUDIENCE= 12 | 13 | # Cache 14 | REDIS_HOST= 15 | REDIS_PORT= 16 | REDIS_PASSWORD= 17 | 18 | # Service URLs 19 | COMMENTS_SVC_URL= 20 | POSTS_SVC_URL= 21 | USERS_SVC_URL= 22 | MAILER_SVC_URL= 23 | -------------------------------------------------------------------------------- /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 | - "plugin:@typescript-eslint/eslint-recommended" 12 | - "plugin:@typescript-eslint/recommended" 13 | - "prettier" 14 | - "prettier/@typescript-eslint" 15 | - "plugin:prettier/recommended" 16 | - "plugin:jest/recommended" 17 | - "plugin:import/errors" 18 | - "plugin:import/warnings" 19 | - "plugin:import/typescript" 20 | root: true 21 | env: 22 | node: true 23 | jest: true 24 | rules: 25 | "@typescript-eslint/ban-ts-comment": off 26 | "@typescript-eslint/interface-name-prefix": off 27 | "@typescript-eslint/explicit-function-return-type": off 28 | "@typescript-eslint/no-explicit-any": off 29 | "@typescript-eslint/explicit-module-boundary-types": off 30 | class-methods-use-this: 0 31 | import/prefer-default-export: 0 32 | import/extensions: 33 | - error 34 | - ignorePackages 35 | - js: never 36 | jsx: never 37 | ts: never 38 | tsx: never 39 | prettier/prettier: 40 | - "error" 41 | - parser: "typescript" 42 | printWidth: 200 43 | semi: false 44 | singleQuote: true 45 | endOfline: "lf" 46 | trailingComma: "none" 47 | 48 | 49 | -------------------------------------------------------------------------------- /api-gateway/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12-alpine as build 2 | 3 | WORKDIR /usr/share/api-gateway 4 | 5 | COPY dist package.json ./ 6 | 7 | RUN npm install --production 8 | 9 | FROM node:12-alpine 10 | 11 | WORKDIR /usr/share/api-gateway 12 | 13 | COPY --from=build /usr/share/api-gateway . 14 | 15 | EXPOSE 3000 16 | 17 | CMD ["node", "main.js"] 18 | -------------------------------------------------------------------------------- /api-gateway/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | setupFilesAfterEnv: [ 3 | "jest-extended" 4 | ], 5 | coverageThreshold: { 6 | global: { 7 | branches: 90, 8 | functions: 90, 9 | lines: 90, 10 | statements: 90 11 | } 12 | }, 13 | collectCoverageFrom: [ 14 | "src/**/*.ts", 15 | "!**/node_modules/**" 16 | ], 17 | "rootDir": "src", 18 | "preset": "ts-jest", 19 | testEnvironment: "node" 20 | } 21 | -------------------------------------------------------------------------------- /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": "GraphQL 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 | "lint": "eslint --max-warnings=0 '{src,__tests__}/**/*.ts'", 12 | "lint:fix": "eslint --fix '{src,__tests__}/**/*.ts'", 13 | "test": "jest", 14 | "test:watch": "jest --watch", 15 | "test:coverage": "jest --coverage", 16 | "generate:typings": "ts-node ./src/graphql/generate-typings", 17 | "copy:protos": "cpy ../_proto ./src/_proto" 18 | }, 19 | "dependencies": { 20 | "@grpc/proto-loader": "0.5.4", 21 | "@nestjs/common": "7.1.1", 22 | "@nestjs/config": "0.5.0", 23 | "@nestjs/core": "7.1.1", 24 | "@nestjs/graphql": "7.3.11", 25 | "@nestjs/jwt": "7.0.0", 26 | "@nestjs/microservices": "7.1.1", 27 | "@nestjs/passport": "7.0.0", 28 | "@nestjs/platform-express": "7.1.1", 29 | "apollo-server-express": "2.14.0", 30 | "bcryptjs": "2.4.3", 31 | "cookie-parser": "1.4.5", 32 | "cors": "2.8.5", 33 | "graphql": "15.0.0", 34 | "graphql-redis-subscriptions": "2.2.1", 35 | "graphql-scalars": "1.1.3", 36 | "graphql-subscriptions": "1.1.0", 37 | "graphql-tools": "6.0.3", 38 | "graphql-type-json": "0.3.1", 39 | "grpc": "1.24.2", 40 | "ioredis": "4.17.1", 41 | "lodash": "4.17.15", 42 | "nestjs-pino": "1.2.0", 43 | "passport": "0.4.1", 44 | "passport-jwt": "4.0.0", 45 | "pino": "6.3.0", 46 | "reflect-metadata": "0.1.13", 47 | "rxjs": "6.5.5" 48 | }, 49 | "devDependencies": { 50 | "@nestjs/cli": "7.2.0", 51 | "@nestjs/schematics": "7.0.0", 52 | "@nestjs/testing": "7.1.1", 53 | "@types/bcryptjs": "2.4.2", 54 | "@types/faker": "4.1.12", 55 | "@types/ioredis": "4.16.2", 56 | "@types/jest": "25.2.3", 57 | "@types/lodash": "4.14.153", 58 | "@types/node": "14.0.5", 59 | "@types/passport-jwt": "3.0.3", 60 | "@typescript-eslint/eslint-plugin": "3.0.2", 61 | "@typescript-eslint/parser": "3.0.2", 62 | "cpy-cli": "3.1.1", 63 | "eslint": "7.1.0", 64 | "eslint-config-prettier": "6.11.0", 65 | "eslint-plugin-import": "2.20.2", 66 | "eslint-plugin-jest": "23.13.2", 67 | "eslint-plugin-prettier": "3.1.3", 68 | "faker": "4.1.0", 69 | "jest": "26.0.1", 70 | "jest-extended": "0.11.5", 71 | "pino-pretty": "4.0.0", 72 | "prettier": "2.0.5", 73 | "rimraf": "3.0.2", 74 | "ts-jest": "26.0.0", 75 | "ts-node": "8.10.1", 76 | "typescript": "3.9.3" 77 | }, 78 | "repository": { 79 | "type": "git", 80 | "url": "git+ssh://git@github.com:benjsicam/nestjs-graphql-microservices.git" 81 | }, 82 | "keywords": [ 83 | "graphql", 84 | "microservices", 85 | "grpc" 86 | ], 87 | "author": "Benj Sicam (https://github.com/benjsicam)", 88 | "license": "MIT", 89 | "bugs": { 90 | "url": "https://github.com/benjsicam/nestjs-graphql-microservices/issues" 91 | }, 92 | "homepage": "https://github.com/benjsicam/nestjs-graphql-microservices#readme" 93 | } 94 | -------------------------------------------------------------------------------- /api-gateway/src/_proto/comment.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package comment; 4 | 5 | import "commons.proto"; 6 | 7 | message Comment { 8 | string id = 1; 9 | string text = 2; 10 | string author = 3; 11 | string post = 4; 12 | string createdAt = 5; 13 | string updatedAt = 6; 14 | int32 version = 7; 15 | } 16 | 17 | message CommentEdge { 18 | Comment node = 1; 19 | string cursor = 2; 20 | } 21 | 22 | message CreateCommentInput { 23 | string text = 1; 24 | string author = 2; 25 | string post = 3; 26 | } 27 | 28 | message UpdateCommentInput { 29 | string id = 1; 30 | Comment data = 2; 31 | } 32 | 33 | message FindCommentsPayload { 34 | repeated CommentEdge edges = 1; 35 | commons.PageInfo pageInfo = 2; 36 | } 37 | 38 | service CommentsService { 39 | rpc find (commons.Query) returns (FindCommentsPayload) {} 40 | rpc findById (commons.Id) returns (Comment) {} 41 | rpc findOne (commons.Query) returns (Comment) {} 42 | rpc count (commons.Query) returns (commons.Count) {} 43 | rpc create (CreateCommentInput) returns (Comment) {} 44 | rpc update (UpdateCommentInput) returns (Comment) {} 45 | rpc destroy (commons.Query) returns (commons.Count) {} 46 | } 47 | -------------------------------------------------------------------------------- /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 Query { 10 | repeated string select = 1; 11 | string where = 2; 12 | repeated string orderBy = 3; 13 | int32 limit = 4; 14 | string before = 5; 15 | string after = 6; 16 | } 17 | 18 | message PageInfo { 19 | string startCursor = 1; 20 | string endCursor = 2; 21 | bool hasNextPage = 3; 22 | bool hasPreviousPage = 4; 23 | } 24 | 25 | message Count { 26 | int32 count = 1; 27 | } 28 | -------------------------------------------------------------------------------- /api-gateway/src/_proto/mailer.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package mailer; 4 | 5 | message SendMailInput { 6 | string template = 1; 7 | string to = 2; 8 | bytes data = 3; 9 | } 10 | 11 | message SendMailPayload { 12 | bool isSent = 1; 13 | } 14 | 15 | service MailerService { 16 | rpc send (SendMailInput) returns (SendMailPayload) {} 17 | } 18 | -------------------------------------------------------------------------------- /api-gateway/src/_proto/post.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package post; 4 | 5 | import "commons.proto"; 6 | 7 | message Post { 8 | string id = 1; 9 | string title = 2; 10 | string body = 3; 11 | bool published = 4; 12 | string author = 5; 13 | string createdAt = 6; 14 | string updatedAt = 7; 15 | int32 version = 8; 16 | } 17 | 18 | message PostEdge { 19 | Post node = 1; 20 | string cursor = 2; 21 | } 22 | 23 | message CreatePostInput { 24 | string title = 2; 25 | string body = 3; 26 | bool published = 4; 27 | string author = 5; 28 | } 29 | 30 | message UpdatePostInput { 31 | string id = 1; 32 | Post data = 2; 33 | } 34 | 35 | message FindPostsPayload { 36 | repeated PostEdge edges = 1; 37 | commons.PageInfo pageInfo = 2; 38 | } 39 | 40 | service PostsService { 41 | rpc find (commons.Query) returns (FindPostsPayload) {} 42 | rpc findById (commons.Id) returns (Post) {} 43 | rpc findOne (commons.Query) returns (Post) {} 44 | rpc count (commons.Query) returns (commons.Count) {} 45 | rpc create (CreatePostInput) returns (Post) {} 46 | rpc update (UpdatePostInput) returns (Post) {} 47 | rpc destroy (commons.Query) returns (commons.Count) {} 48 | } 49 | -------------------------------------------------------------------------------- /api-gateway/src/_proto/user.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package user; 4 | 5 | import "commons.proto"; 6 | 7 | message User { 8 | string id = 1; 9 | string name = 2; 10 | string email = 3; 11 | string password = 4; 12 | int32 age = 5; 13 | string createdAt = 6; 14 | string updatedAt = 7; 15 | int32 version = 8; 16 | } 17 | 18 | message UserEdge { 19 | User node = 1; 20 | string cursor = 2; 21 | } 22 | 23 | message CreateUserInput { 24 | string name = 1; 25 | string email = 2; 26 | string password = 3; 27 | int32 age = 4; 28 | } 29 | 30 | message UpdateUserInput { 31 | string id = 1; 32 | User data = 2; 33 | } 34 | 35 | message FindUsersPayload { 36 | repeated UserEdge edges = 1; 37 | commons.PageInfo pageInfo = 2; 38 | } 39 | 40 | service UsersService { 41 | rpc find (commons.Query) returns (FindUsersPayload) {} 42 | rpc findById (commons.Id) returns (User) {} 43 | rpc findOne (commons.Query) returns (User) {} 44 | rpc count (commons.Query) returns (commons.Count) {} 45 | rpc create (CreateUserInput) returns (User) {} 46 | rpc update (UpdateUserInput) returns (User) {} 47 | rpc destroy (commons.Query) returns (commons.Count) {} 48 | } 49 | -------------------------------------------------------------------------------- /api-gateway/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path' 2 | 3 | import { Module } from '@nestjs/common' 4 | import { ConfigModule, ConfigService } from '@nestjs/config' 5 | import { GraphQLModule, GqlModuleOptions } from '@nestjs/graphql' 6 | 7 | import { LoggerModule, PinoLogger } from 'nestjs-pino' 8 | 9 | import { DateTimeResolver, EmailAddressResolver, UnsignedIntResolver } from 'graphql-scalars' 10 | import { GraphQLJSONObject } from 'graphql-type-json' 11 | 12 | import { AuthModule } from './auth/auth.module' 13 | import { CommentsModule } from './comments/comments.module' 14 | import { PostsModule } from './posts/posts.module' 15 | import { UsersModule } from './users/users.module' 16 | 17 | import { playgroundQuery } from './graphql/playground-query' 18 | 19 | @Module({ 20 | imports: [ 21 | ConfigModule.forRoot(), 22 | LoggerModule.forRootAsync({ 23 | imports: [ConfigModule], 24 | useFactory: async (configService: ConfigService) => ({ 25 | pinoHttp: { 26 | safe: true, 27 | prettyPrint: configService.get('NODE_ENV') !== 'production' 28 | } 29 | }), 30 | inject: [ConfigService] 31 | }), 32 | GraphQLModule.forRootAsync({ 33 | imports: [LoggerModule], 34 | useFactory: async (logger: PinoLogger): Promise => ({ 35 | path: '/', 36 | subscriptions: '/', 37 | typePaths: ['./**/*.graphql'], 38 | resolvers: { 39 | DateTime: DateTimeResolver, 40 | EmailAddress: EmailAddressResolver, 41 | UnsignedInt: UnsignedIntResolver, 42 | JSONObject: GraphQLJSONObject 43 | }, 44 | definitions: { 45 | path: join(__dirname, 'graphql.ts') 46 | }, 47 | logger, 48 | debug: true, 49 | cors: false, 50 | installSubscriptionHandlers: true, 51 | playground: { 52 | endpoint: '/', 53 | subscriptionEndpoint: '/', 54 | settings: { 55 | 'request.credentials': 'include' 56 | }, 57 | tabs: [ 58 | { 59 | name: 'GraphQL API', 60 | endpoint: '/', 61 | query: playgroundQuery 62 | } 63 | ] 64 | }, 65 | context: ({ req, res }): any => ({ req, res }) 66 | }), 67 | inject: [PinoLogger] 68 | }), 69 | AuthModule, 70 | UsersModule, 71 | PostsModule, 72 | CommentsModule 73 | ] 74 | }) 75 | export class AppModule {} 76 | -------------------------------------------------------------------------------- /api-gateway/src/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, forwardRef } from '@nestjs/common' 2 | import { LoggerModule } from 'nestjs-pino' 3 | import { ConfigModule, ConfigService } from '@nestjs/config' 4 | import { PassportModule } from '@nestjs/passport' 5 | import { JwtService } from '@nestjs/jwt' 6 | 7 | import { AuthService } from './auth.service' 8 | import { AuthResolver } from './auth.resolver' 9 | import { JwtStrategy } from './jwt.strategy' 10 | import { JwtRefreshStrategy } from './jwt-refresh.strategy' 11 | 12 | import { UsersModule } from '../users/users.module' 13 | import { UtilsModule } from '../utils/utils.module' 14 | 15 | @Module({ 16 | imports: [ConfigModule, LoggerModule, UtilsModule, PassportModule.register({ defaultStrategy: 'jwt' }), forwardRef(() => UsersModule)], 17 | providers: [ 18 | AuthService, 19 | JwtStrategy, 20 | JwtRefreshStrategy, 21 | AuthResolver, 22 | { 23 | provide: 'JwtAccessTokenService', 24 | inject: [ConfigService], 25 | useFactory: (configService: ConfigService): JwtService => { 26 | return new JwtService({ 27 | secret: configService.get('JWT_ACCESSTOKEN_SECRET'), 28 | signOptions: { 29 | audience: configService.get('JWT_AUDIENCE'), 30 | issuer: configService.get('JWT_ISSUER'), 31 | expiresIn: '30min' 32 | } 33 | }) 34 | } 35 | }, 36 | { 37 | provide: 'JwtRefreshTokenService', 38 | inject: [ConfigService], 39 | useFactory: (configService: ConfigService): JwtService => { 40 | return new JwtService({ 41 | secret: configService.get('JWT_REFRESHTOKEN_SECRET'), 42 | signOptions: { 43 | audience: configService.get('JWT_AUDIENCE'), 44 | issuer: configService.get('JWT_ISSUER'), 45 | expiresIn: '30min' 46 | } 47 | }) 48 | } 49 | } 50 | ], 51 | exports: [AuthService] 52 | }) 53 | export class AuthModule {} 54 | -------------------------------------------------------------------------------- /api-gateway/src/auth/auth.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Inject, OnModuleInit, UseGuards } from '@nestjs/common' 2 | import { ClientGrpcProxy } from '@nestjs/microservices' 3 | import { Resolver, Args, Mutation, Context } from '@nestjs/graphql' 4 | 5 | import { isEmpty } from 'lodash' 6 | import { PinoLogger } from 'nestjs-pino' 7 | 8 | import { AuthService } from './auth.service' 9 | import { RefreshAuthGuard } from './refresh-auth.guard' 10 | import { CurrentUser } from './user.decorator' 11 | 12 | import { IUsersService } from '../users/users.interface' 13 | import { User, SignupUserInput, UserPayload, LoginUserInput } from '../graphql/typings' 14 | 15 | import { PasswordUtils } from '../utils/password.utils' 16 | 17 | @Resolver() 18 | export class AuthResolver implements OnModuleInit { 19 | constructor( 20 | @Inject('UsersServiceClient') 21 | private readonly usersServiceClient: ClientGrpcProxy, 22 | 23 | private readonly authService: AuthService, 24 | 25 | private readonly passwordUtils: PasswordUtils, 26 | 27 | private readonly logger: PinoLogger 28 | ) { 29 | logger.setContext(AuthResolver.name) 30 | } 31 | 32 | private usersService: IUsersService 33 | 34 | onModuleInit(): void { 35 | this.usersService = this.usersServiceClient.getService('UsersService') 36 | } 37 | 38 | @Mutation() 39 | async signup(@Args('data') data: SignupUserInput): Promise { 40 | const { count } = await this.usersService 41 | .count({ 42 | where: JSON.stringify({ email: data.email }) 43 | }) 44 | .toPromise() 45 | 46 | if (count >= 1) throw new Error('Email taken') 47 | 48 | const user: User = await this.usersService 49 | .create({ 50 | ...data, 51 | password: await this.passwordUtils.hash(data.password) 52 | }) 53 | .toPromise() 54 | 55 | return { user } 56 | } 57 | 58 | @Mutation() 59 | async login(@Context() context: any, @Args('data') data: LoginUserInput): Promise { 60 | const { res } = context 61 | 62 | const user: any = await this.usersService 63 | .findOne({ 64 | where: JSON.stringify({ email: data.email }) 65 | }) 66 | .toPromise() 67 | 68 | if (isEmpty(user)) throw new Error('Unable to login') 69 | 70 | const isSame: boolean = await this.passwordUtils.compare(data.password, user.password) 71 | 72 | if (!isSame) throw new Error('Unable to login') 73 | 74 | res.cookie('access-token', await this.authService.generateAccessToken(user), { 75 | httpOnly: true, 76 | maxAge: 1.8e6 77 | }) 78 | res.cookie('refresh-token', await this.authService.generateRefreshToken(user), { 79 | httpOnly: true, 80 | maxAge: 1.728e8 81 | }) 82 | 83 | return { user } 84 | } 85 | 86 | @Mutation() 87 | @UseGuards(RefreshAuthGuard) 88 | async refreshToken(@Context() context: any, @CurrentUser() user: User): Promise { 89 | const { res } = context 90 | 91 | res.cookie('access-token', await this.authService.generateAccessToken(user), { 92 | httpOnly: true, 93 | maxAge: 1.8e6 94 | }) 95 | res.cookie('refresh-token', await this.authService.generateRefreshToken(user), { 96 | httpOnly: true, 97 | maxAge: 1.728e8 98 | }) 99 | 100 | return { user } 101 | } 102 | 103 | @Mutation() 104 | async logout(@Context() context: any): Promise { 105 | const { res } = context 106 | 107 | res.cookie('access-token', '', { 108 | httpOnly: true, 109 | maxAge: 0 110 | }) 111 | res.cookie('refresh-token', '', { 112 | httpOnly: true, 113 | maxAge: 0 114 | }) 115 | 116 | return true 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /api-gateway/src/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Inject } from '@nestjs/common' 2 | import { JwtService } from '@nestjs/jwt' 3 | import { PinoLogger } from 'nestjs-pino' 4 | 5 | import { User } from '../graphql/typings' 6 | 7 | @Injectable() 8 | export class AuthService { 9 | constructor( 10 | @Inject('JwtAccessTokenService') 11 | private readonly accessTokenService: JwtService, 12 | 13 | @Inject('JwtRefreshTokenService') 14 | private readonly refreshTokenService: JwtService, 15 | 16 | private readonly logger: PinoLogger 17 | ) { 18 | logger.setContext(AuthService.name) 19 | } 20 | 21 | async generateAccessToken(user: User): Promise { 22 | return this.accessTokenService.sign( 23 | { 24 | user: user.id 25 | }, 26 | { 27 | subject: user.id 28 | } 29 | ) 30 | } 31 | 32 | async generateRefreshToken(user: User): Promise { 33 | return this.refreshTokenService.sign( 34 | { 35 | user: user.email 36 | }, 37 | { 38 | subject: user.id 39 | } 40 | ) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /api-gateway/src/auth/gql-auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, ExecutionContext } from '@nestjs/common' 2 | import { AuthGuard } from '@nestjs/passport' 3 | import { GqlExecutionContext } from '@nestjs/graphql' 4 | 5 | @Injectable() 6 | export class GqlAuthGuard extends AuthGuard('jwt') { 7 | getRequest(context: ExecutionContext): any { 8 | const ctx = GqlExecutionContext.create(context) 9 | return ctx.getContext().req 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /api-gateway/src/auth/jwt-refresh.strategy.ts: -------------------------------------------------------------------------------- 1 | import { PassportStrategy } from '@nestjs/passport' 2 | import { ConfigService } from '@nestjs/config' 3 | import { Injectable, OnModuleInit, Inject } from '@nestjs/common' 4 | import { ClientGrpcProxy } from '@nestjs/microservices' 5 | 6 | import { PinoLogger } from 'nestjs-pino' 7 | import { get } from 'lodash' 8 | import { Strategy, ExtractJwt } from 'passport-jwt' 9 | 10 | import { IUsersService } from '../users/users.interface' 11 | import { User } from '../graphql/typings' 12 | 13 | @Injectable() 14 | export class JwtRefreshStrategy extends PassportStrategy(Strategy, 'jwt-refresh') implements OnModuleInit { 15 | constructor( 16 | @Inject('UsersServiceClient') 17 | private readonly usersServiceClient: ClientGrpcProxy, 18 | 19 | private readonly configService: ConfigService, 20 | 21 | private readonly logger: PinoLogger 22 | ) { 23 | super({ 24 | secretOrKey: configService.get('JWT_REFRESHTOKEN_SECRET'), 25 | issuer: configService.get('JWT_ISSUER'), 26 | audience: configService.get('JWT_AUDIENCE'), 27 | jwtFromRequest: ExtractJwt.fromExtractors([(req) => get(req, 'cookies.refresh-token')]) 28 | }) 29 | 30 | logger.setContext(JwtRefreshStrategy.name) 31 | } 32 | 33 | private usersService: IUsersService 34 | 35 | onModuleInit(): void { 36 | this.usersService = this.usersServiceClient.getService('UsersService') 37 | } 38 | 39 | async validate(payload: any): Promise { 40 | return this.usersService 41 | .findById({ 42 | id: payload.sub 43 | }) 44 | .toPromise() 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /api-gateway/src/auth/jwt.strategy.ts: -------------------------------------------------------------------------------- 1 | import { PassportStrategy } from '@nestjs/passport' 2 | import { ConfigService } from '@nestjs/config' 3 | import { Injectable, OnModuleInit, Inject } from '@nestjs/common' 4 | import { ClientGrpcProxy } from '@nestjs/microservices' 5 | 6 | import { PinoLogger } from 'nestjs-pino' 7 | import { get } from 'lodash' 8 | import { Strategy, ExtractJwt } from 'passport-jwt' 9 | 10 | import { IUsersService } from '../users/users.interface' 11 | import { User } from '../graphql/typings' 12 | 13 | @Injectable() 14 | export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') implements OnModuleInit { 15 | constructor( 16 | @Inject('UsersServiceClient') 17 | private readonly usersServiceClient: ClientGrpcProxy, 18 | 19 | private readonly configService: ConfigService, 20 | 21 | private readonly logger: PinoLogger 22 | ) { 23 | super({ 24 | secretOrKey: configService.get('JWT_ACCESSTOKEN_SECRET'), 25 | issuer: configService.get('JWT_ISSUER'), 26 | audience: configService.get('JWT_AUDIENCE'), 27 | jwtFromRequest: ExtractJwt.fromExtractors([(req) => get(req, 'cookies.access-token')]) 28 | }) 29 | 30 | logger.setContext(JwtStrategy.name) 31 | } 32 | 33 | private usersService: IUsersService 34 | 35 | onModuleInit(): void { 36 | this.usersService = this.usersServiceClient.getService('UsersService') 37 | } 38 | 39 | async validate(payload: any): Promise { 40 | return this.usersService 41 | .findById({ 42 | id: payload.sub 43 | }) 44 | .toPromise() 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /api-gateway/src/auth/refresh-auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, ExecutionContext } from '@nestjs/common' 2 | import { AuthGuard } from '@nestjs/passport' 3 | import { GqlExecutionContext } from '@nestjs/graphql' 4 | 5 | @Injectable() 6 | export class RefreshAuthGuard extends AuthGuard('jwt-refresh') { 7 | getRequest(context: ExecutionContext): any { 8 | const ctx = GqlExecutionContext.create(context) 9 | return ctx.getContext().req 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /api-gateway/src/auth/user.decorator.ts: -------------------------------------------------------------------------------- 1 | import { createParamDecorator, ExecutionContext } from '@nestjs/common' 2 | import { GqlExecutionContext } from '@nestjs/graphql' 3 | 4 | export const CurrentUser = createParamDecorator((data: unknown, context: ExecutionContext) => { 5 | const ctx = GqlExecutionContext.create(context) 6 | return ctx.getContext().req.user 7 | }) 8 | -------------------------------------------------------------------------------- /api-gateway/src/comments/comment.dto.ts: -------------------------------------------------------------------------------- 1 | export class CommentDto { 2 | readonly id?: string 3 | readonly text?: string 4 | readonly author?: string 5 | readonly post?: string 6 | readonly createdAt?: string 7 | readonly updatedAt?: string 8 | readonly version?: number 9 | } 10 | -------------------------------------------------------------------------------- /api-gateway/src/comments/comments-mutation.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Inject, OnModuleInit, UseGuards } from '@nestjs/common' 2 | import { ClientGrpcProxy } from '@nestjs/microservices' 3 | import { Resolver, Args, Mutation } from '@nestjs/graphql' 4 | 5 | import { Metadata } from 'grpc' 6 | import { PinoLogger } from 'nestjs-pino' 7 | import { PubSub } from 'graphql-subscriptions' 8 | 9 | import { CommentDto } from './comment.dto' 10 | import { ICommentsService } from './comments.interface' 11 | import { GqlAuthGuard } from '../auth/gql-auth.guard' 12 | import { CurrentUser } from '../auth/user.decorator' 13 | import { Comment, User, CommentPayload, UpdateCommentInput, DeleteCommentPayload } from '../graphql/typings' 14 | 15 | @Resolver() 16 | export class CommentsMutationResolver implements OnModuleInit { 17 | constructor( 18 | @Inject('CommentsServiceClient') 19 | private readonly commentsServiceClient: ClientGrpcProxy, 20 | 21 | @Inject('PubSubService') 22 | private readonly pubSubService: PubSub, 23 | 24 | private readonly logger: PinoLogger 25 | ) { 26 | logger.setContext(CommentsMutationResolver.name) 27 | } 28 | 29 | private commentsService: ICommentsService 30 | 31 | onModuleInit(): void { 32 | this.commentsService = this.commentsServiceClient.getService('CommentsService') 33 | } 34 | 35 | @Mutation() 36 | @UseGuards(GqlAuthGuard) 37 | async createComment(@CurrentUser() user: User, @Args('data') data: CommentDto): Promise { 38 | const comment: Comment = await this.commentsService 39 | .create({ 40 | ...data, 41 | author: user.id 42 | }) 43 | .toPromise() 44 | 45 | this.pubSubService.publish('commentAdded', comment) 46 | 47 | return { comment } 48 | } 49 | 50 | @Mutation() 51 | @UseGuards(GqlAuthGuard) 52 | async updateComment(@CurrentUser() user: User, @Args('id') id: string, @Args('data') data: UpdateCommentInput): Promise { 53 | const metadata: Metadata = new Metadata() 54 | 55 | metadata.add('user', user.id) 56 | 57 | const comment: Comment = await this.commentsService 58 | .update( 59 | { 60 | id, 61 | data: { 62 | ...data 63 | } 64 | }, 65 | metadata 66 | ) 67 | .toPromise() 68 | 69 | return { comment } 70 | } 71 | 72 | @Mutation() 73 | @UseGuards(GqlAuthGuard) 74 | async deleteComment(@CurrentUser() user: User, @Args('id') id: string): Promise { 75 | return this.commentsService 76 | .destroy({ 77 | where: JSON.stringify({ id, author: user.id }) 78 | }) 79 | .toPromise() 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /api-gateway/src/comments/comments-query.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Inject, OnModuleInit } from '@nestjs/common' 2 | import { ClientGrpcProxy } from '@nestjs/microservices' 3 | import { Query, Resolver, Args } from '@nestjs/graphql' 4 | 5 | import { isEmpty, merge } from 'lodash' 6 | import { PinoLogger } from 'nestjs-pino' 7 | 8 | import { ICommentsService } from './comments.interface' 9 | import { CommentsConnection } from '../graphql/typings' 10 | 11 | import { QueryUtils } from '../utils/query.utils' 12 | 13 | @Resolver() 14 | export class CommentsQueryResolver implements OnModuleInit { 15 | constructor( 16 | @Inject('CommentsServiceClient') 17 | private readonly commentsServiceClient: ClientGrpcProxy, 18 | 19 | private readonly queryUtils: QueryUtils, 20 | 21 | private readonly logger: PinoLogger 22 | ) { 23 | logger.setContext(CommentsQueryResolver.name) 24 | } 25 | 26 | private commentsService: ICommentsService 27 | 28 | onModuleInit(): void { 29 | this.commentsService = this.commentsServiceClient.getService('CommentsService') 30 | } 31 | 32 | @Query('comments') 33 | async getComments( 34 | @Args('q') q: string, 35 | @Args('first') first: number, 36 | @Args('last') last: number, 37 | @Args('before') before: string, 38 | @Args('after') after: string, 39 | @Args('filterBy') filterBy: any, 40 | @Args('orderBy') orderBy: string 41 | ): Promise { 42 | const query = { where: {} } 43 | 44 | if (!isEmpty(q)) merge(query, { where: { text: { _iLike: q } } }) 45 | 46 | merge(query, await this.queryUtils.buildQuery(filterBy, orderBy, first, last, before, after)) 47 | 48 | return this.commentsService 49 | .find({ 50 | ...query, 51 | where: JSON.stringify(query.where) 52 | }) 53 | .toPromise() 54 | } 55 | 56 | @Query('commentCount') 57 | async getCommentCount(@Args('q') q: string, @Args('filterBy') filterBy: any): Promise { 58 | const query = { where: {} } 59 | 60 | if (!isEmpty(q)) merge(query, { where: { title: { _iLike: q } } }) 61 | 62 | merge(query, await this.queryUtils.getFilters(filterBy)) 63 | 64 | const { count } = await this.commentsService 65 | .count({ 66 | ...query, 67 | where: JSON.stringify(query.where) 68 | }) 69 | .toPromise() 70 | 71 | return count 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /api-gateway/src/comments/comments-subscription.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Inject } from '@nestjs/common' 2 | import { Resolver, Subscription } from '@nestjs/graphql' 3 | 4 | import { PinoLogger } from 'nestjs-pino' 5 | import { PubSub } from 'graphql-subscriptions' 6 | 7 | import { Comment } from '../graphql/typings' 8 | 9 | @Resolver() 10 | export class CommentsSubscriptionResolver { 11 | constructor( 12 | @Inject('PubSubService') 13 | private readonly pubSubService: PubSub, 14 | 15 | private readonly logger: PinoLogger 16 | ) { 17 | logger.setContext(CommentsSubscriptionResolver.name) 18 | } 19 | 20 | @Subscription('commentAdded', { 21 | resolve: (value: Comment) => value, 22 | filter: (payload: Comment, variables: Record) => payload.post === variables.post 23 | }) 24 | commentAdded(): AsyncIterator { 25 | return this.pubSubService.asyncIterator('commentAdded') 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /api-gateway/src/comments/comments-type.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Inject, OnModuleInit } from '@nestjs/common' 2 | import { ClientGrpcProxy } from '@nestjs/microservices' 3 | import { Resolver, Parent, ResolveField } from '@nestjs/graphql' 4 | 5 | import { PinoLogger } from 'nestjs-pino' 6 | 7 | import { CommentDto } from './comment.dto' 8 | import { IPostsService } from '../posts/posts.interface' 9 | import { IUsersService } from '../users/users.interface' 10 | import { Post, User } from '../graphql/typings' 11 | 12 | @Resolver('Comment') 13 | export class CommentsTypeResolver implements OnModuleInit { 14 | constructor( 15 | @Inject('PostsServiceClient') 16 | private readonly postsServiceClient: ClientGrpcProxy, 17 | 18 | @Inject('UsersServiceClient') 19 | private readonly usersServiceClient: ClientGrpcProxy, 20 | 21 | private readonly logger: PinoLogger 22 | ) { 23 | logger.setContext(CommentsTypeResolver.name) 24 | } 25 | 26 | private postsService: IPostsService 27 | 28 | private usersService: IUsersService 29 | 30 | onModuleInit(): void { 31 | this.postsService = this.postsServiceClient.getService('PostsService') 32 | this.usersService = this.usersServiceClient.getService('UsersService') 33 | } 34 | 35 | @ResolveField('author') 36 | async getAuthor(@Parent() comment: CommentDto): Promise { 37 | return this.usersService 38 | .findById({ 39 | id: comment.author 40 | }) 41 | .toPromise() 42 | } 43 | 44 | @ResolveField('post') 45 | async getPost(@Parent() comment: CommentDto): Promise { 46 | return this.postsService 47 | .findById({ 48 | id: comment.post 49 | }) 50 | .toPromise() 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /api-gateway/src/comments/comments.interface.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs' 2 | import { Metadata } from 'grpc' 3 | 4 | import { IId, IQuery, ICount } from '../commons/commons.interface' 5 | import { CommentsConnection, Comment } from '../graphql/typings' 6 | import { CommentDto } from './comment.dto' 7 | 8 | interface IUpdateCommentInput { 9 | id: string 10 | data: CommentDto 11 | } 12 | 13 | export interface ICommentsService { 14 | find(query: IQuery, metadata?: Metadata): Observable 15 | findById(id: IId, metadata?: Metadata): Observable 16 | findOne(query: IQuery, metadata?: Metadata): Observable 17 | count(query: IQuery, metadata?: Metadata): Observable 18 | create(input: CommentDto, metadata?: Metadata): Observable 19 | update(input: IUpdateCommentInput, metadata?: Metadata): Observable 20 | destroy(query: IQuery, metadata?: Metadata): Observable 21 | } 22 | -------------------------------------------------------------------------------- /api-gateway/src/comments/comments.module.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path' 2 | import { Module, forwardRef } from '@nestjs/common' 3 | import { ConfigModule, ConfigService } from '@nestjs/config' 4 | import { LoggerModule } from 'nestjs-pino' 5 | import { ClientProxyFactory, Transport, ClientGrpcProxy } from '@nestjs/microservices' 6 | 7 | import { CommentsTypeResolver } from './comments-type.resolver' 8 | import { CommentsQueryResolver } from './comments-query.resolver' 9 | import { CommentsMutationResolver } from './comments-mutation.resolver' 10 | import { CommentsSubscriptionResolver } from './comments-subscription.resolver' 11 | 12 | import { UtilsModule } from '../utils/utils.module' 13 | import { PostsModule } from '../posts/posts.module' 14 | import { UsersModule } from '../users/users.module' 15 | import { CommonsModule } from '../commons/commons.module' 16 | 17 | @Module({ 18 | imports: [ConfigModule, LoggerModule, CommonsModule, UtilsModule, forwardRef(() => PostsModule), forwardRef(() => UsersModule)], 19 | providers: [ 20 | CommentsTypeResolver, 21 | CommentsQueryResolver, 22 | CommentsMutationResolver, 23 | CommentsSubscriptionResolver, 24 | { 25 | provide: 'CommentsServiceClient', 26 | useFactory: (configService: ConfigService): ClientGrpcProxy => { 27 | return ClientProxyFactory.create({ 28 | transport: Transport.GRPC, 29 | options: { 30 | url: configService.get('COMMENTS_SVC_URL'), 31 | package: 'comment', 32 | protoPath: join(__dirname, '../_proto/comment.proto'), 33 | loader: { 34 | keepCase: true, 35 | enums: String, 36 | oneofs: true, 37 | arrays: true 38 | } 39 | } 40 | }) 41 | }, 42 | inject: [ConfigService] 43 | } 44 | ], 45 | exports: ['CommentsServiceClient'] 46 | }) 47 | export class CommentsModule {} 48 | -------------------------------------------------------------------------------- /api-gateway/src/commons/commons.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IId { 2 | id: string 3 | } 4 | 5 | export interface IQuery { 6 | select?: string[] 7 | where?: string 8 | orderBy?: string[] 9 | limit?: number 10 | before?: string 11 | after?: string 12 | } 13 | 14 | export interface ICount { 15 | count: number 16 | } 17 | -------------------------------------------------------------------------------- /api-gateway/src/commons/commons.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { ConfigModule, ConfigService } from '@nestjs/config' 3 | 4 | import Redis from 'ioredis' 5 | import { RedisPubSub } from 'graphql-redis-subscriptions' 6 | 7 | @Module({ 8 | imports: [ConfigModule], 9 | providers: [ 10 | { 11 | provide: 'PubSubService', 12 | useFactory: async (configService: ConfigService): Promise => { 13 | const redisOptions: Redis.RedisOptions = { 14 | host: configService.get('REDIS_HOST'), 15 | port: configService.get('REDIS_PORT'), 16 | password: configService.get('REDIS_PASSWORD'), 17 | keyPrefix: configService.get('NODE_ENV') 18 | } 19 | 20 | return new RedisPubSub({ 21 | publisher: new Redis(redisOptions), 22 | subscriber: new Redis(redisOptions) 23 | }) 24 | }, 25 | inject: [ConfigService] 26 | } 27 | ], 28 | exports: ['PubSubService'] 29 | }) 30 | export class CommonsModule {} 31 | -------------------------------------------------------------------------------- /api-gateway/src/graphql/generate-typings.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path' 2 | import { GraphQLDefinitionsFactory } from '@nestjs/graphql' 3 | 4 | const definitionsFactory = new GraphQLDefinitionsFactory() 5 | 6 | definitionsFactory.generate({ 7 | typePaths: ['./**/*.schema.graphql'], 8 | path: join(__dirname, 'typings.ts'), 9 | watch: true 10 | }) 11 | -------------------------------------------------------------------------------- /api-gateway/src/graphql/schema/_mutation.schema.graphql: -------------------------------------------------------------------------------- 1 | type Mutation { 2 | # Auth Mutations 3 | signup(data: SignupUserInput!): UserPayload! 4 | login(data: LoginUserInput!): UserPayload! 5 | refreshToken: UserPayload! 6 | logout: Boolean! 7 | 8 | # Comments Mutations 9 | createComment(data: CreateCommentInput!): CommentPayload! 10 | updateComment(id: ID!, data: UpdateCommentInput!): CommentPayload! 11 | deleteComment(id: ID!): DeleteCommentPayload! 12 | 13 | # Posts Mutations 14 | createPost(data: CreatePostInput!): PostPayload! 15 | updatePost(id: ID!, data: UpdatePostInput!): PostPayload! 16 | deletePost(id: ID!): DeletePostPayload! 17 | 18 | # Users Mutations 19 | updateProfile(data: UpdateProfileInput!): UserPayload! 20 | updateEmail(data: UpdateEmailInput): UserPayload! 21 | updatePassword(data: UpdatePasswordInput): UserPayload! 22 | deleteAccount: DeleteAccountPayload! 23 | } 24 | -------------------------------------------------------------------------------- /api-gateway/src/graphql/schema/_query.schema.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | # Comments Query 3 | comments(q: String, first: Int, last: Int, before: String, after: String, filterBy: JSONObject, orderBy: String): CommentsConnection 4 | commentCount(q: String, filterBy: JSONObject): Int! 5 | 6 | # Posts Query 7 | post(id: ID!): Post! 8 | posts(q: String, first: Int, last: Int, before: String, after: String, filterBy: JSONObject, orderBy: String): PostsConnection 9 | postCount(q: String, filterBy: JSONObject): Int! 10 | myPosts(q: String, first: Int, last: Int, before: String, after: String, filterBy: JSONObject, orderBy: String): PostsConnection 11 | 12 | # Users Query 13 | user(id: ID!): User! 14 | users(q: String, first: Int, last: Int, before: String, after: String, filterBy: JSONObject, orderBy: String): UsersConnection 15 | userCount(q: String, filterBy: JSONObject): Int! 16 | me: User! 17 | } 18 | -------------------------------------------------------------------------------- /api-gateway/src/graphql/schema/_scalar.schema.graphql: -------------------------------------------------------------------------------- 1 | scalar DateTime 2 | 3 | scalar EmailAddress 4 | 5 | scalar UnsignedInt 6 | 7 | scalar JSONObject 8 | -------------------------------------------------------------------------------- /api-gateway/src/graphql/schema/_subscription.schema.graphql: -------------------------------------------------------------------------------- 1 | type Subscription { 2 | # Comments Subscription 3 | commentAdded(post: ID!): Comment! 4 | 5 | # Posts Subscription 6 | postAdded: Post! 7 | } 8 | -------------------------------------------------------------------------------- /api-gateway/src/graphql/schema/comment.schema.graphql: -------------------------------------------------------------------------------- 1 | type Comment { 2 | id: ID! 3 | text: String! 4 | author: User! 5 | post: Post! 6 | createdAt: DateTime! 7 | updatedAt: DateTime! 8 | version: Int! 9 | } 10 | 11 | type CommentsConnection { 12 | edges: [CommentEdge!]! 13 | pageInfo: PageInfo! 14 | } 15 | 16 | type CommentEdge { 17 | node: Comment! 18 | cursor: String! 19 | } 20 | 21 | type CommentPayload { 22 | errors: [ErrorPayload] 23 | comment: Comment 24 | } 25 | 26 | type DeleteCommentPayload { 27 | errors: [ErrorPayload] 28 | count: Int 29 | } 30 | 31 | input CreateCommentInput { 32 | text: String! 33 | post: ID! 34 | } 35 | 36 | input UpdateCommentInput { 37 | text: String 38 | } 39 | -------------------------------------------------------------------------------- /api-gateway/src/graphql/schema/commons.schema.graphql: -------------------------------------------------------------------------------- 1 | type ErrorPayload { 2 | field: String 3 | message: [String] 4 | } 5 | 6 | type PageInfo { 7 | startCursor: String! 8 | endCursor: String! 9 | hasNextPage: Boolean! 10 | hasPreviousPage: Boolean! 11 | } 12 | -------------------------------------------------------------------------------- /api-gateway/src/graphql/schema/post.schema.graphql: -------------------------------------------------------------------------------- 1 | 2 | type Post { 3 | id: ID! 4 | title: String! 5 | body: String! 6 | published: Boolean! 7 | author: User! 8 | comments(q: String, first: Int, last: Int, before: String, after: String, filterBy: JSONObject, orderBy: String): CommentsConnection 9 | createdAt: DateTime! 10 | updatedAt: DateTime! 11 | version: Int! 12 | } 13 | 14 | type PostsConnection { 15 | edges: [PostEdge!]! 16 | pageInfo: PageInfo! 17 | } 18 | 19 | type PostEdge { 20 | node: Post! 21 | cursor: String! 22 | } 23 | 24 | type PostPayload { 25 | errors: [ErrorPayload] 26 | post: Post 27 | } 28 | 29 | type DeletePostPayload { 30 | errors: [ErrorPayload] 31 | count: Int 32 | } 33 | 34 | input CreatePostInput { 35 | title: String! 36 | body: String! 37 | published: Boolean! 38 | } 39 | 40 | input UpdatePostInput { 41 | title: String 42 | body: String 43 | published: Boolean 44 | } 45 | -------------------------------------------------------------------------------- /api-gateway/src/graphql/schema/user.schema.graphql: -------------------------------------------------------------------------------- 1 | 2 | type User { 3 | id: ID! 4 | name: String! 5 | email: EmailAddress! 6 | age: UnsignedInt 7 | posts(q: String, first: Int, last: Int, before: String, after: String, filterBy: JSONObject, orderBy: String): PostsConnection 8 | comments(q: String, first: Int, last: Int, before: String, after: String, filterBy: JSONObject, orderBy: String): CommentsConnection 9 | createdAt: DateTime! 10 | updatedAt: DateTime! 11 | version: Int! 12 | } 13 | 14 | type UsersConnection { 15 | edges: [UserEdge!]! 16 | pageInfo: PageInfo! 17 | } 18 | 19 | type UserEdge { 20 | node: User! 21 | cursor: String! 22 | } 23 | 24 | type UserPayload { 25 | errors: [ErrorPayload] 26 | user: User 27 | } 28 | 29 | type DeleteAccountPayload { 30 | errors: [ErrorPayload] 31 | count: Int 32 | } 33 | 34 | input SignupUserInput { 35 | name: String! 36 | email: EmailAddress! 37 | password: String! 38 | age: UnsignedInt 39 | } 40 | 41 | input LoginUserInput { 42 | email: EmailAddress! 43 | password: String! 44 | } 45 | 46 | input UpdateProfileInput { 47 | name: String 48 | age: UnsignedInt 49 | } 50 | 51 | input UpdateEmailInput { 52 | email: EmailAddress! 53 | currentPassword: String! 54 | } 55 | 56 | input UpdatePasswordInput { 57 | currentPassword: String! 58 | newPassword: String! 59 | confirmPassword: String! 60 | } 61 | -------------------------------------------------------------------------------- /api-gateway/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core' 2 | import { NestExpressApplication, ExpressAdapter } from '@nestjs/platform-express' 3 | import { Logger } from 'nestjs-pino' 4 | 5 | // @ts-ignore 6 | import cors from 'cors' 7 | import cookieParser from 'cookie-parser' 8 | 9 | import { AppModule } from './app.module' 10 | import { ConfigService } from '@nestjs/config' 11 | 12 | async function main() { 13 | const app: NestExpressApplication = await NestFactory.create(AppModule, new ExpressAdapter()) 14 | const configService: ConfigService = app.get(ConfigService) 15 | 16 | app.use( 17 | cors({ 18 | origin: '*', 19 | credentials: true 20 | }) 21 | ) 22 | app.use(cookieParser()) 23 | 24 | app.useLogger(app.get(Logger)) 25 | 26 | return app.listenAsync(configService.get('GRAPHQL_PORT')) 27 | } 28 | 29 | main() 30 | -------------------------------------------------------------------------------- /api-gateway/src/posts/post.dto.ts: -------------------------------------------------------------------------------- 1 | export class PostDto { 2 | readonly id?: string 3 | readonly title?: string 4 | readonly body?: string 5 | readonly published?: boolean 6 | readonly author?: string 7 | readonly createdAt?: string 8 | readonly updatedAt?: string 9 | readonly version?: number 10 | } 11 | -------------------------------------------------------------------------------- /api-gateway/src/posts/posts-mutation.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Inject, OnModuleInit, UseGuards } from '@nestjs/common' 2 | import { ClientGrpcProxy } from '@nestjs/microservices' 3 | import { Resolver, Args, Mutation } from '@nestjs/graphql' 4 | 5 | import { Metadata } from 'grpc' 6 | import { PinoLogger } from 'nestjs-pino' 7 | import { PubSub } from 'graphql-subscriptions' 8 | 9 | import { IPostsService } from './posts.interface' 10 | import { GqlAuthGuard } from '../auth/gql-auth.guard' 11 | import { CurrentUser } from '../auth/user.decorator' 12 | import { User, PostPayload, UpdatePostInput, DeletePostPayload } from '../graphql/typings' 13 | 14 | import { PostDto } from './post.dto' 15 | 16 | @Resolver() 17 | export class PostsMutationResolver implements OnModuleInit { 18 | constructor( 19 | @Inject('PostsServiceClient') 20 | private readonly postsServiceClient: ClientGrpcProxy, 21 | 22 | @Inject('PubSubService') 23 | private readonly pubSubService: PubSub, 24 | 25 | private readonly logger: PinoLogger 26 | ) { 27 | logger.setContext(PostsMutationResolver.name) 28 | } 29 | 30 | private postsService: IPostsService 31 | 32 | onModuleInit(): void { 33 | this.postsService = this.postsServiceClient.getService('PostsService') 34 | } 35 | 36 | @Mutation() 37 | @UseGuards(GqlAuthGuard) 38 | async createPost(@CurrentUser() user: User, @Args('data') data: PostDto): Promise { 39 | const post = await this.postsService 40 | .create({ 41 | ...data, 42 | author: user.id 43 | }) 44 | .toPromise() 45 | 46 | this.pubSubService.publish('postAdded', post) 47 | 48 | return { post } 49 | } 50 | 51 | @Mutation() 52 | @UseGuards(GqlAuthGuard) 53 | async updatePost(@CurrentUser() user: User, @Args('id') id: string, @Args('data') data: UpdatePostInput): Promise { 54 | const metadata: Metadata = new Metadata() 55 | 56 | metadata.add('user', user.id) 57 | 58 | const post = await this.postsService 59 | .update( 60 | { 61 | id, 62 | data: { 63 | ...data 64 | } 65 | }, 66 | metadata 67 | ) 68 | .toPromise() 69 | 70 | return { post } 71 | } 72 | 73 | @Mutation() 74 | @UseGuards(GqlAuthGuard) 75 | async deletePost(@CurrentUser() user: User, @Args('id') id: string): Promise { 76 | return this.postsService 77 | .destroy({ 78 | where: JSON.stringify({ id, author: user.id }) 79 | }) 80 | .toPromise() 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /api-gateway/src/posts/posts-query.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Inject, OnModuleInit, UseGuards } from '@nestjs/common' 2 | import { ClientGrpcProxy } from '@nestjs/microservices' 3 | import { Query, Resolver, Args, Context } from '@nestjs/graphql' 4 | 5 | import { isEmpty, merge } from 'lodash' 6 | import { PinoLogger } from 'nestjs-pino' 7 | 8 | import { IPostsService } from './posts.interface' 9 | import { GqlAuthGuard } from '../auth/gql-auth.guard' 10 | import { Post, PostsConnection } from '../graphql/typings' 11 | 12 | import { QueryUtils } from '../utils/query.utils' 13 | 14 | @Resolver() 15 | export class PostsQueryResolver implements OnModuleInit { 16 | constructor( 17 | @Inject('PostsServiceClient') 18 | private readonly postsServiceClient: ClientGrpcProxy, 19 | 20 | private readonly queryUtils: QueryUtils, 21 | 22 | private readonly logger: PinoLogger 23 | ) { 24 | logger.setContext(PostsQueryResolver.name) 25 | } 26 | 27 | private postsService: IPostsService 28 | 29 | onModuleInit(): void { 30 | this.postsService = this.postsServiceClient.getService('PostsService') 31 | } 32 | 33 | @Query('posts') 34 | async getPosts( 35 | @Args('q') q: string, 36 | @Args('first') first: number, 37 | @Args('last') last: number, 38 | @Args('before') before: string, 39 | @Args('after') after: string, 40 | @Args('filterBy') filterBy: any, 41 | @Args('orderBy') orderBy: string 42 | ): Promise { 43 | const query = { where: {} } 44 | 45 | if (!isEmpty(q)) merge(query, { where: { title: { _iLike: q } } }) 46 | 47 | merge(query, await this.queryUtils.buildQuery(filterBy, orderBy, first, last, before, after)) 48 | 49 | return this.postsService 50 | .find({ 51 | ...query, 52 | where: JSON.stringify(query.where) 53 | }) 54 | .toPromise() 55 | } 56 | 57 | @Query('post') 58 | async getPost(@Args('id') id: string): Promise { 59 | return this.postsService.findById({ id }).toPromise() 60 | } 61 | 62 | @Query('postCount') 63 | async getPostCount(@Args('q') q: string, @Args('filterBy') filterBy: any): Promise { 64 | const query = { where: {} } 65 | 66 | if (!isEmpty(q)) merge(query, { where: { title: { _iLike: q } } }) 67 | 68 | merge(query, await this.queryUtils.getFilters(filterBy)) 69 | 70 | const { count } = await this.postsService 71 | .count({ 72 | ...query, 73 | where: JSON.stringify(query.where) 74 | }) 75 | .toPromise() 76 | 77 | return count 78 | } 79 | 80 | @Query('myPosts') 81 | @UseGuards(GqlAuthGuard) 82 | async getMyPosts( 83 | @Context() context, 84 | @Args('q') q: string, 85 | @Args('first') first: number, 86 | @Args('last') last: number, 87 | @Args('before') before: string, 88 | @Args('after') after: string, 89 | @Args('filterBy') filterBy: any, 90 | @Args('orderBy') orderBy: string 91 | ): Promise { 92 | this.logger.info('========USER %o', context.req.user) 93 | const query = { where: { author: context.req.user.id } } 94 | 95 | if (!isEmpty(q)) merge(query, { where: { title: { _iLike: q } } }) 96 | 97 | merge(query, await this.queryUtils.buildQuery(filterBy, orderBy, first, last, before, after)) 98 | 99 | return this.postsService 100 | .find({ 101 | ...query, 102 | where: JSON.stringify(query.where) 103 | }) 104 | .toPromise() 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /api-gateway/src/posts/posts-subscription.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Inject } from '@nestjs/common' 2 | import { Resolver, Subscription } from '@nestjs/graphql' 3 | 4 | import { PinoLogger } from 'nestjs-pino' 5 | import { PubSub } from 'graphql-subscriptions' 6 | 7 | @Resolver() 8 | export class PostsSubscriptionResolver { 9 | constructor( 10 | @Inject('PubSubService') 11 | private readonly pubSubService: PubSub, 12 | 13 | private readonly logger: PinoLogger 14 | ) { 15 | logger.setContext(PostsSubscriptionResolver.name) 16 | } 17 | 18 | @Subscription('postAdded', { 19 | resolve: (value: Comment) => value 20 | }) 21 | postAdded(): AsyncIterator { 22 | return this.pubSubService.asyncIterator('postAdded') 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /api-gateway/src/posts/posts-type.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Inject, OnModuleInit } from '@nestjs/common' 2 | import { ClientGrpcProxy } from '@nestjs/microservices' 3 | import { Resolver, Args, Parent, ResolveField } from '@nestjs/graphql' 4 | 5 | import { isEmpty, merge } from 'lodash' 6 | import { PinoLogger } from 'nestjs-pino' 7 | 8 | import { ICommentsService } from '../comments/comments.interface' 9 | import { IUsersService } from '../users/users.interface' 10 | import { CommentsConnection, Post, User } from '../graphql/typings' 11 | 12 | import { QueryUtils } from '../utils/query.utils' 13 | import { PostDto } from './post.dto' 14 | 15 | @Resolver('Post') 16 | export class PostsTypeResolver implements OnModuleInit { 17 | constructor( 18 | @Inject('CommentsServiceClient') 19 | private readonly commentsServiceClient: ClientGrpcProxy, 20 | 21 | @Inject('UsersServiceClient') 22 | private readonly usersServiceClient: ClientGrpcProxy, 23 | 24 | private readonly queryUtils: QueryUtils, 25 | 26 | private readonly logger: PinoLogger 27 | ) { 28 | logger.setContext(PostsTypeResolver.name) 29 | } 30 | 31 | private commentsService: ICommentsService 32 | 33 | private usersService: IUsersService 34 | 35 | onModuleInit(): void { 36 | this.commentsService = this.commentsServiceClient.getService('CommentsService') 37 | this.usersService = this.usersServiceClient.getService('UsersService') 38 | } 39 | 40 | @ResolveField('author') 41 | async getAuthor(@Parent() post: PostDto): Promise { 42 | return this.usersService 43 | .findById({ 44 | id: post.author 45 | }) 46 | .toPromise() 47 | } 48 | 49 | @ResolveField('comments') 50 | async getComments( 51 | @Parent() post: Post, 52 | @Args('q') q: string, 53 | @Args('first') first: number, 54 | @Args('last') last: number, 55 | @Args('before') before: string, 56 | @Args('after') after: string, 57 | @Args('filterBy') filterBy: any, 58 | @Args('orderBy') orderBy: string 59 | ): Promise { 60 | const query = { where: { post: post.id } } 61 | 62 | if (!isEmpty(q)) merge(query, { where: { text: { _iLike: q } } }) 63 | 64 | merge(query, await this.queryUtils.buildQuery(filterBy, orderBy, first, last, before, after)) 65 | 66 | return this.commentsService 67 | .find({ 68 | ...query, 69 | where: JSON.stringify(query.where) 70 | }) 71 | .toPromise() 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /api-gateway/src/posts/posts.interface.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs' 2 | import { Metadata } from 'grpc' 3 | 4 | import { IId, IQuery, ICount } from '../commons/commons.interface' 5 | import { Post, PostsConnection } from '../graphql/typings' 6 | import { PostDto } from './post.dto' 7 | 8 | interface UpdatePostInput { 9 | id: string 10 | data: PostDto 11 | } 12 | 13 | export interface IPostsService { 14 | find(query: IQuery, metadata?: Metadata): Observable 15 | findById(id: IId, metadata?: Metadata): Observable 16 | findOne(query: IQuery, metadata?: Metadata): Observable 17 | count(query: IQuery, metadata?: Metadata): Observable 18 | create(input: PostDto, metadata?: Metadata): Observable 19 | update(input: UpdatePostInput, metadata?: Metadata): Observable 20 | destroy(query: IQuery, metadata?: Metadata): Observable 21 | } 22 | -------------------------------------------------------------------------------- /api-gateway/src/posts/posts.module.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path' 2 | import { Module, forwardRef } from '@nestjs/common' 3 | import { ConfigModule, ConfigService } from '@nestjs/config' 4 | import { LoggerModule } from 'nestjs-pino' 5 | import { ClientProxyFactory, Transport, ClientGrpcProxy } from '@nestjs/microservices' 6 | 7 | import { PostsTypeResolver } from './posts-type.resolver' 8 | import { PostsQueryResolver } from './posts-query.resolver' 9 | import { PostsMutationResolver } from './posts-mutation.resolver' 10 | import { PostsSubscriptionResolver } from './posts-subscription.resolver' 11 | 12 | import { UtilsModule } from '../utils/utils.module' 13 | import { CommentsModule } from '../comments/comments.module' 14 | import { UsersModule } from '../users/users.module' 15 | import { CommonsModule } from '../commons/commons.module' 16 | 17 | @Module({ 18 | imports: [ConfigModule, LoggerModule, CommonsModule, UtilsModule, forwardRef(() => CommentsModule), forwardRef(() => UsersModule)], 19 | providers: [ 20 | PostsTypeResolver, 21 | PostsQueryResolver, 22 | PostsMutationResolver, 23 | PostsSubscriptionResolver, 24 | { 25 | provide: 'PostsServiceClient', 26 | useFactory: (configService: ConfigService): ClientGrpcProxy => { 27 | return ClientProxyFactory.create({ 28 | transport: Transport.GRPC, 29 | options: { 30 | url: configService.get('POSTS_SVC_URL'), 31 | package: 'post', 32 | protoPath: join(__dirname, '../_proto/post.proto'), 33 | loader: { 34 | keepCase: true, 35 | enums: String, 36 | oneofs: true, 37 | arrays: true 38 | } 39 | } 40 | }) 41 | }, 42 | inject: [ConfigService] 43 | } 44 | ], 45 | exports: ['PostsServiceClient'] 46 | }) 47 | export class PostsModule {} 48 | -------------------------------------------------------------------------------- /api-gateway/src/users/user.dto.ts: -------------------------------------------------------------------------------- 1 | export class UserDto { 2 | readonly id?: string 3 | readonly name?: string 4 | readonly email?: string 5 | readonly password?: string 6 | readonly age?: number 7 | readonly createdAt?: string 8 | readonly updatedAt?: string 9 | readonly version?: number 10 | } 11 | -------------------------------------------------------------------------------- /api-gateway/src/users/users-mutation.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Inject, OnModuleInit, UseGuards } from '@nestjs/common' 2 | import { ClientGrpcProxy } from '@nestjs/microservices' 3 | import { Resolver, Args, Mutation } from '@nestjs/graphql' 4 | 5 | import { PinoLogger } from 'nestjs-pino' 6 | 7 | import { IUsersService } from './users.interface' 8 | import { User, UserPayload, UpdateProfileInput, UpdateEmailInput, UpdatePasswordInput, DeleteAccountPayload } from '../graphql/typings' 9 | 10 | import { PasswordUtils } from '../utils/password.utils' 11 | import { GqlAuthGuard } from '../auth/gql-auth.guard' 12 | import { CurrentUser } from '../auth/user.decorator' 13 | 14 | @Resolver() 15 | export class UsersMutationResolver implements OnModuleInit { 16 | constructor( 17 | @Inject('UsersServiceClient') 18 | private readonly usersServiceClient: ClientGrpcProxy, 19 | 20 | private readonly passwordUtils: PasswordUtils, 21 | 22 | private readonly logger: PinoLogger 23 | ) { 24 | logger.setContext(UsersMutationResolver.name) 25 | } 26 | 27 | private usersService: IUsersService 28 | 29 | onModuleInit(): void { 30 | this.usersService = this.usersServiceClient.getService('UsersService') 31 | } 32 | 33 | @Mutation() 34 | @UseGuards(GqlAuthGuard) 35 | async updateProfile(@CurrentUser() user: User, @Args('data') data: UpdateProfileInput): Promise { 36 | const updatedUser: User = await this.usersService 37 | .update({ 38 | id: user.id, 39 | data: { 40 | ...data 41 | } 42 | }) 43 | .toPromise() 44 | 45 | return { user: updatedUser } 46 | } 47 | 48 | @Mutation() 49 | @UseGuards(GqlAuthGuard) 50 | async updateEmail(@CurrentUser() user: any, @Args('data') data: UpdateEmailInput): Promise { 51 | const { count } = await this.usersService 52 | .count({ 53 | where: JSON.stringify({ email: data.email }) 54 | }) 55 | .toPromise() 56 | 57 | if (count >= 1) throw new Error('Email taken') 58 | 59 | const isSame: boolean = await this.passwordUtils.compare(data.currentPassword, user.password) 60 | 61 | if (!isSame) throw new Error('Error updating email. Kindly check the email or password provided') 62 | 63 | const updatedUser: User = await this.usersService 64 | .update({ 65 | id: user.id, 66 | data: { 67 | ...data 68 | } 69 | }) 70 | .toPromise() 71 | 72 | return { user: updatedUser } 73 | } 74 | 75 | @Mutation() 76 | @UseGuards(GqlAuthGuard) 77 | async updatePassword(@CurrentUser() user: any, @Args('data') data: UpdatePasswordInput): Promise { 78 | const isSame: boolean = await this.passwordUtils.compare(data.currentPassword, user.password) 79 | const isConfirmed: boolean = data.newPassword === data.confirmPassword 80 | 81 | if (!isSame || !isConfirmed) { 82 | throw new Error('Error updating password. Kindly check your passwords.') 83 | } 84 | 85 | const password: string = await this.passwordUtils.hash(data.newPassword) 86 | 87 | const updatedUser: any = await this.usersService.update({ 88 | id: user.id, 89 | data: { 90 | password 91 | } 92 | }) 93 | 94 | return { user: updatedUser } 95 | } 96 | 97 | @Mutation() 98 | @UseGuards(GqlAuthGuard) 99 | async deleteAccount(@CurrentUser() user: User): Promise { 100 | return this.usersService 101 | .destroy({ 102 | where: JSON.stringify({ 103 | id: user.id 104 | }) 105 | }) 106 | .toPromise() 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /api-gateway/src/users/users-query.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Inject, OnModuleInit, UseGuards } from '@nestjs/common' 2 | import { ClientGrpcProxy } from '@nestjs/microservices' 3 | import { Query, Resolver, Args } from '@nestjs/graphql' 4 | 5 | import { isEmpty, merge } from 'lodash' 6 | import { PinoLogger } from 'nestjs-pino' 7 | 8 | import { IUsersService } from './users.interface' 9 | import { User, UsersConnection } from '../graphql/typings' 10 | 11 | import { QueryUtils } from '../utils/query.utils' 12 | import { GqlAuthGuard } from '../auth/gql-auth.guard' 13 | import { CurrentUser } from '../auth/user.decorator' 14 | 15 | @Resolver('User') 16 | export class UsersQueryResolver implements OnModuleInit { 17 | constructor( 18 | @Inject('UsersServiceClient') 19 | private readonly usersServiceClient: ClientGrpcProxy, 20 | 21 | private readonly queryUtils: QueryUtils, 22 | 23 | private readonly logger: PinoLogger 24 | ) { 25 | logger.setContext(UsersQueryResolver.name) 26 | } 27 | 28 | private usersService: IUsersService 29 | 30 | onModuleInit(): void { 31 | this.usersService = this.usersServiceClient.getService('UsersService') 32 | } 33 | 34 | @Query('users') 35 | @UseGuards(GqlAuthGuard) 36 | async getUsers( 37 | @Args('q') q: string, 38 | @Args('first') first: number, 39 | @Args('last') last: number, 40 | @Args('before') before: string, 41 | @Args('after') after: string, 42 | @Args('filterBy') filterBy: any, 43 | @Args('orderBy') orderBy: string 44 | ): Promise { 45 | const query = { where: {} } 46 | 47 | if (!isEmpty(q)) merge(query, { where: { name: { _iLike: q } } }) 48 | 49 | merge(query, await this.queryUtils.buildQuery(filterBy, orderBy, first, last, before, after)) 50 | 51 | return this.usersService 52 | .find({ 53 | ...query, 54 | where: JSON.stringify(query.where) 55 | }) 56 | .toPromise() 57 | } 58 | 59 | @Query('user') 60 | @UseGuards(GqlAuthGuard) 61 | async getUser(@Args('id') id: string): Promise { 62 | return this.usersService.findById({ id }).toPromise() 63 | } 64 | 65 | @Query('userCount') 66 | @UseGuards(GqlAuthGuard) 67 | async getUserCount(@Args('q') q: string, @Args('filterBy') filterBy: any): Promise { 68 | const query = { where: {} } 69 | 70 | if (!isEmpty(q)) merge(query, { where: { name: { _iLike: q } } }) 71 | 72 | merge(query, await this.queryUtils.getFilters(filterBy)) 73 | 74 | const { count } = await this.usersService 75 | .count({ 76 | ...query, 77 | where: JSON.stringify(query.where) 78 | }) 79 | .toPromise() 80 | 81 | return count 82 | } 83 | 84 | @Query('me') 85 | @UseGuards(GqlAuthGuard) 86 | async getProfile(@CurrentUser() user: User): Promise { 87 | return this.usersService.findById({ id: user.id }).toPromise() 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /api-gateway/src/users/users-type.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Inject, OnModuleInit } from '@nestjs/common' 2 | import { ClientGrpcProxy } from '@nestjs/microservices' 3 | import { Resolver, Args, Parent, ResolveField } from '@nestjs/graphql' 4 | 5 | import { isEmpty, merge } from 'lodash' 6 | import { PinoLogger } from 'nestjs-pino' 7 | 8 | import { ICommentsService } from '../comments/comments.interface' 9 | import { IPostsService } from '../posts/posts.interface' 10 | import { CommentsConnection, User, PostsConnection } from '../graphql/typings' 11 | 12 | import { QueryUtils } from '../utils/query.utils' 13 | 14 | @Resolver('User') 15 | export class UsersTypeResolver implements OnModuleInit { 16 | constructor( 17 | @Inject('CommentsServiceClient') 18 | private readonly commentsServiceClient: ClientGrpcProxy, 19 | 20 | @Inject('PostsServiceClient') 21 | private readonly postsServiceClient: ClientGrpcProxy, 22 | 23 | private readonly queryUtils: QueryUtils, 24 | 25 | private readonly logger: PinoLogger 26 | ) { 27 | logger.setContext(UsersTypeResolver.name) 28 | } 29 | 30 | private commentsService: ICommentsService 31 | 32 | private postsService: IPostsService 33 | 34 | onModuleInit(): void { 35 | this.commentsService = this.commentsServiceClient.getService('CommentsService') 36 | this.postsService = this.postsServiceClient.getService('PostsService') 37 | } 38 | 39 | @ResolveField('posts') 40 | async getPosts( 41 | @Parent() user: User, 42 | @Args('q') q: string, 43 | @Args('first') first: number, 44 | @Args('last') last: number, 45 | @Args('before') before: string, 46 | @Args('after') after: string, 47 | @Args('filterBy') filterBy: any, 48 | @Args('orderBy') orderBy: string 49 | ): Promise { 50 | const query = { where: { author: user.id } } 51 | 52 | if (!isEmpty(q)) merge(query, { where: { title: { _iLike: q } } }) 53 | 54 | merge(query, await this.queryUtils.buildQuery(filterBy, orderBy, first, last, before, after)) 55 | 56 | return this.postsService 57 | .find({ 58 | ...query, 59 | where: JSON.stringify(query.where) 60 | }) 61 | .toPromise() 62 | } 63 | 64 | @ResolveField('comments') 65 | async getComments( 66 | @Parent() user: User, 67 | @Args('q') q: string, 68 | @Args('first') first: number, 69 | @Args('last') last: number, 70 | @Args('before') before: string, 71 | @Args('after') after: string, 72 | @Args('filterBy') filterBy: any, 73 | @Args('orderBy') orderBy: string 74 | ): Promise { 75 | const query = { where: { author: user.id } } 76 | 77 | if (!isEmpty(q)) merge(query, { where: { text: { _iLike: q } } }) 78 | 79 | merge(query, await this.queryUtils.buildQuery(filterBy, orderBy, first, last, before, after)) 80 | 81 | return this.commentsService 82 | .find({ 83 | ...query, 84 | where: JSON.stringify(query.where) 85 | }) 86 | .toPromise() 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /api-gateway/src/users/users.interface.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs' 2 | import { Metadata } from 'grpc' 3 | 4 | import { IId, IQuery, ICount } from '../commons/commons.interface' 5 | import { User, UsersConnection } from '../graphql/typings' 6 | import { UserDto } from './user.dto' 7 | 8 | interface UpdateUserInput { 9 | id: string 10 | data: UserDto 11 | } 12 | 13 | export interface IUsersService { 14 | find(query: IQuery, metadata?: Metadata): Observable 15 | findById(id: IId, metadata?: Metadata): Observable 16 | findOne(query: IQuery, metadata?: Metadata): Observable 17 | count(query: IQuery, metadata?: Metadata): Observable 18 | create(input: UserDto, metadata?: Metadata): Observable 19 | update(input: UpdateUserInput): Observable 20 | destroy(query: IQuery, metadata?: Metadata): Observable 21 | } 22 | -------------------------------------------------------------------------------- /api-gateway/src/users/users.module.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path' 2 | import { Module, forwardRef } from '@nestjs/common' 3 | import { ConfigModule, ConfigService } from '@nestjs/config' 4 | import { LoggerModule } from 'nestjs-pino' 5 | import { ClientProxyFactory, Transport, ClientGrpcProxy } from '@nestjs/microservices' 6 | 7 | import { UsersTypeResolver } from './users-type.resolver' 8 | import { UsersQueryResolver } from './users-query.resolver' 9 | import { UsersMutationResolver } from './users-mutation.resolver' 10 | 11 | import { UtilsModule } from '../utils/utils.module' 12 | import { CommentsModule } from '../comments/comments.module' 13 | import { PostsModule } from '../posts/posts.module' 14 | 15 | @Module({ 16 | imports: [ConfigModule, LoggerModule, UtilsModule, forwardRef(() => CommentsModule), forwardRef(() => PostsModule)], 17 | providers: [ 18 | UsersTypeResolver, 19 | UsersQueryResolver, 20 | UsersMutationResolver, 21 | { 22 | provide: 'UsersServiceClient', 23 | useFactory: (configService: ConfigService): ClientGrpcProxy => { 24 | return ClientProxyFactory.create({ 25 | transport: Transport.GRPC, 26 | options: { 27 | url: configService.get('USERS_SVC_URL'), 28 | package: 'user', 29 | protoPath: join(__dirname, '../_proto/user.proto'), 30 | loader: { 31 | keepCase: true, 32 | enums: String, 33 | oneofs: true, 34 | arrays: true 35 | } 36 | } 37 | }) 38 | }, 39 | inject: [ConfigService] 40 | } 41 | ], 42 | exports: ['UsersServiceClient'] 43 | }) 44 | export class UsersModule {} 45 | -------------------------------------------------------------------------------- /api-gateway/src/utils/password.utils.ts: -------------------------------------------------------------------------------- 1 | import { compare, hash } from 'bcryptjs' 2 | 3 | import { isEmpty } from 'lodash' 4 | import { Injectable } from '@nestjs/common' 5 | 6 | @Injectable() 7 | export class PasswordUtils { 8 | async compare(password: string, hash: string): Promise { 9 | return compare(password, hash) 10 | } 11 | 12 | async hash(password: string): Promise { 13 | if (isEmpty(password) || password.length < 8) { 14 | throw new Error('Password must be at least 8 characters.') 15 | } 16 | 17 | return hash(password, 10) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /api-gateway/src/utils/query.utils.ts: -------------------------------------------------------------------------------- 1 | import { isEmpty, isNil, merge } from 'lodash' 2 | import { Injectable } from '@nestjs/common' 3 | 4 | import { IQuery } from '../commons/commons.interface' 5 | 6 | @Injectable() 7 | export class QueryUtils { 8 | async getFilters(filterBy: Record): Promise> { 9 | const queryFilters = { where: {} } 10 | 11 | if (!isEmpty(filterBy)) Object.assign(queryFilters.where, filterBy) 12 | 13 | return queryFilters 14 | } 15 | 16 | async getOrder(orderBy: string): Promise { 17 | const queryOrder: IQuery = {} 18 | 19 | if (!isEmpty(orderBy)) { 20 | if (orderBy.trim().charAt(0) === '-') { 21 | Object.assign(queryOrder, { orderBy: [orderBy.trim().substr(1), 'DESC'] }) 22 | } else { 23 | Object.assign(queryOrder, { orderBy: [orderBy.trim(), 'ASC'] }) 24 | } 25 | } 26 | 27 | return queryOrder 28 | } 29 | 30 | async getCursor(first: number, last: number, before: string, after: string): Promise { 31 | const queryCursor: IQuery = {} 32 | 33 | if (!isNil(first)) Object.assign(queryCursor, { limit: first }) 34 | 35 | if (!isEmpty(after)) { 36 | Object.assign(queryCursor, { after, limit: first }) 37 | } else if (!isEmpty(before)) { 38 | Object.assign(queryCursor, { before, limit: last }) 39 | } 40 | 41 | return queryCursor 42 | } 43 | 44 | async buildQuery(filterBy: Record, orderBy: string, first: number, last: number, before: string, after: string): Promise { 45 | return merge({}, await this.getFilters(filterBy), await this.getOrder(orderBy), await this.getCursor(first, last, before, after)) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /api-gateway/src/utils/utils.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | 3 | import { QueryUtils } from './query.utils' 4 | import { PasswordUtils } from './password.utils' 5 | 6 | @Module({ 7 | exports: [QueryUtils, PasswordUtils], 8 | providers: [QueryUtils, PasswordUtils] 9 | }) 10 | export class UtilsModule {} 11 | -------------------------------------------------------------------------------- /api-gateway/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /api-gateway/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "esModuleInterop": true, 7 | "emitDecoratorMetadata": true, 8 | "experimentalDecorators": true, 9 | "target": "ES2019", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true 14 | }, 15 | "exclude": ["node_modules", "dist", "__tests__", "**/*.spec.ts", "**/*.test.ts"] 16 | } 17 | -------------------------------------------------------------------------------- /docs/img/archi-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkssharma/nestjs-graphql-microservices/40225721c938adddb4488a814251d81034b544fb/docs/img/archi-diagram.png -------------------------------------------------------------------------------- /docs/img/graph-model.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkssharma/nestjs-graphql-microservices/40225721c938adddb4488a814251d81034b544fb/docs/img/graph-model.png -------------------------------------------------------------------------------- /k8s/configuration/api-gateway.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ConfigMap 4 | metadata: 5 | name: api-gateway 6 | namespace: dev 7 | labels: 8 | app: api-gateway 9 | data: 10 | NODE_ENV: "development" 11 | GRAPHQL_PORT: "3000" 12 | COMMENTS_SVC_URL: :50051 13 | POSTS_SVC_URL: :50051 14 | USERS_SVC_URL: :50051 15 | MAILER_SVC_URL: :50051 16 | -------------------------------------------------------------------------------- /k8s/configuration/cache.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Secret 4 | metadata: 5 | name: cache 6 | namespace: dev 7 | type: Opaque 8 | data: 9 | REDIS_HOST: 10 | REDIS_PORT: NjM3OQ== 11 | REDIS_PASSWORD: 12 | -------------------------------------------------------------------------------- /k8s/configuration/comments-svc.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ConfigMap 4 | metadata: 5 | name: comments-svc 6 | namespace: dev 7 | labels: 8 | app: comments-svc 9 | data: 10 | NODE_ENV: "development" 11 | GRPC_HOST: "0.0.0.0" 12 | GRPC_PORT: "50051" 13 | -------------------------------------------------------------------------------- /k8s/configuration/database.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Secret 4 | metadata: 5 | name: database 6 | namespace: dev 7 | type: Opaque 8 | data: 9 | DB_HOST: 10 | DB_PORT: NTQzMg== 11 | DB_USERNAME: 12 | DB_PASSWORD: 13 | DB_DATABASE: 14 | DB_SCHEMA: cHVibGlj 15 | DB_SYNC: dHJ1ZQ== 16 | -------------------------------------------------------------------------------- /k8s/configuration/jwt.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Secret 4 | metadata: 5 | name: jwt 6 | namespace: dev 7 | type: Opaque 8 | data: 9 | JWT_ACCESSTOKEN_SECRET: 10 | JWT_REFRESHTOKEN_SECRET: 11 | JWT_ISSUER: 12 | JWT_AUDIENCE: 13 | -------------------------------------------------------------------------------- /k8s/configuration/posts-svc.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ConfigMap 4 | metadata: 5 | name: posts-svc 6 | namespace: dev 7 | labels: 8 | app: posts-svc 9 | data: 10 | NODE_ENV: "development" 11 | GRPC_HOST: "0.0.0.0" 12 | GRPC_PORT: "50051" 13 | -------------------------------------------------------------------------------- /k8s/configuration/smtp.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Secret 4 | metadata: 5 | name: smtp 6 | namespace: dev 7 | type: Opaque 8 | data: 9 | SMTP_HOST: 10 | SMTP_PORT: NTg3 11 | SMTP_SECURE: dHJ1ZQ== 12 | SMTP_USER: 13 | SMTP_PASS: 14 | -------------------------------------------------------------------------------- /k8s/configuration/users-svc.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ConfigMap 4 | metadata: 5 | name: users-svc 6 | namespace: dev 7 | labels: 8 | app: users-svc 9 | data: 10 | NODE_ENV: "development" 11 | GRPC_HOST: "0.0.0.0" 12 | GRPC_PORT: "50051" 13 | -------------------------------------------------------------------------------- /k8s/services/api-gateway.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: Service 3 | apiVersion: v1 4 | metadata: 5 | name: api-gateway 6 | namespace: dev 7 | labels: 8 | app: api-gateway 9 | spec: 10 | selector: 11 | app: api-gateway 12 | ports: 13 | - name: http 14 | port: 80 15 | targetPort: http 16 | protocol: TCP 17 | --- 18 | apiVersion: extensions/v1beta1 19 | kind: Ingress 20 | metadata: 21 | name: api-gateway 22 | namespace: dev 23 | annotations: 24 | kubernetes.io/ingress.class: "nginx" 25 | certmanager.k8s.io/cluster-issuer: "letsencrypt-prod" 26 | nginx.ingress.kubernetes.io/ssl-redirect: "true" 27 | nginx.ingress.kubernetes.io/rewrite-target: / 28 | nginx.ingress.kubernetes.io/configuration-snippet: | 29 | more_set_headers "X-Content-Type-Options: nosniff"; 30 | more_set_headers "X-Download-Options: noopen"; 31 | more_set_headers "X-UA-Compatible: IE=Edge,chrome=1"; 32 | more_set_headers "X-XSS-Protection: 1; mode=block"; 33 | more_set_headers "X-Frame-Options: SAMEORIGIN"; 34 | 35 | spec: 36 | tls: 37 | - hosts: 38 | - example.com 39 | secretName: api-gateway-cert 40 | rules: 41 | - host: example.com 42 | http: 43 | paths: 44 | - backend: 45 | serviceName: api-gateway 46 | servicePort: http 47 | path: / 48 | -------------------------------------------------------------------------------- /k8s/services/comments-svc.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: Service 3 | apiVersion: v1 4 | metadata: 5 | name: comments-svc 6 | namespace: dev 7 | labels: 8 | app: comments-svc 9 | spec: 10 | selector: 11 | app: comments-svc 12 | ports: 13 | - name: http 14 | port: 80 15 | targetPort: http 16 | protocol: TCP 17 | -------------------------------------------------------------------------------- /k8s/services/mailer-svc.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: Service 3 | apiVersion: v1 4 | metadata: 5 | name: mailer-svc 6 | namespace: dev 7 | labels: 8 | app: mailer-svc 9 | spec: 10 | selector: 11 | app: mailer-svc 12 | ports: 13 | - name: http 14 | port: 80 15 | targetPort: http 16 | protocol: TCP 17 | -------------------------------------------------------------------------------- /k8s/services/posts-svc.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: Service 3 | apiVersion: v1 4 | metadata: 5 | name: posts-svc 6 | namespace: dev 7 | labels: 8 | app: posts-svc 9 | spec: 10 | selector: 11 | app: posts-svc 12 | ports: 13 | - name: http 14 | port: 80 15 | targetPort: http 16 | protocol: TCP 17 | -------------------------------------------------------------------------------- /k8s/services/users-svc.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: Service 3 | apiVersion: v1 4 | metadata: 5 | name: users-svc 6 | namespace: dev 7 | labels: 8 | app: users-svc 9 | spec: 10 | selector: 11 | app: users-svc 12 | ports: 13 | - name: http 14 | port: 80 15 | targetPort: http 16 | protocol: TCP 17 | -------------------------------------------------------------------------------- /k8s/workloads/api-gateway.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: api-gateway 6 | namespace: dev 7 | labels: 8 | app: api-gateway 9 | spec: 10 | replicas: 3 11 | selector: 12 | matchLabels: 13 | app: api-gateway 14 | strategy: 15 | type: RollingUpdate 16 | rollingUpdate: 17 | maxSurge: 50% 18 | maxUnavailable: 25% 19 | template: 20 | metadata: 21 | labels: 22 | app: api-gateway 23 | spec: 24 | containers: 25 | - name: api-gateway 26 | image: "api-gateway:latest" 27 | imagePullPolicy: Always 28 | resources: 29 | requests: 30 | cpu: 50m 31 | memory: 150Mi 32 | limits: 33 | cpu: 100m 34 | memory: 300Mi 35 | envFrom: 36 | - configMapRef: 37 | name: api-gateway 38 | - secretRef: 39 | name: cache 40 | - secretRef: 41 | name: jwt 42 | ports: 43 | - name: http 44 | containerPort: 3000 45 | protocol: TCP 46 | livenessProbe: 47 | httpGet: 48 | path: /healthz 49 | port: 3000 50 | initialDelaySeconds: 10 51 | periodSeconds: 30 52 | successThreshold: 1 53 | failureThreshold: 3 54 | timeoutSeconds: 10 55 | readinessProbe: 56 | httpGet: 57 | path: /healthz 58 | port: 3000 59 | initialDelaySeconds: 5 60 | periodSeconds: 30 61 | successThreshold: 1 62 | failureThreshold: 3 63 | timeoutSeconds: 10 64 | terminationGracePeriodSeconds: 60 65 | # affinity: 66 | # nodeAffinity: 67 | # requiredDuringSchedulingIgnoredDuringExecution: 68 | # nodeSelectorTerms: 69 | # - matchExpressions: 70 | # - key: node-pool 71 | # operator: In 72 | # values: 73 | # - dev 74 | # --- 75 | # apiVersion: autoscaling/v2beta1 76 | # kind: HorizontalPodAutoscaler 77 | # metadata: 78 | # name: api-gateway 79 | # namespace: dev 80 | # labels: 81 | # app: api-gateway 82 | # spec: 83 | # scaleTargetRef: 84 | # apiVersion: apps/v1 85 | # name: api-gateway 86 | # kind: Deployment 87 | # minReplicas: 3 88 | # maxReplicas: 10 89 | # metrics: 90 | # - type: Resource 91 | # resource: 92 | # name: cpu 93 | # targetAverageUtilization: 80 94 | # - type: Resource 95 | # resource: 96 | # name: memory 97 | # targetAverageUtilization: 80 98 | -------------------------------------------------------------------------------- /k8s/workloads/comments-svc.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: comments-svc 6 | namespace: dev 7 | labels: 8 | app: comments-svc 9 | spec: 10 | replicas: 3 11 | selector: 12 | matchLabels: 13 | app: comments-svc 14 | strategy: 15 | type: RollingUpdate 16 | rollingUpdate: 17 | maxSurge: 50% 18 | maxUnavailable: 25% 19 | template: 20 | metadata: 21 | labels: 22 | app: comments-svc 23 | spec: 24 | containers: 25 | - name: comments-svc 26 | image: "comments-svc:latest" 27 | imagePullPolicy: Always 28 | resources: 29 | requests: 30 | cpu: 50m 31 | memory: 150Mi 32 | limits: 33 | cpu: 100m 34 | memory: 300Mi 35 | envFrom: 36 | - configMapRef: 37 | name: comments-svc 38 | - secretRef: 39 | name: database 40 | - secretRef: 41 | name: cache 42 | ports: 43 | - name: http 44 | containerPort: 50051 45 | protocol: TCP 46 | livenessProbe: 47 | exec: 48 | command: 49 | - "/bin/grpc_health_probe" 50 | - "-addr=:50051" 51 | initialDelaySeconds: 10 52 | periodSeconds: 30 53 | successThreshold: 1 54 | failureThreshold: 3 55 | timeoutSeconds: 10 56 | readinessProbe: 57 | exec: 58 | command: 59 | - "/bin/grpc_health_probe" 60 | - "-addr=:50051" 61 | initialDelaySeconds: 5 62 | periodSeconds: 30 63 | successThreshold: 1 64 | failureThreshold: 3 65 | timeoutSeconds: 10 66 | terminationGracePeriodSeconds: 60 67 | # affinity: 68 | # nodeAffinity: 69 | # requiredDuringSchedulingIgnoredDuringExecution: 70 | # nodeSelectorTerms: 71 | # - matchExpressions: 72 | # - key: node-pool 73 | # operator: In 74 | # values: 75 | # - dev 76 | # --- 77 | # apiVersion: autoscaling/v2beta1 78 | # kind: HorizontalPodAutoscaler 79 | # metadata: 80 | # name: comments-svc 81 | # namespace: dev 82 | # labels: 83 | # app: comments-svc 84 | # spec: 85 | # scaleTargetRef: 86 | # apiVersion: apps/v1 87 | # name: comments-svc 88 | # kind: Deployment 89 | # minReplicas: 3 90 | # maxReplicas: 10 91 | # metrics: 92 | # - type: Resource 93 | # resource: 94 | # name: cpu 95 | # targetAverageUtilization: 80 96 | # - type: Resource 97 | # resource: 98 | # name: memory 99 | # targetAverageUtilization: 80 100 | -------------------------------------------------------------------------------- /k8s/workloads/mailer-svc.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: mailer-svc 6 | namespace: dev 7 | labels: 8 | app: mailer-svc 9 | spec: 10 | replicas: 3 11 | selector: 12 | matchLabels: 13 | app: mailer-svc 14 | strategy: 15 | type: RollingUpdate 16 | rollingUpdate: 17 | maxSurge: 50% 18 | maxUnavailable: 25% 19 | template: 20 | metadata: 21 | labels: 22 | app: mailer-svc 23 | spec: 24 | containers: 25 | - name: mailer-svc 26 | image: "mailer-svc:latest" 27 | imagePullPolicy: Always 28 | resources: 29 | requests: 30 | cpu: 50m 31 | memory: 150Mi 32 | limits: 33 | cpu: 100m 34 | memory: 300Mi 35 | envFrom: 36 | - configMapRef: 37 | name: mailer-svc 38 | - secretRef: 39 | name: smtp 40 | ports: 41 | - name: http 42 | containerPort: 50051 43 | protocol: TCP 44 | livenessProbe: 45 | exec: 46 | command: 47 | - "/bin/grpc_health_probe" 48 | - "-addr=:50051" 49 | initialDelaySeconds: 10 50 | periodSeconds: 30 51 | successThreshold: 1 52 | failureThreshold: 3 53 | timeoutSeconds: 10 54 | readinessProbe: 55 | exec: 56 | command: 57 | - "/bin/grpc_health_probe" 58 | - "-addr=:50051" 59 | initialDelaySeconds: 5 60 | periodSeconds: 30 61 | successThreshold: 1 62 | failureThreshold: 3 63 | timeoutSeconds: 10 64 | terminationGracePeriodSeconds: 60 65 | # affinity: 66 | # nodeAffinity: 67 | # requiredDuringSchedulingIgnoredDuringExecution: 68 | # nodeSelectorTerms: 69 | # - matchExpressions: 70 | # - key: node-pool 71 | # operator: In 72 | # values: 73 | # - dev 74 | # --- 75 | # apiVersion: autoscaling/v2beta1 76 | # kind: HorizontalPodAutoscaler 77 | # metadata: 78 | # name: mailer-svc 79 | # namespace: dev 80 | # labels: 81 | # app: mailer-svc 82 | # spec: 83 | # scaleTargetRef: 84 | # apiVersion: apps/v1 85 | # name: mailer-svc 86 | # kind: Deployment 87 | # minReplicas: 3 88 | # maxReplicas: 10 89 | # metrics: 90 | # - type: Resource 91 | # resource: 92 | # name: cpu 93 | # targetAverageUtilization: 80 94 | # - type: Resource 95 | # resource: 96 | # name: memory 97 | # targetAverageUtilization: 80 98 | -------------------------------------------------------------------------------- /k8s/workloads/posts-svc.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: posts-svc 6 | namespace: dev 7 | labels: 8 | app: posts-svc 9 | spec: 10 | replicas: 3 11 | selector: 12 | matchLabels: 13 | app: posts-svc 14 | strategy: 15 | type: RollingUpdate 16 | rollingUpdate: 17 | maxSurge: 50% 18 | maxUnavailable: 25% 19 | template: 20 | metadata: 21 | labels: 22 | app: posts-svc 23 | spec: 24 | containers: 25 | - name: posts-svc 26 | image: "posts-svc:latest" 27 | imagePullPolicy: Always 28 | resources: 29 | requests: 30 | cpu: 50m 31 | memory: 150Mi 32 | limits: 33 | cpu: 100m 34 | memory: 300Mi 35 | envFrom: 36 | - configMapRef: 37 | name: posts-svc 38 | - secretRef: 39 | name: database 40 | - secretRef: 41 | name: cache 42 | ports: 43 | - name: http 44 | containerPort: 50051 45 | protocol: TCP 46 | livenessProbe: 47 | exec: 48 | command: 49 | - "/bin/grpc_health_probe" 50 | - "-addr=:50051" 51 | initialDelaySeconds: 10 52 | periodSeconds: 30 53 | successThreshold: 1 54 | failureThreshold: 3 55 | timeoutSeconds: 10 56 | readinessProbe: 57 | exec: 58 | command: 59 | - "/bin/grpc_health_probe" 60 | - "-addr=:50051" 61 | initialDelaySeconds: 5 62 | periodSeconds: 30 63 | successThreshold: 1 64 | failureThreshold: 3 65 | timeoutSeconds: 10 66 | terminationGracePeriodSeconds: 60 67 | # affinity: 68 | # nodeAffinity: 69 | # requiredDuringSchedulingIgnoredDuringExecution: 70 | # nodeSelectorTerms: 71 | # - matchExpressions: 72 | # - key: node-pool 73 | # operator: In 74 | # values: 75 | # - dev 76 | # --- 77 | # apiVersion: autoscaling/v2beta1 78 | # kind: HorizontalPodAutoscaler 79 | # metadata: 80 | # name: posts-svc 81 | # namespace: dev 82 | # labels: 83 | # app: posts-svc 84 | # spec: 85 | # scaleTargetRef: 86 | # apiVersion: apps/v1 87 | # name: posts-svc 88 | # kind: Deployment 89 | # minReplicas: 3 90 | # maxReplicas: 10 91 | # metrics: 92 | # - type: Resource 93 | # resource: 94 | # name: cpu 95 | # targetAverageUtilization: 80 96 | # - type: Resource 97 | # resource: 98 | # name: memory 99 | # targetAverageUtilization: 80 100 | -------------------------------------------------------------------------------- /k8s/workloads/users-svc.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: users-svc 6 | namespace: dev 7 | labels: 8 | app: users-svc 9 | spec: 10 | replicas: 3 11 | selector: 12 | matchLabels: 13 | app: users-svc 14 | strategy: 15 | type: RollingUpdate 16 | rollingUpdate: 17 | maxSurge: 50% 18 | maxUnavailable: 25% 19 | template: 20 | metadata: 21 | labels: 22 | app: users-svc 23 | spec: 24 | containers: 25 | - name: users-svc 26 | image: "users-svc:latest" 27 | imagePullPolicy: Always 28 | resources: 29 | requests: 30 | cpu: 50m 31 | memory: 150Mi 32 | limits: 33 | cpu: 100m 34 | memory: 300Mi 35 | envFrom: 36 | - configMapRef: 37 | name: users-svc 38 | - secretRef: 39 | name: database 40 | - secretRef: 41 | name: cache 42 | ports: 43 | - name: http 44 | containerPort: 50051 45 | protocol: TCP 46 | livenessProbe: 47 | exec: 48 | command: 49 | - "/bin/grpc_health_probe" 50 | - "-addr=:50051" 51 | initialDelaySeconds: 10 52 | periodSeconds: 30 53 | successThreshold: 1 54 | failureThreshold: 3 55 | timeoutSeconds: 10 56 | readinessProbe: 57 | exec: 58 | command: 59 | - "/bin/grpc_health_probe" 60 | - "-addr=:50051" 61 | initialDelaySeconds: 5 62 | periodSeconds: 30 63 | successThreshold: 1 64 | failureThreshold: 3 65 | timeoutSeconds: 10 66 | terminationGracePeriodSeconds: 60 67 | # affinity: 68 | # nodeAffinity: 69 | # requiredDuringSchedulingIgnoredDuringExecution: 70 | # nodeSelectorTerms: 71 | # - matchExpressions: 72 | # - key: node-pool 73 | # operator: In 74 | # values: 75 | # - dev 76 | # --- 77 | # apiVersion: autoscaling/v2beta1 78 | # kind: HorizontalPodAutoscaler 79 | # metadata: 80 | # name: users-svc 81 | # namespace: dev 82 | # labels: 83 | # app: users-svc 84 | # spec: 85 | # scaleTargetRef: 86 | # apiVersion: apps/v1 87 | # name: users-svc 88 | # kind: Deployment 89 | # minReplicas: 3 90 | # maxReplicas: 10 91 | # metrics: 92 | # - type: Resource 93 | # resource: 94 | # name: cpu 95 | # targetAverageUtilization: 80 96 | # - type: Resource 97 | # resource: 98 | # name: memory 99 | # targetAverageUtilization: 80 100 | -------------------------------------------------------------------------------- /microservices/comments-svc/.env.example: -------------------------------------------------------------------------------- 1 | # Environment 2 | NODE_ENV= 3 | 4 | # gRPC Server Options 5 | GRPC_HOST= 6 | GRPC_PORT= 7 | 8 | # DB Options 9 | DB_HOST= 10 | DB_PORT= 11 | DB_USERNAME= 12 | DB_PASSWORD= 13 | DB_DATABASE= 14 | DB_SCHEMA= 15 | DB_SYNC= 16 | 17 | # Cache Options 18 | REDIS_HOST= 19 | REDIS_PORT= 20 | -------------------------------------------------------------------------------- /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 | - "plugin:@typescript-eslint/eslint-recommended" 12 | - "plugin:@typescript-eslint/recommended" 13 | - "prettier" 14 | - "prettier/@typescript-eslint" 15 | - "plugin:prettier/recommended" 16 | - "plugin:jest/recommended" 17 | - "plugin:import/errors" 18 | - "plugin:import/warnings" 19 | - "plugin:import/typescript" 20 | root: true 21 | env: 22 | node: true 23 | jest: true 24 | rules: 25 | "@typescript-eslint/interface-name-prefix": off 26 | "@typescript-eslint/explicit-function-return-type": off 27 | "@typescript-eslint/no-explicit-any": off 28 | "@typescript-eslint/ban-ts-comment": 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: 300 42 | semi: false 43 | singleQuote: true 44 | endOfline: "lf" 45 | trailingComma: "none" 46 | 47 | 48 | -------------------------------------------------------------------------------- /microservices/comments-svc/.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | cache: 3 | key: ${CI_PROJECT_NAME} 4 | paths: 5 | - node_modules/ 6 | 7 | variables: 8 | K8S_DEPLOYMENT_NAME: comments-svc 9 | K8S_CONTAINER_NAME: comments-svc 10 | K8S_DEV_NAMESPACE: dev 11 | K8S_STG_NAMESPACE: stg 12 | K8S_PROD_NAMESPACE: prod 13 | CONTAINER_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA 14 | 15 | stages: 16 | - setup 17 | - test 18 | - build 19 | - release 20 | - deploy 21 | 22 | setup: 23 | image: node:12-alpine 24 | stage: setup 25 | before_script: 26 | - apk add --no-cache make g++ python postgresql-dev &> /dev/null 27 | - rm -rf ./node_modules/.cache 28 | script: 29 | - npm install --prefer-offline &> /dev/null 30 | only: 31 | - merge_requests 32 | tags: 33 | - docker 34 | allow_failure: false 35 | when: always 36 | 37 | test: 38 | image: node:12-alpine 39 | stage: test 40 | dependencies: 41 | - setup 42 | before_script: 43 | - apk add --no-cache libpq 44 | script: 45 | - npm run lint 46 | - npm run test:coverage 47 | coverage: /All files[^|]*\|[^|]*\s+([\d\.]+)/ 48 | services: 49 | - name: postgres:12-alpine 50 | alias: db 51 | - name: redis:5-alpine 52 | alias: cache 53 | variables: 54 | NODE_ENV: "test" 55 | GRPC_HOST: "0.0.0.0" 56 | GRPC_PORT: "50051" 57 | DB_HOST: "db" 58 | DB_PORT: "5432" 59 | DB_USERNAME: "postgres" 60 | DB_PASSWORD: "postgres" 61 | DB_DATABASE: "postgres" 62 | DB_SCHEMA: "public" 63 | DB_SYNC: "true" 64 | REDIS_HOST: "cache" 65 | REDIS_PORT: "6379" 66 | POSTGRES_USER: "postgres" 67 | POSTGRES_PASSWORD: "postgres" 68 | only: 69 | - dev 70 | - merge_requests 71 | tags: 72 | - docker 73 | allow_failure: false 74 | when: always 75 | 76 | build: 77 | image: node:12-alpine 78 | stage: build 79 | dependencies: 80 | - test 81 | script: 82 | - npm run build 83 | only: 84 | - dev 85 | tags: 86 | - docker 87 | artifacts: 88 | paths: 89 | - dist/ 90 | allow_failure: false 91 | when: always 92 | 93 | release: 94 | image: docker:19.03.8 95 | stage: release 96 | dependencies: 97 | - build 98 | script: 99 | - docker build -t ${CONTAINER_IMAGE} . 100 | - docker tag ${CONTAINER_IMAGE} $CI_REGISTRY_IMAGE:dev 101 | - docker tag ${CONTAINER_IMAGE} $CI_REGISTRY_IMAGE:latest 102 | - docker login -u ${CI_REGISTRY_USER} -p $CI_REGISTRY_PASSWORD ${CI_REGISTRY} 103 | - docker push ${CONTAINER_IMAGE} 104 | - docker push $CI_REGISTRY_IMAGE:dev 105 | - docker push $CI_REGISTRY_IMAGE:latest 106 | only: 107 | - dev 108 | tags: 109 | - docker 110 | allow_failure: false 111 | when: always 112 | 113 | deploy-dev: 114 | image: docker:19.03.8 115 | stage: deploy 116 | dependencies: 117 | - release 118 | script: 119 | - mkdir -p /builds/${CI_PROJECT_PATH} 120 | - echo "${KUBE_CONFIG}" | base64 -d > /builds/${CI_PROJECT_PATH}/config 121 | - docker run --rm -v /builds/${CI_PROJECT_PATH}/config:/.kube/config bitnami/kubectl:1.17.4 set image deployment/${K8S_DEPLOYMENT_NAME} ${K8S_CONTAINER_NAME}=${CONTAINER_IMAGE} -n ${K8S_DEV_NAMESPACE} 122 | only: 123 | - dev 124 | tags: 125 | - docker 126 | allow_failure: true 127 | when: always 128 | 129 | deploy-stg: 130 | image: docker:19.03.8 131 | stage: deploy 132 | dependencies: 133 | - release 134 | script: 135 | - mkdir -p /builds/${CI_PROJECT_PATH} 136 | - echo "${KUBE_CONFIG}" | base64 -d > /builds/${CI_PROJECT_PATH}/config 137 | - docker run --rm -v /builds/${CI_PROJECT_PATH}/config:/.kube/config bitnami/kubectl:1.17.4 set image deployment/${K8S_DEPLOYMENT_NAME} ${K8S_CONTAINER_NAME}=${CONTAINER_IMAGE} -n ${K8S_STG_NAMESPACE} 138 | only: 139 | - dev 140 | tags: 141 | - docker 142 | allow_failure: true 143 | when: manual 144 | 145 | deploy-prod: 146 | image: docker:19.03.8 147 | stage: deploy 148 | dependencies: 149 | - release 150 | script: 151 | - mkdir -p /builds/${CI_PROJECT_PATH} 152 | - echo "${KUBE_CONFIG}" | base64 -d > /builds/${CI_PROJECT_PATH}/config 153 | - docker run --rm -v /builds/${CI_PROJECT_PATH}/config:/.kube/config bitnami/kubectl:1.17.4 set image deployment/${K8S_DEPLOYMENT_NAME} ${K8S_CONTAINER_NAME}=${CONTAINER_IMAGE} -n ${K8S_PROD_NAMESPACE} 154 | only: 155 | - dev 156 | tags: 157 | - docker 158 | allow_failure: true 159 | when: manual 160 | -------------------------------------------------------------------------------- /microservices/comments-svc/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12-alpine as build 2 | 3 | WORKDIR /usr/share/comments-svc 4 | 5 | ADD dist package.json ./ 6 | 7 | RUN apk add --no-cache make g++ python postgresql-dev \ 8 | && npm install --production 9 | 10 | FROM node:12-alpine 11 | 12 | RUN apk add --no-cache libpq 13 | 14 | ADD https://github.com/grpc-ecosystem/grpc-health-probe/releases/download/v0.3.2/grpc_health_probe-linux-amd64 /bin/grpc_health_probe 15 | 16 | RUN chmod +x /bin/grpc_health_probe 17 | 18 | WORKDIR /usr/share/comments-svc 19 | 20 | COPY --from=build /usr/share/comments-svc . 21 | 22 | EXPOSE 50051 23 | 24 | CMD ["node", "main.js"] 25 | -------------------------------------------------------------------------------- /microservices/comments-svc/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | 5 | comments-svc: 6 | build: 7 | context: "." 8 | networks: 9 | - "comments-svc" 10 | ports: 11 | - "50051:50051" 12 | depends_on: 13 | - "db" 14 | - "cache" 15 | environment: 16 | NODE_ENV: "test" 17 | GRPC_HOST: "0.0.0.0" 18 | GRPC_PORT: "50051" 19 | DB_HOST: "db" 20 | DB_PORT: "5432" 21 | DB_USERNAME: "postgres" 22 | DB_PASSWORD: "postgres" 23 | DB_DATABASE: "postgres" 24 | DB_SCHEMA: "public" 25 | DB_SYNC: true 26 | REDIS_HOST: "cache" 27 | REDIS_PORT: "6379" 28 | healthcheck: 29 | test: ["CMD", "/bin/grpc_health_probe", "-addr=:50051"] 30 | interval: 30s 31 | timeout: 10s 32 | retries: 5 33 | restart: "on-failure" 34 | 35 | db: 36 | image: "postgres:12-alpine" 37 | networks: 38 | - "comments-svc" 39 | expose: 40 | - "5432" 41 | environment: 42 | POSTGRES_USER: "postgres" 43 | POSTGRES_PASSWORD: "postgres" 44 | healthcheck: 45 | test: ["CMD-SHELL", "sh -c 'pg_isready -U postgres'"] 46 | interval: 30s 47 | timeout: 30s 48 | retries: 3 49 | restart: "on-failure" 50 | 51 | cache: 52 | image: "redis:5-alpine" 53 | networks: 54 | - "comments-svc" 55 | expose: 56 | - "6379" 57 | healthcheck: 58 | test: ["CMD-SHELL", "sh -c 'redis-cli PING'"] 59 | interval: 30s 60 | timeout: 30s 61 | retries: 3 62 | restart: "on-failure" 63 | 64 | networks: 65 | comments-svc: 66 | -------------------------------------------------------------------------------- /microservices/comments-svc/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | setupFilesAfterEnv: [ 3 | "jest-extended" 4 | ], 5 | coverageThreshold: { 6 | global: { 7 | branches: 90, 8 | functions: 90, 9 | lines: 90, 10 | statements: 90 11 | } 12 | }, 13 | collectCoverageFrom: [ 14 | "src/**/*.ts", 15 | "!**/node_modules/**" 16 | ], 17 | "rootDir": "src", 18 | "preset": "ts-jest", 19 | testEnvironment: "node" 20 | } 21 | -------------------------------------------------------------------------------- /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 | "lint": "eslint --max-warnings=0 '{src,__tests__}/**/*.ts'", 12 | "lint:fix": "eslint --fix '{src,__tests__}/**/*.ts'", 13 | "test": "jest", 14 | "test:watch": "jest --watch", 15 | "test:coverage": "jest --coverage", 16 | "copy:protos": "cpy ../../_proto ./src/_proto" 17 | }, 18 | "dependencies": { 19 | "@grpc/proto-loader": "0.5.4", 20 | "@nestjs/common": "7.1.1", 21 | "@nestjs/config": "0.5.0", 22 | "@nestjs/core": "7.1.1", 23 | "@nestjs/microservices": "7.1.1", 24 | "@nestjs/sequelize": "0.1.0", 25 | "aigle": "1.14.1", 26 | "grpc": "1.24.2", 27 | "lodash": "4.17.15", 28 | "nestjs-pino": "1.2.0", 29 | "pg": "8.2.1", 30 | "pg-hstore": "2.3.3", 31 | "pg-native": "3.0.0", 32 | "pino": "6.3.0", 33 | "reflect-metadata": "0.1.13", 34 | "sequelize": "5.21.11", 35 | "sequelize-cursor-pagination": "1.7.0", 36 | "sequelize-typescript": "1.1.0" 37 | }, 38 | "devDependencies": { 39 | "@nestjs/cli": "7.2.0", 40 | "@nestjs/schematics": "7.0.0", 41 | "@nestjs/testing": "7.1.1", 42 | "@types/faker": "4.1.12", 43 | "@types/jest": "25.2.3", 44 | "@types/lodash": "4.14.153", 45 | "@types/node": "14.0.5", 46 | "@types/sequelize": "4.28.9", 47 | "@typescript-eslint/eslint-plugin": "3.0.2", 48 | "@typescript-eslint/parser": "3.0.2", 49 | "cpy-cli": "3.1.1", 50 | "eslint": "7.1.0", 51 | "eslint-config-prettier": "6.11.0", 52 | "eslint-plugin-import": "2.20.2", 53 | "eslint-plugin-jest": "23.13.2", 54 | "eslint-plugin-prettier": "3.1.3", 55 | "faker": "4.1.0", 56 | "jest": "26.0.1", 57 | "jest-extended": "0.11.5", 58 | "pino-pretty": "4.0.0", 59 | "prettier": "2.0.5", 60 | "rimraf": "3.0.2", 61 | "ts-jest": "26.0.0", 62 | "typescript": "3.9.3" 63 | }, 64 | "repository": { 65 | "type": "git", 66 | "url": "git+ssh://git@github.com:benjsicam/nestjs-graphql-microservices.git" 67 | }, 68 | "author": "Benj Sicam (https://github.com/benjsicam)", 69 | "license": "MIT", 70 | "bugs": { 71 | "url": "https://github.com/benjsicam/nestjs-graphql-microservices/issues" 72 | }, 73 | "homepage": "https://github.com/benjsicam/nestjs-graphql-microservices#readme" 74 | } 75 | -------------------------------------------------------------------------------- /microservices/comments-svc/src/_proto/README.md: -------------------------------------------------------------------------------- 1 | ## Note on Maintenence 2 | 3 | Maintenance of these files should be done under the _proto folder on the root of the project. Use `npm run copy:protos` to copy updates to this directory. -------------------------------------------------------------------------------- /microservices/comments-svc/src/_proto/comment.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package comment; 4 | 5 | import "commons.proto"; 6 | 7 | message Comment { 8 | string id = 1; 9 | string text = 2; 10 | string author = 3; 11 | string post = 4; 12 | string createdAt = 5; 13 | string updatedAt = 6; 14 | int32 version = 7; 15 | } 16 | 17 | message CommentEdge { 18 | Comment node = 1; 19 | string cursor = 2; 20 | } 21 | 22 | message CreateCommentInput { 23 | string text = 1; 24 | string author = 2; 25 | string post = 3; 26 | } 27 | 28 | message UpdateCommentInput { 29 | string id = 1; 30 | Comment data = 2; 31 | } 32 | 33 | message FindCommentsPayload { 34 | repeated CommentEdge edges = 1; 35 | commons.PageInfo pageInfo = 2; 36 | } 37 | 38 | service CommentsService { 39 | rpc find (commons.Query) returns (FindCommentsPayload) {} 40 | rpc findById (commons.Id) returns (Comment) {} 41 | rpc findOne (commons.Query) returns (Comment) {} 42 | rpc count (commons.Query) returns (commons.Count) {} 43 | rpc create (CreateCommentInput) returns (Comment) {} 44 | rpc update (UpdateCommentInput) returns (Comment) {} 45 | rpc destroy (commons.Query) returns (commons.Count) {} 46 | } 47 | -------------------------------------------------------------------------------- /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 Query { 10 | repeated string select = 1; 11 | string where = 2; 12 | repeated string orderBy = 3; 13 | int32 limit = 4; 14 | string before = 5; 15 | string after = 6; 16 | } 17 | 18 | message PageInfo { 19 | string startCursor = 1; 20 | string endCursor = 2; 21 | bool hasNextPage = 3; 22 | bool hasPreviousPage = 4; 23 | } 24 | 25 | message Count { 26 | int32 count = 1; 27 | } 28 | -------------------------------------------------------------------------------- /microservices/comments-svc/src/_proto/mailer.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package mailer; 4 | 5 | message SendMailInput { 6 | string template = 1; 7 | string to = 2; 8 | bytes data = 3; 9 | } 10 | 11 | message SendMailPayload { 12 | bool isSent = 1; 13 | } 14 | 15 | service MailerService { 16 | rpc send (SendMailInput) returns (SendMailPayload) {} 17 | } 18 | -------------------------------------------------------------------------------- /microservices/comments-svc/src/_proto/post.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package post; 4 | 5 | import "commons.proto"; 6 | 7 | message Post { 8 | string id = 1; 9 | string title = 2; 10 | string body = 3; 11 | bool published = 4; 12 | string author = 5; 13 | string createdAt = 6; 14 | string updatedAt = 7; 15 | int32 version = 8; 16 | } 17 | 18 | message PostEdge { 19 | Post node = 1; 20 | string cursor = 2; 21 | } 22 | 23 | message CreatePostInput { 24 | string title = 2; 25 | string body = 3; 26 | bool published = 4; 27 | string author = 5; 28 | } 29 | 30 | message UpdatePostInput { 31 | string id = 1; 32 | Post data = 2; 33 | } 34 | 35 | message FindPostsPayload { 36 | repeated PostEdge edges = 1; 37 | commons.PageInfo pageInfo = 2; 38 | } 39 | 40 | service PostsService { 41 | rpc find (commons.Query) returns (FindPostsPayload) {} 42 | rpc findById (commons.Id) returns (Post) {} 43 | rpc findOne (commons.Query) returns (Post) {} 44 | rpc count (commons.Query) returns (commons.Count) {} 45 | rpc create (CreatePostInput) returns (Post) {} 46 | rpc update (UpdatePostInput) returns (Post) {} 47 | rpc destroy (commons.Query) returns (commons.Count) {} 48 | } 49 | -------------------------------------------------------------------------------- /microservices/comments-svc/src/_proto/user.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package user; 4 | 5 | import "commons.proto"; 6 | 7 | message User { 8 | string id = 1; 9 | string name = 2; 10 | string email = 3; 11 | string password = 4; 12 | int32 age = 5; 13 | string createdAt = 6; 14 | string updatedAt = 7; 15 | int32 version = 8; 16 | } 17 | 18 | message UserEdge { 19 | User node = 1; 20 | string cursor = 2; 21 | } 22 | 23 | message CreateUserInput { 24 | string name = 1; 25 | string email = 2; 26 | string password = 3; 27 | int32 age = 4; 28 | } 29 | 30 | message UpdateUserInput { 31 | string id = 1; 32 | User data = 2; 33 | } 34 | 35 | message FindUsersPayload { 36 | repeated UserEdge edges = 1; 37 | commons.PageInfo pageInfo = 2; 38 | } 39 | 40 | service UsersService { 41 | rpc find (commons.Query) returns (FindUsersPayload) {} 42 | rpc findById (commons.Id) returns (User) {} 43 | rpc findOne (commons.Query) returns (User) {} 44 | rpc count (commons.Query) returns (commons.Count) {} 45 | rpc create (CreateUserInput) returns (User) {} 46 | rpc update (UpdateUserInput) returns (User) {} 47 | rpc destroy (commons.Query) returns (commons.Count) {} 48 | } 49 | -------------------------------------------------------------------------------- /microservices/comments-svc/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { ConfigModule, ConfigService } from '@nestjs/config' 3 | import { SequelizeModule, SequelizeModuleOptions } from '@nestjs/sequelize' 4 | 5 | import { Op, OperatorsAliases } from 'sequelize' 6 | import { LoggerModule, PinoLogger } from 'nestjs-pino' 7 | 8 | import { CommentsModule } from './comments/comments.module' 9 | 10 | const operatorsAliases: OperatorsAliases = { 11 | _and: Op.and, 12 | _or: Op.or, 13 | _eq: Op.eq, 14 | _ne: Op.ne, 15 | _is: Op.is, 16 | _not: Op.not, 17 | _col: Op.col, 18 | _gt: Op.gt, 19 | _gte: Op.gte, 20 | _lt: Op.lt, 21 | _lte: Op.lte, 22 | _between: Op.between, 23 | _notBetween: Op.notBetween, 24 | _all: Op.all, 25 | _in: Op.in, 26 | _notIn: Op.notIn, 27 | _like: Op.like, 28 | _notLike: Op.notLike, 29 | _startsWith: Op.startsWith, 30 | _endsWith: Op.endsWith, 31 | _substring: Op.substring, 32 | _iLike: Op.iLike, 33 | _notILike: Op.notILike, 34 | _regexp: Op.regexp, 35 | _notRegexp: Op.notRegexp, 36 | _iRegexp: Op.iRegexp, 37 | _notIRegexp: Op.notIRegexp, 38 | _any: Op.any, 39 | _contains: Op.contains, 40 | _contained: Op.contained, 41 | _overlap: Op.overlap, 42 | _adjacent: Op.adjacent, 43 | _strictLeft: Op.strictLeft, 44 | _strictRight: Op.strictRight, 45 | _noExtendRight: Op.noExtendRight, 46 | _noExtendLeft: Op.noExtendLeft, 47 | _values: Op.values 48 | } 49 | 50 | @Module({ 51 | imports: [ 52 | ConfigModule.forRoot(), 53 | LoggerModule.forRootAsync({ 54 | imports: [ConfigModule], 55 | useFactory: async (configService: ConfigService) => ({ 56 | pinoHttp: { 57 | safe: true, 58 | prettyPrint: configService.get('NODE_ENV') !== 'production' 59 | } 60 | }), 61 | inject: [ConfigService] 62 | }), 63 | SequelizeModule.forRootAsync({ 64 | imports: [ConfigModule, LoggerModule], 65 | useFactory: async (configService: ConfigService, logger: PinoLogger): Promise => ({ 66 | dialect: 'postgres', 67 | host: configService.get('DB_HOST'), 68 | port: configService.get('DB_PORT'), 69 | username: configService.get('DB_USERNAME'), 70 | password: configService.get('DB_PASSWORD'), 71 | database: configService.get('DB_DATABASE'), 72 | logging: logger.info.bind(logger), 73 | typeValidation: true, 74 | benchmark: true, 75 | native: true, 76 | operatorsAliases, 77 | autoLoadModels: true, 78 | synchronize: configService.get('DB_SYNC'), 79 | define: { 80 | timestamps: true, 81 | underscored: true, 82 | version: true, 83 | schema: configService.get('DB_SCHEMA') 84 | } 85 | }), 86 | inject: [ConfigService, PinoLogger] 87 | }), 88 | CommentsModule 89 | ] 90 | }) 91 | export class AppModule {} 92 | -------------------------------------------------------------------------------- /microservices/comments-svc/src/comments/comment.dto.ts: -------------------------------------------------------------------------------- 1 | export class CommentDto { 2 | readonly id?: string 3 | readonly text?: string 4 | readonly author?: string 5 | readonly post?: string 6 | readonly createdAt?: string 7 | readonly updatedAt?: string 8 | readonly version?: number 9 | } 10 | -------------------------------------------------------------------------------- /microservices/comments-svc/src/comments/comment.model.ts: -------------------------------------------------------------------------------- 1 | import * as paginate from 'sequelize-cursor-pagination' 2 | import { Column, Model, Table, DataType, Index } from 'sequelize-typescript' 3 | 4 | @Table({ 5 | modelName: 'comment', 6 | tableName: 'comments' 7 | }) 8 | export class Comment extends Model { 9 | @Column({ 10 | primaryKey: true, 11 | type: DataType.UUID, 12 | defaultValue: DataType.UUIDV1, 13 | comment: 'The identifier for the comment record.' 14 | }) 15 | id: string 16 | 17 | @Column({ 18 | type: DataType.TEXT, 19 | comment: 'The comment text.' 20 | }) 21 | text: string 22 | 23 | @Index('comment_author') 24 | @Column({ 25 | type: DataType.UUID, 26 | comment: 'Ref: User. The author of the comment.' 27 | }) 28 | author: string 29 | 30 | @Index('comment_post') 31 | @Column({ 32 | type: DataType.UUID, 33 | comment: 'Ref: Post. The post for which the comment is associated with.' 34 | }) 35 | post: string 36 | } 37 | 38 | paginate({ 39 | methodName: 'findAndPaginate', 40 | primaryKeyField: 'id' 41 | })(Comment) 42 | -------------------------------------------------------------------------------- /microservices/comments-svc/src/comments/comments.interface.ts: -------------------------------------------------------------------------------- 1 | import { FindOptions } from 'sequelize/types' 2 | 3 | import { Comment } from './comment.model' 4 | import { CommentDto } from './comment.dto' 5 | import { IFindAndPaginateOptions, IFindAndPaginateResult } from '../commons/find-and-paginate.interface' 6 | 7 | export interface ICommentUpdateInput { 8 | id: string 9 | data: CommentDto 10 | } 11 | 12 | export interface ICommentsService { 13 | find(query?: IFindAndPaginateOptions): Promise> 14 | findById(id: string): Promise 15 | findOne(query?: FindOptions): Promise 16 | count(query?: FindOptions): Promise 17 | create(comment: CommentDto): Promise 18 | update(id: string, comment: CommentDto): Promise 19 | destroy(query?: FindOptions): Promise 20 | } 21 | -------------------------------------------------------------------------------- /microservices/comments-svc/src/comments/comments.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { LoggerModule } from 'nestjs-pino' 3 | import { SequelizeModule } from '@nestjs/sequelize' 4 | 5 | import { Comment } from './comment.model' 6 | import { CommentsController } from './comments.controller' 7 | import { CommentsService } from './comments.service' 8 | 9 | @Module({ 10 | imports: [LoggerModule, SequelizeModule.forFeature([Comment])], 11 | providers: [{ provide: 'CommentsService', useClass: CommentsService }], 12 | controllers: [CommentsController] 13 | }) 14 | export class CommentsModule {} 15 | -------------------------------------------------------------------------------- /microservices/comments-svc/src/comments/comments.service.ts: -------------------------------------------------------------------------------- 1 | import { isEmpty } from 'lodash' 2 | import { PinoLogger } from 'nestjs-pino' 3 | import { Injectable } from '@nestjs/common' 4 | import { FindOptions } from 'sequelize/types' 5 | import { InjectModel } from '@nestjs/sequelize' 6 | 7 | import { ICommentsService } from './comments.interface' 8 | import { IFindAndPaginateOptions, IFindAndPaginateResult } from '../commons/find-and-paginate.interface' 9 | 10 | import { Comment } from './comment.model' 11 | import { CommentDto } from './comment.dto' 12 | 13 | @Injectable() 14 | export class CommentsService implements ICommentsService { 15 | constructor( 16 | @InjectModel(Comment) 17 | private readonly repo: typeof Comment, 18 | private readonly logger: PinoLogger 19 | ) { 20 | logger.setContext(CommentsService.name) 21 | } 22 | 23 | async find(query?: IFindAndPaginateOptions): Promise> { 24 | this.logger.info('CommentsService#findAll.call %o', query) 25 | 26 | // @ts-ignore 27 | const result: IFindAndPaginateResult = await this.repo.findAndPaginate({ 28 | ...query, 29 | raw: true, 30 | paranoid: false 31 | }) 32 | 33 | this.logger.info('CommentsService#findAll.result %o', result) 34 | 35 | return result 36 | } 37 | 38 | async findById(id: string): Promise { 39 | this.logger.info('CommentsService#findById.call %o', id) 40 | 41 | const result: Comment = await this.repo.findByPk(id, { 42 | raw: true 43 | }) 44 | 45 | this.logger.info('CommentsService#findById.result %o', result) 46 | 47 | return result 48 | } 49 | 50 | async findOne(query: FindOptions): Promise { 51 | this.logger.info('CommentsService#findOne.call %o', query) 52 | 53 | const result: Comment = await this.repo.findOne({ 54 | ...query, 55 | raw: true 56 | }) 57 | 58 | this.logger.info('CommentsService#findOne.result %o', result) 59 | 60 | return result 61 | } 62 | 63 | async count(query?: FindOptions): Promise { 64 | this.logger.info('CommentsService#count.call %o', query) 65 | 66 | const result: number = await this.repo.count(query) 67 | 68 | this.logger.info('CommentsService#count.result %o', result) 69 | 70 | return result 71 | } 72 | 73 | async create(commentDto: CommentDto): Promise { 74 | this.logger.info('CommentsService#create.call %o', commentDto) 75 | 76 | const result: Comment = await this.repo.create(commentDto) 77 | 78 | this.logger.info('CommentsService#create.result %o', result) 79 | 80 | return result 81 | } 82 | 83 | async update(id: string, comment: CommentDto): Promise { 84 | this.logger.info('CommentsService#update.call %o', comment) 85 | 86 | const record: Comment = await this.repo.findByPk(id) 87 | 88 | if (isEmpty(record)) throw new Error('Record not found.') 89 | 90 | const result: Comment = await record.update(comment) 91 | 92 | this.logger.info('CommentsService#update.result %o', result) 93 | 94 | return result 95 | } 96 | 97 | async destroy(query?: FindOptions): Promise { 98 | this.logger.info('CommentsService#destroy.call %o', query) 99 | 100 | const result: number = await this.repo.destroy(query) 101 | 102 | this.logger.info('CommentsService#destroy.result %o', result) 103 | 104 | return result 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /microservices/comments-svc/src/commons/commons.interface.ts: -------------------------------------------------------------------------------- 1 | export interface Iid { 2 | id: string 3 | } 4 | 5 | export interface IQuery { 6 | select?: string[] 7 | where?: string 8 | orderBy?: string[] 9 | limit?: number 10 | before?: string 11 | after?: string 12 | } 13 | 14 | export interface ICount { 15 | count: number 16 | } 17 | -------------------------------------------------------------------------------- /microservices/comments-svc/src/commons/cursor-pagination.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IEdge { 2 | node: T 3 | cursor: string 4 | } 5 | 6 | export interface IPageInfo { 7 | startCursor: string 8 | endCursor: string 9 | hasNextPage: boolean 10 | hasPreviousPage: boolean 11 | } 12 | 13 | export interface IFindPayload { 14 | edges: IEdge[] 15 | pageInfo: IPageInfo 16 | } 17 | -------------------------------------------------------------------------------- /microservices/comments-svc/src/commons/find-and-paginate.interface.ts: -------------------------------------------------------------------------------- 1 | import { WhereOptions, FindAttributeOptions } from 'sequelize/types' 2 | 3 | export interface IFindAndPaginateOptions { 4 | attributes: FindAttributeOptions 5 | where: WhereOptions 6 | order: string[] 7 | limit: number 8 | before: string 9 | after: string 10 | } 11 | 12 | export interface ICursor { 13 | before: string 14 | after: string 15 | hasNext: boolean 16 | hasPrevious: boolean 17 | } 18 | 19 | export interface IFindAndPaginateResult { 20 | results: T[] 21 | cursors: ICursor 22 | } 23 | -------------------------------------------------------------------------------- /microservices/comments-svc/src/main.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path' 2 | 3 | import { NestFactory } from '@nestjs/core' 4 | import { Transport, MicroserviceOptions } from '@nestjs/microservices' 5 | import { Logger } from 'nestjs-pino' 6 | 7 | import { LoggerService, INestMicroservice } from '@nestjs/common' 8 | import { AppModule } from './app.module' 9 | 10 | async function main() { 11 | const app: INestMicroservice = await NestFactory.createMicroservice(AppModule, { 12 | transport: Transport.GRPC, 13 | options: { 14 | url: `${process.env.GRPC_HOST}:${process.env.GRPC_PORT}`, 15 | package: 'comment', 16 | protoPath: join(__dirname, './_proto/comment.proto'), 17 | loader: { 18 | keepCase: true, 19 | enums: String, 20 | oneofs: true, 21 | arrays: true 22 | } 23 | } 24 | }) 25 | 26 | app.useLogger(app.get(Logger)) 27 | 28 | return app.listenAsync() 29 | } 30 | 31 | main() 32 | -------------------------------------------------------------------------------- /microservices/comments-svc/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /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": "ES2019", 9 | "sourceMap": true, 10 | "outDir": "./dist", 11 | "baseUrl": "./", 12 | "incremental": true 13 | }, 14 | "exclude": ["node_modules", "dist", "__tests__", "**/*.spec.ts", "**/*.test.ts"] 15 | } 16 | -------------------------------------------------------------------------------- /microservices/mailer-svc/.env.example: -------------------------------------------------------------------------------- 1 | # Application Variables 2 | NODE_ENV= 3 | 4 | # GRPC Variables 5 | GRPC_HOST= 6 | GRPC_PORT= 7 | 8 | # SMTP Variables 9 | SMTP_HOST= 10 | SMTP_PORT= 11 | SMTP_SECURE= 12 | SMTP_USER= 13 | SMTP_PASS= 14 | -------------------------------------------------------------------------------- /microservices/mailer-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 | - "plugin:@typescript-eslint/eslint-recommended" 12 | - "plugin:@typescript-eslint/recommended" 13 | - "prettier" 14 | - "prettier/@typescript-eslint" 15 | - "plugin:prettier/recommended" 16 | - "plugin:jest/recommended" 17 | - "plugin:import/errors" 18 | - "plugin:import/warnings" 19 | - "plugin:import/typescript" 20 | root: true 21 | env: 22 | node: true 23 | jest: true 24 | rules: 25 | "@typescript-eslint/interface-name-prefix": off 26 | "@typescript-eslint/explicit-function-return-type": off 27 | "@typescript-eslint/no-explicit-any": off 28 | "@typescript-eslint/ban-ts-comment": 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: 300 42 | semi: false 43 | singleQuote: true 44 | endOfline: "lf" 45 | trailingComma: "none" 46 | 47 | 48 | -------------------------------------------------------------------------------- /microservices/mailer-svc/.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | cache: 3 | key: ${CI_PROJECT_NAME} 4 | paths: 5 | - node_modules/ 6 | 7 | variables: 8 | K8S_DEPLOYMENT_NAME: mailer-svc 9 | K8S_CONTAINER_NAME: mailer-svc 10 | K8S_DEV_NAMESPACE: dev 11 | K8S_STG_NAMESPACE: stg 12 | K8S_PROD_NAMESPACE: prod 13 | CONTAINER_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA 14 | 15 | stages: 16 | - setup 17 | - test 18 | - build 19 | - release 20 | - deploy 21 | 22 | setup: 23 | image: node:12-alpine 24 | stage: setup 25 | script: 26 | - npm install --prefer-offline &> /dev/null 27 | only: 28 | - merge_requests 29 | tags: 30 | - docker 31 | allow_failure: false 32 | when: always 33 | 34 | test: 35 | image: node:12-alpine 36 | stage: test 37 | dependencies: 38 | - setup 39 | script: 40 | - npm run lint 41 | - npm run test:coverage 42 | coverage: /All files[^|]*\|[^|]*\s+([\d\.]+)/ 43 | variables: 44 | NODE_ENV: "test" 45 | GRPC_HOST: "0.0.0.0" 46 | GRPC_PORT: "50051" 47 | SMTP_HOST: "smtp.gmail.com" 48 | SMTP_PORT: "587" 49 | SMTP_SECURE: "true" 50 | SMTP_USER: "username@gmail.com" 51 | SMTP_PASS: "password" 52 | only: 53 | - dev 54 | - merge_requests 55 | tags: 56 | - docker 57 | allow_failure: false 58 | when: always 59 | 60 | build: 61 | image: node:12-alpine 62 | stage: build 63 | dependencies: 64 | - test 65 | script: 66 | - npm run build 67 | only: 68 | - dev 69 | tags: 70 | - docker 71 | artifacts: 72 | paths: 73 | - dist/ 74 | allow_failure: false 75 | when: always 76 | 77 | release: 78 | image: docker:19.03.8 79 | stage: release 80 | dependencies: 81 | - build 82 | script: 83 | - docker build -t ${CONTAINER_IMAGE} . 84 | - docker tag ${CONTAINER_IMAGE} $CI_REGISTRY_IMAGE:dev 85 | - docker tag ${CONTAINER_IMAGE} $CI_REGISTRY_IMAGE:latest 86 | - docker login -u ${CI_REGISTRY_USER} -p $CI_REGISTRY_PASSWORD ${CI_REGISTRY} 87 | - docker push ${CONTAINER_IMAGE} 88 | - docker push $CI_REGISTRY_IMAGE:dev 89 | - docker push $CI_REGISTRY_IMAGE:latest 90 | only: 91 | - dev 92 | tags: 93 | - docker 94 | allow_failure: false 95 | when: always 96 | 97 | deploy-dev: 98 | image: docker:19.03.8 99 | stage: deploy 100 | dependencies: 101 | - release 102 | script: 103 | - mkdir -p /builds/${CI_PROJECT_PATH} 104 | - echo "${KUBE_CONFIG}" | base64 -d > /builds/${CI_PROJECT_PATH}/config 105 | - docker run --rm -v /builds/${CI_PROJECT_PATH}/config:/.kube/config bitnami/kubectl:1.17.4 set image deployment/${K8S_DEPLOYMENT_NAME} ${K8S_CONTAINER_NAME}=${CONTAINER_IMAGE} -n ${K8S_DEV_NAMESPACE} 106 | only: 107 | - dev 108 | tags: 109 | - docker 110 | allow_failure: true 111 | when: always 112 | 113 | deploy-stg: 114 | image: docker:19.03.8 115 | stage: deploy 116 | dependencies: 117 | - release 118 | script: 119 | - mkdir -p /builds/${CI_PROJECT_PATH} 120 | - echo "${KUBE_CONFIG}" | base64 -d > /builds/${CI_PROJECT_PATH}/config 121 | - docker run --rm -v /builds/${CI_PROJECT_PATH}/config:/.kube/config bitnami/kubectl:1.17.4 set image deployment/${K8S_DEPLOYMENT_NAME} ${K8S_CONTAINER_NAME}=${CONTAINER_IMAGE} -n ${K8S_STG_NAMESPACE} 122 | only: 123 | - dev 124 | tags: 125 | - docker 126 | allow_failure: true 127 | when: manual 128 | 129 | deploy-prod: 130 | image: docker:19.03.8 131 | stage: deploy 132 | dependencies: 133 | - release 134 | script: 135 | - mkdir -p /builds/${CI_PROJECT_PATH} 136 | - echo "${KUBE_CONFIG}" | base64 -d > /builds/${CI_PROJECT_PATH}/config 137 | - docker run --rm -v /builds/${CI_PROJECT_PATH}/config:/.kube/config bitnami/kubectl:1.17.4 set image deployment/${K8S_DEPLOYMENT_NAME} ${K8S_CONTAINER_NAME}=${CONTAINER_IMAGE} -n ${K8S_PROD_NAMESPACE} 138 | only: 139 | - dev 140 | tags: 141 | - docker 142 | allow_failure: true 143 | when: manual 144 | -------------------------------------------------------------------------------- /microservices/mailer-svc/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12-alpine as build 2 | 3 | WORKDIR /usr/share/mailer-svc 4 | 5 | ADD dist package.json ./ 6 | 7 | RUN npm install --production 8 | 9 | FROM node:12-alpine 10 | 11 | ADD https://github.com/grpc-ecosystem/grpc-health-probe/releases/download/v0.3.2/grpc_health_probe-linux-amd64 /bin/grpc_health_probe 12 | 13 | RUN chmod +x /bin/grpc_health_probe 14 | 15 | WORKDIR /usr/share/mailer-svc 16 | 17 | COPY --from=build /usr/share/mailer-svc . 18 | 19 | EXPOSE 50051 20 | 21 | CMD ["node", "main.js"] 22 | -------------------------------------------------------------------------------- /microservices/mailer-svc/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | 5 | mailer-svc: 6 | build: 7 | context: "." 8 | networks: 9 | - "mailer-svc" 10 | ports: 11 | - "50051:50051" 12 | depends_on: 13 | - "db" 14 | - "cache" 15 | environment: 16 | NODE_ENV: "test" 17 | GRPC_HOST: "0.0.0.0" 18 | GRPC_PORT: "50051" 19 | SMTP_HOST: "smtp.gmail.com" 20 | SMTP_PORT: "587" 21 | SMTP_SECURE: "true" 22 | SMTP_USER: "username@gmail.com" 23 | SMTP_PASS: "password" 24 | healthcheck: 25 | test: ["CMD", "/bin/grpc_health_probe", "-addr=:50051"] 26 | interval: 30s 27 | timeout: 10s 28 | retries: 5 29 | restart: "on-failure" 30 | 31 | networks: 32 | mailer-svc: 33 | -------------------------------------------------------------------------------- /microservices/mailer-svc/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | setupFilesAfterEnv: [ 3 | "jest-extended" 4 | ], 5 | coverageThreshold: { 6 | global: { 7 | branches: 90, 8 | functions: 90, 9 | lines: 90, 10 | statements: 90 11 | } 12 | }, 13 | collectCoverageFrom: [ 14 | "src/**/*.ts", 15 | "!**/node_modules/**" 16 | ], 17 | "rootDir": "src", 18 | "preset": "ts-jest", 19 | testEnvironment: "node" 20 | } 21 | -------------------------------------------------------------------------------- /microservices/mailer-svc/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src", 4 | "compilerOptions": { 5 | "assets": ["**/*.proto"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /microservices/mailer-svc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mailer-svc", 3 | "version": "1.0.0", 4 | "description": "gRPC microservice back-end for mailer. 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 | "lint": "eslint --max-warnings=0 '{src,__tests__}/**/*.ts'", 12 | "lint:fix": "eslint --fix '{src,__tests__}/**/*.ts'", 13 | "test": "jest", 14 | "test:watch": "jest --watch", 15 | "test:coverage": "jest --coverage", 16 | "copy:protos": "cpy ../../_proto ./src/_proto" 17 | }, 18 | "dependencies": { 19 | "@grpc/proto-loader": "0.5.4", 20 | "@nestjs-modules/mailer": "1.5.0", 21 | "@nestjs/common": "7.1.1", 22 | "@nestjs/config": "0.5.0", 23 | "@nestjs/core": "7.1.1", 24 | "@nestjs/microservices": "7.1.1", 25 | "grpc": "1.24.2", 26 | "handlebars": "4.7.6", 27 | "lodash": "4.17.15", 28 | "nestjs-pino": "1.2.0", 29 | "nodemailer": "6.4.8", 30 | "pino": "6.3.0", 31 | "reflect-metadata": "0.1.13" 32 | }, 33 | "devDependencies": { 34 | "@nestjs/cli": "7.2.0", 35 | "@nestjs/schematics": "7.0.0", 36 | "@nestjs/testing": "7.1.1", 37 | "@types/faker": "4.1.12", 38 | "@types/jest": "25.2.3", 39 | "@types/lodash": "4.14.153", 40 | "@types/node": "14.0.5", 41 | "@typescript-eslint/eslint-plugin": "3.0.2", 42 | "@typescript-eslint/parser": "3.0.2", 43 | "cpy-cli": "3.1.1", 44 | "eslint": "7.1.0", 45 | "eslint-config-prettier": "6.11.0", 46 | "eslint-plugin-import": "2.20.2", 47 | "eslint-plugin-jest": "23.13.2", 48 | "eslint-plugin-prettier": "3.1.3", 49 | "faker": "4.1.0", 50 | "jest": "26.0.1", 51 | "jest-extended": "0.11.5", 52 | "pino-pretty": "4.0.0", 53 | "prettier": "2.0.5", 54 | "rimraf": "3.0.2", 55 | "ts-jest": "26.0.0", 56 | "typescript": "3.9.3" 57 | }, 58 | "repository": { 59 | "type": "git", 60 | "url": "git+ssh://git@github.com:benjsicam/nestjs-graphql-microservices.git" 61 | }, 62 | "author": "Benj Sicam (https://github.com/benjsicam)", 63 | "license": "MIT", 64 | "bugs": { 65 | "url": "https://github.com/benjsicam/nestjs-graphql-microservices/issues" 66 | }, 67 | "homepage": "https://github.com/benjsicam/nestjs-graphql-microservices#readme" 68 | } 69 | -------------------------------------------------------------------------------- /microservices/mailer-svc/src/_proto/README.md: -------------------------------------------------------------------------------- 1 | ## Note on Maintenence 2 | 3 | Maintenance of these files should be done under the _proto folder on the root of the project. Use `npm run copy:protos` to copy updates to this directory. -------------------------------------------------------------------------------- /microservices/mailer-svc/src/_proto/comment.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package comment; 4 | 5 | import "commons.proto"; 6 | 7 | message Comment { 8 | string id = 1; 9 | string text = 2; 10 | string author = 3; 11 | string post = 4; 12 | string createdAt = 5; 13 | string updatedAt = 6; 14 | int32 version = 7; 15 | } 16 | 17 | message CommentEdge { 18 | Comment node = 1; 19 | string cursor = 2; 20 | } 21 | 22 | message CreateCommentInput { 23 | string text = 1; 24 | string author = 2; 25 | string post = 3; 26 | } 27 | 28 | message UpdateCommentInput { 29 | string id = 1; 30 | Comment data = 2; 31 | } 32 | 33 | message FindCommentsPayload { 34 | repeated CommentEdge edges = 1; 35 | commons.PageInfo pageInfo = 2; 36 | } 37 | 38 | service CommentsService { 39 | rpc find (commons.Query) returns (FindCommentsPayload) {} 40 | rpc findById (commons.Id) returns (Comment) {} 41 | rpc findOne (commons.Query) returns (Comment) {} 42 | rpc count (commons.Query) returns (commons.Count) {} 43 | rpc create (CreateCommentInput) returns (Comment) {} 44 | rpc update (UpdateCommentInput) returns (Comment) {} 45 | rpc destroy (commons.Query) returns (commons.Count) {} 46 | } 47 | -------------------------------------------------------------------------------- /microservices/mailer-svc/src/_proto/commons.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package commons; 4 | 5 | message Id { 6 | string id = 1; 7 | } 8 | 9 | message Query { 10 | repeated string select = 1; 11 | string where = 2; 12 | repeated string orderBy = 3; 13 | int32 limit = 4; 14 | string before = 5; 15 | string after = 6; 16 | } 17 | 18 | message PageInfo { 19 | string startCursor = 1; 20 | string endCursor = 2; 21 | bool hasNextPage = 3; 22 | bool hasPreviousPage = 4; 23 | } 24 | 25 | message Count { 26 | int32 count = 1; 27 | } 28 | -------------------------------------------------------------------------------- /microservices/mailer-svc/src/_proto/mailer.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package mailer; 4 | 5 | message SendMailInput { 6 | string template = 1; 7 | string to = 2; 8 | bytes data = 3; 9 | } 10 | 11 | message SendMailPayload { 12 | bool isSent = 1; 13 | } 14 | 15 | service MailerService { 16 | rpc send (SendMailInput) returns (SendMailPayload) {} 17 | } 18 | -------------------------------------------------------------------------------- /microservices/mailer-svc/src/_proto/post.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package post; 4 | 5 | import "commons.proto"; 6 | 7 | message Post { 8 | string id = 1; 9 | string title = 2; 10 | string body = 3; 11 | bool published = 4; 12 | string author = 5; 13 | string createdAt = 6; 14 | string updatedAt = 7; 15 | int32 version = 8; 16 | } 17 | 18 | message PostEdge { 19 | Post node = 1; 20 | string cursor = 2; 21 | } 22 | 23 | message CreatePostInput { 24 | string title = 2; 25 | string body = 3; 26 | bool published = 4; 27 | string author = 5; 28 | } 29 | 30 | message UpdatePostInput { 31 | string id = 1; 32 | Post data = 2; 33 | } 34 | 35 | message FindPostsPayload { 36 | repeated PostEdge edges = 1; 37 | commons.PageInfo pageInfo = 2; 38 | } 39 | 40 | service PostsService { 41 | rpc find (commons.Query) returns (FindPostsPayload) {} 42 | rpc findById (commons.Id) returns (Post) {} 43 | rpc findOne (commons.Query) returns (Post) {} 44 | rpc count (commons.Query) returns (commons.Count) {} 45 | rpc create (CreatePostInput) returns (Post) {} 46 | rpc update (UpdatePostInput) returns (Post) {} 47 | rpc destroy (commons.Query) returns (commons.Count) {} 48 | } 49 | -------------------------------------------------------------------------------- /microservices/mailer-svc/src/_proto/user.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package user; 4 | 5 | import "commons.proto"; 6 | 7 | message User { 8 | string id = 1; 9 | string name = 2; 10 | string email = 3; 11 | string password = 4; 12 | int32 age = 5; 13 | string createdAt = 6; 14 | string updatedAt = 7; 15 | int32 version = 8; 16 | } 17 | 18 | message UserEdge { 19 | User node = 1; 20 | string cursor = 2; 21 | } 22 | 23 | message CreateUserInput { 24 | string name = 1; 25 | string email = 2; 26 | string password = 3; 27 | int32 age = 4; 28 | } 29 | 30 | message UpdateUserInput { 31 | string id = 1; 32 | User data = 2; 33 | } 34 | 35 | message FindUsersPayload { 36 | repeated UserEdge edges = 1; 37 | commons.PageInfo pageInfo = 2; 38 | } 39 | 40 | service UsersService { 41 | rpc find (commons.Query) returns (FindUsersPayload) {} 42 | rpc findById (commons.Id) returns (User) {} 43 | rpc findOne (commons.Query) returns (User) {} 44 | rpc count (commons.Query) returns (commons.Count) {} 45 | rpc create (CreateUserInput) returns (User) {} 46 | rpc update (UpdateUserInput) returns (User) {} 47 | rpc destroy (commons.Query) returns (commons.Count) {} 48 | } 49 | -------------------------------------------------------------------------------- /microservices/mailer-svc/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { ConfigModule, ConfigService } from '@nestjs/config' 3 | import { MailerModule as MailModule } from '@nestjs-modules/mailer' 4 | import { PugAdapter } from '@nestjs-modules/mailer/dist/adapters/pug.adapter' 5 | 6 | import { LoggerModule } from 'nestjs-pino' 7 | 8 | import { MailerModule } from './mailer/mailer.module' 9 | 10 | @Module({ 11 | imports: [ 12 | ConfigModule.forRoot(), 13 | LoggerModule.forRootAsync({ 14 | imports: [ConfigModule], 15 | useFactory: async (configService: ConfigService) => ({ 16 | pinoHttp: { 17 | safe: true, 18 | prettyPrint: configService.get('NODE_ENV') !== 'production' 19 | } 20 | }), 21 | inject: [ConfigService] 22 | }), 23 | MailModule.forRootAsync({ 24 | imports: [ConfigModule], 25 | useFactory: async (configService: ConfigService) => ({ 26 | transport: { 27 | host: configService.get('SMTP_HOST'), 28 | port: configService.get('SMTP_PORT'), 29 | secure: configService.get('SMTP_SECURE'), 30 | auth: { 31 | user: configService.get('SMTP_USER'), 32 | pass: configService.get('SMTP_PASS') 33 | }, 34 | preview: configService.get('NODE_ENV') !== 'production', 35 | defaults: { 36 | from: '"No Reply" ' 37 | }, 38 | template: { 39 | dir: __dirname + '/templates', 40 | adapter: new PugAdapter(), 41 | options: { 42 | strict: true 43 | } 44 | } 45 | } 46 | }), 47 | inject: [ConfigService] 48 | }), 49 | MailerModule 50 | ] 51 | }) 52 | export class AppModule {} 53 | -------------------------------------------------------------------------------- /microservices/mailer-svc/src/mailer/mailer.controller.ts: -------------------------------------------------------------------------------- 1 | import { PinoLogger } from 'nestjs-pino' 2 | import { Controller } from '@nestjs/common' 3 | import { GrpcMethod } from '@nestjs/microservices' 4 | import { MailerService } from '@nestjs-modules/mailer' 5 | 6 | import { ISendMailInput, ISendMailPayload } from './mailer.interface' 7 | 8 | @Controller() 9 | export class MailerController { 10 | constructor(private readonly service: MailerService, private readonly logger: PinoLogger) { 11 | logger.setContext(MailerController.name) 12 | } 13 | 14 | @GrpcMethod('MailerService', 'send') 15 | async send(input: ISendMailInput): Promise { 16 | const mailInput = { 17 | ...input, 18 | data: JSON.parse(Buffer.from(input.data).toString()) 19 | } 20 | 21 | this.logger.info('MailerController#send.call %o', mailInput) 22 | 23 | let subject = '' 24 | 25 | switch (mailInput.template) { 26 | case 'new-comment': 27 | subject = 'Notice: New Comment on your Post' 28 | break 29 | case 'signup': 30 | subject = 'Welcome to GraphQL Blog' 31 | break 32 | case 'update-email': 33 | subject = 'Notice: Email Update' 34 | break 35 | case 'update-password': 36 | subject = 'Notice: Password Update' 37 | break 38 | default: 39 | break 40 | } 41 | 42 | await this.service.sendMail({ 43 | to: mailInput.to, 44 | subject, 45 | template: mailInput.template, 46 | context: mailInput.data 47 | }) 48 | 49 | this.logger.info('MailerController#sent') 50 | 51 | return { isSent: true } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /microservices/mailer-svc/src/mailer/mailer.interface.ts: -------------------------------------------------------------------------------- 1 | export interface ISendMailInput { 2 | template: string 3 | to: string 4 | data: Buffer 5 | } 6 | 7 | export interface ISendMailPayload { 8 | isSent: boolean 9 | } 10 | -------------------------------------------------------------------------------- /microservices/mailer-svc/src/mailer/mailer.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { LoggerModule } from 'nestjs-pino' 3 | import { MailerModule as MailModule } from '@nestjs-modules/mailer' 4 | 5 | import { MailerController } from './mailer.controller' 6 | 7 | @Module({ 8 | imports: [LoggerModule, MailModule], 9 | controllers: [MailerController] 10 | }) 11 | export class MailerModule {} 12 | -------------------------------------------------------------------------------- /microservices/mailer-svc/src/main.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path' 2 | 3 | import { NestFactory } from '@nestjs/core' 4 | import { Transport, MicroserviceOptions } from '@nestjs/microservices' 5 | import { Logger } from 'nestjs-pino' 6 | 7 | import { LoggerService, INestMicroservice } from '@nestjs/common' 8 | import { AppModule } from './app.module' 9 | 10 | async function main() { 11 | const app: INestMicroservice = await NestFactory.createMicroservice(AppModule, { 12 | transport: Transport.GRPC, 13 | options: { 14 | url: `${process.env.GRPC_HOST}:${process.env.GRPC_PORT}`, 15 | package: 'mailer', 16 | protoPath: join(__dirname, './_proto/mailer.proto'), 17 | loader: { 18 | keepCase: true, 19 | enums: String, 20 | oneofs: true, 21 | arrays: true 22 | } 23 | } 24 | }) 25 | 26 | app.useLogger(app.get(Logger)) 27 | 28 | return app.listenAsync() 29 | } 30 | 31 | main() 32 | -------------------------------------------------------------------------------- /microservices/mailer-svc/src/templates/new-comment.pug: -------------------------------------------------------------------------------- 1 | p Hi #{data.postAuthor}, 2 | p #{data.commentAuthor} commented: "#{data.comment}" to your post: #{data.post} 3 | -------------------------------------------------------------------------------- /microservices/mailer-svc/src/templates/signup.pug: -------------------------------------------------------------------------------- 1 | p Hi #{data.name}, 2 | p Welcome to GraphQL Blog 3 | -------------------------------------------------------------------------------- /microservices/mailer-svc/src/templates/update-email.pug: -------------------------------------------------------------------------------- 1 | p Hi #{data.name}, 2 | p Congratulation! Your email has been updated successfully. 3 | -------------------------------------------------------------------------------- /microservices/mailer-svc/src/templates/update-password.pug: -------------------------------------------------------------------------------- 1 | p Hi #{data.name}, 2 | p Congratulation! You have updated your password successfully. 3 | -------------------------------------------------------------------------------- /microservices/mailer-svc/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /microservices/mailer-svc/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "target": "ES2019", 9 | "sourceMap": true, 10 | "outDir": "./dist", 11 | "baseUrl": "./", 12 | "incremental": true 13 | }, 14 | "exclude": ["node_modules", "dist", "__tests__", "**/*.spec.ts", "**/*.test.ts"] 15 | } 16 | -------------------------------------------------------------------------------- /microservices/posts-svc/.env.example: -------------------------------------------------------------------------------- 1 | # Environment 2 | NODE_ENV= 3 | 4 | # gRPC Server Options 5 | GRPC_HOST= 6 | GRPC_PORT= 7 | 8 | # DB Options 9 | DB_HOST= 10 | DB_PORT= 11 | DB_USERNAME= 12 | DB_PASSWORD= 13 | DB_DATABASE= 14 | DB_SCHEMA= 15 | DB_SYNC= 16 | 17 | # Cache Options 18 | REDIS_HOST= 19 | REDIS_PORT= 20 | -------------------------------------------------------------------------------- /microservices/posts-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 | - "plugin:@typescript-eslint/eslint-recommended" 12 | - "plugin:@typescript-eslint/recommended" 13 | - "prettier" 14 | - "prettier/@typescript-eslint" 15 | - "plugin:prettier/recommended" 16 | - "plugin:jest/recommended" 17 | - "plugin:import/errors" 18 | - "plugin:import/warnings" 19 | - "plugin:import/typescript" 20 | root: true 21 | env: 22 | node: true 23 | jest: true 24 | rules: 25 | "@typescript-eslint/interface-name-prefix": off 26 | "@typescript-eslint/explicit-function-return-type": off 27 | "@typescript-eslint/no-explicit-any": off 28 | "@typescript-eslint/ban-ts-comment": 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: 300 42 | semi: false 43 | singleQuote: true 44 | endOfline: "lf" 45 | trailingComma: "none" 46 | 47 | 48 | -------------------------------------------------------------------------------- /microservices/posts-svc/.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | cache: 3 | key: ${CI_PROJECT_NAME} 4 | paths: 5 | - node_modules/ 6 | 7 | variables: 8 | K8S_DEPLOYMENT_NAME: posts-svc 9 | K8S_CONTAINER_NAME: posts-svc 10 | K8S_DEV_NAMESPACE: dev 11 | K8S_STG_NAMESPACE: stg 12 | K8S_PROD_NAMESPACE: prod 13 | CONTAINER_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA 14 | 15 | stages: 16 | - setup 17 | - test 18 | - build 19 | - release 20 | - deploy 21 | 22 | setup: 23 | image: node:12-alpine 24 | stage: setup 25 | before_script: 26 | - apk add --no-cache make g++ python postgresql-dev &> /dev/null 27 | - rm -rf ./node_modules/.cache 28 | script: 29 | - npm install --prefer-offline &> /dev/null 30 | only: 31 | - merge_requests 32 | tags: 33 | - docker 34 | allow_failure: false 35 | when: always 36 | 37 | test: 38 | image: node:12-alpine 39 | stage: test 40 | dependencies: 41 | - setup 42 | before_script: 43 | - apk add --no-cache libpq 44 | script: 45 | - npm run lint 46 | - npm run test:coverage 47 | coverage: /All files[^|]*\|[^|]*\s+([\d\.]+)/ 48 | services: 49 | - name: postgres:12-alpine 50 | alias: db 51 | - name: redis:5-alpine 52 | alias: cache 53 | variables: 54 | NODE_ENV: "test" 55 | GRPC_HOST: "0.0.0.0" 56 | GRPC_PORT: "50051" 57 | DB_HOST: "db" 58 | DB_PORT: "5432" 59 | DB_USERNAME: "postgres" 60 | DB_PASSWORD: "postgres" 61 | DB_DATABASE: "postgres" 62 | DB_SCHEMA: "public" 63 | REDIS_HOST: "cache" 64 | REDIS_PORT: "6379" 65 | POSTGRES_USER: "postgres" 66 | POSTGRES_PASSWORD: "postgres" 67 | only: 68 | - dev 69 | - merge_requests 70 | tags: 71 | - docker 72 | allow_failure: false 73 | when: always 74 | 75 | build: 76 | image: node:12-alpine 77 | stage: build 78 | dependencies: 79 | - test 80 | script: 81 | - npm run build 82 | only: 83 | - dev 84 | tags: 85 | - docker 86 | artifacts: 87 | paths: 88 | - dist/ 89 | allow_failure: false 90 | when: always 91 | 92 | release: 93 | image: docker:19.03.8 94 | stage: release 95 | dependencies: 96 | - build 97 | script: 98 | - docker build -t ${CONTAINER_IMAGE} . 99 | - docker tag ${CONTAINER_IMAGE} $CI_REGISTRY_IMAGE:dev 100 | - docker tag ${CONTAINER_IMAGE} $CI_REGISTRY_IMAGE:latest 101 | - docker login -u ${CI_REGISTRY_USER} -p $CI_REGISTRY_PASSWORD ${CI_REGISTRY} 102 | - docker push ${CONTAINER_IMAGE} 103 | - docker push $CI_REGISTRY_IMAGE:dev 104 | - docker push $CI_REGISTRY_IMAGE:latest 105 | only: 106 | - dev 107 | tags: 108 | - docker 109 | allow_failure: false 110 | when: always 111 | 112 | deploy-dev: 113 | image: docker:19.03.8 114 | stage: deploy 115 | dependencies: 116 | - release 117 | script: 118 | - mkdir -p /builds/${CI_PROJECT_PATH} 119 | - echo "${KUBE_CONFIG}" | base64 -d > /builds/${CI_PROJECT_PATH}/config 120 | - docker run --rm -v /builds/${CI_PROJECT_PATH}/config:/.kube/config bitnami/kubectl:1.17.4 set image deployment/${K8S_DEPLOYMENT_NAME} ${K8S_CONTAINER_NAME}=${CONTAINER_IMAGE} -n ${K8S_DEV_NAMESPACE} 121 | only: 122 | - dev 123 | tags: 124 | - docker 125 | allow_failure: true 126 | when: always 127 | 128 | deploy-stg: 129 | image: docker:19.03.8 130 | stage: deploy 131 | dependencies: 132 | - release 133 | script: 134 | - mkdir -p /builds/${CI_PROJECT_PATH} 135 | - echo "${KUBE_CONFIG}" | base64 -d > /builds/${CI_PROJECT_PATH}/config 136 | - docker run --rm -v /builds/${CI_PROJECT_PATH}/config:/.kube/config bitnami/kubectl:1.17.4 set image deployment/${K8S_DEPLOYMENT_NAME} ${K8S_CONTAINER_NAME}=${CONTAINER_IMAGE} -n ${K8S_STG_NAMESPACE} 137 | only: 138 | - dev 139 | tags: 140 | - docker 141 | allow_failure: true 142 | when: manual 143 | 144 | deploy-prod: 145 | image: docker:19.03.8 146 | stage: deploy 147 | dependencies: 148 | - release 149 | script: 150 | - mkdir -p /builds/${CI_PROJECT_PATH} 151 | - echo "${KUBE_CONFIG}" | base64 -d > /builds/${CI_PROJECT_PATH}/config 152 | - docker run --rm -v /builds/${CI_PROJECT_PATH}/config:/.kube/config bitnami/kubectl:1.17.4 set image deployment/${K8S_DEPLOYMENT_NAME} ${K8S_CONTAINER_NAME}=${CONTAINER_IMAGE} -n ${K8S_PROD_NAMESPACE} 153 | only: 154 | - dev 155 | tags: 156 | - docker 157 | allow_failure: true 158 | when: manual 159 | -------------------------------------------------------------------------------- /microservices/posts-svc/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12-alpine as build 2 | 3 | WORKDIR /usr/share/posts-svc 4 | 5 | ADD dist package.json ./ 6 | 7 | RUN apk add --no-cache make g++ python postgresql-dev \ 8 | && npm install --production 9 | 10 | FROM node:12-alpine 11 | 12 | RUN apk add --no-cache libpq 13 | 14 | ADD https://github.com/grpc-ecosystem/grpc-health-probe/releases/download/v0.3.2/grpc_health_probe-linux-amd64 /bin/grpc_health_probe 15 | 16 | RUN chmod +x /bin/grpc_health_probe 17 | 18 | WORKDIR /usr/share/posts-svc 19 | 20 | COPY --from=build /usr/share/posts-svc . 21 | 22 | EXPOSE 50051 23 | 24 | CMD ["node", "main.js"] 25 | -------------------------------------------------------------------------------- /microservices/posts-svc/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | 5 | posts-svc: 6 | build: 7 | context: "." 8 | networks: 9 | - "posts-svc" 10 | ports: 11 | - "50051:50051" 12 | depends_on: 13 | - "db" 14 | - "cache" 15 | environment: 16 | NODE_ENV: "test" 17 | GRPC_HOST: "0.0.0.0" 18 | GRPC_PORT: "50051" 19 | DB_HOST: "db" 20 | DB_PORT: "5432" 21 | DB_USERNAME: "postgres" 22 | DB_PASSWORD: "postgres" 23 | DB_DATABASE: "postgres" 24 | DB_SCHEMA: "public" 25 | DB_SYNC: true 26 | REDIS_HOST: "cache" 27 | REDIS_PORT: "6379" 28 | healthcheck: 29 | test: ["CMD", "/bin/grpc_health_probe", "-addr=:50051"] 30 | interval: 30s 31 | timeout: 10s 32 | retries: 5 33 | restart: "on-failure" 34 | 35 | db: 36 | image: "postgres:12-alpine" 37 | networks: 38 | - "posts-svc" 39 | expose: 40 | - "5432" 41 | environment: 42 | POSTGRES_USER: "postgres" 43 | POSTGRES_PASSWORD: "postgres" 44 | healthcheck: 45 | test: ["CMD-SHELL", "sh -c 'pg_isready -U postgres'"] 46 | interval: 30s 47 | timeout: 30s 48 | retries: 3 49 | restart: "on-failure" 50 | 51 | cache: 52 | image: "redis:5-alpine" 53 | networks: 54 | - "posts-svc" 55 | expose: 56 | - "6379" 57 | healthcheck: 58 | test: ["CMD-SHELL", "sh -c 'redis-cli PING'"] 59 | interval: 30s 60 | timeout: 30s 61 | retries: 3 62 | restart: "on-failure" 63 | 64 | networks: 65 | posts-svc: 66 | -------------------------------------------------------------------------------- /microservices/posts-svc/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | setupFilesAfterEnv: [ 3 | "jest-extended" 4 | ], 5 | coverageThreshold: { 6 | global: { 7 | branches: 90, 8 | functions: 90, 9 | lines: 90, 10 | statements: 90 11 | } 12 | }, 13 | collectCoverageFrom: [ 14 | "src/**/*.ts", 15 | "!**/node_modules/**" 16 | ], 17 | "rootDir": "src", 18 | "preset": "ts-jest", 19 | testEnvironment: "node" 20 | } 21 | -------------------------------------------------------------------------------- /microservices/posts-svc/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src", 4 | "compilerOptions": { 5 | "assets": ["**/*.proto"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /microservices/posts-svc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "posts-svc", 3 | "version": "1.0.0", 4 | "description": "gRPC microservice back-end for posts. 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 | "lint": "eslint --max-warnings=0 '{src,__tests__}/**/*.ts'", 12 | "lint:fix": "eslint --fix '{src,__tests__}/**/*.ts'", 13 | "test": "jest", 14 | "test:watch": "jest --watch", 15 | "test:coverage": "jest --coverage", 16 | "copy:protos": "cpy ../../_proto ./src/_proto" 17 | }, 18 | "dependencies": { 19 | "@grpc/proto-loader": "0.5.4", 20 | "@nestjs/common": "7.1.1", 21 | "@nestjs/config": "0.5.0", 22 | "@nestjs/core": "7.1.1", 23 | "@nestjs/microservices": "7.1.1", 24 | "@nestjs/sequelize": "0.1.0", 25 | "aigle": "1.14.1", 26 | "grpc": "1.24.2", 27 | "lodash": "4.17.15", 28 | "nestjs-pino": "1.2.0", 29 | "pg": "8.2.1", 30 | "pg-hstore": "2.3.3", 31 | "pg-native": "3.0.0", 32 | "pino": "6.3.0", 33 | "reflect-metadata": "0.1.13", 34 | "sequelize": "5.21.11", 35 | "sequelize-cursor-pagination": "1.7.0", 36 | "sequelize-typescript": "1.1.0" 37 | }, 38 | "devDependencies": { 39 | "@nestjs/cli": "7.2.0", 40 | "@nestjs/schematics": "7.0.0", 41 | "@nestjs/testing": "7.1.1", 42 | "@types/faker": "4.1.12", 43 | "@types/jest": "25.2.3", 44 | "@types/lodash": "4.14.153", 45 | "@types/node": "14.0.5", 46 | "@types/sequelize": "4.28.9", 47 | "@typescript-eslint/eslint-plugin": "3.0.2", 48 | "@typescript-eslint/parser": "3.0.2", 49 | "cpy-cli": "3.1.1", 50 | "eslint": "7.1.0", 51 | "eslint-config-prettier": "6.11.0", 52 | "eslint-plugin-import": "2.20.2", 53 | "eslint-plugin-jest": "23.13.2", 54 | "eslint-plugin-prettier": "3.1.3", 55 | "faker": "4.1.0", 56 | "jest": "26.0.1", 57 | "jest-extended": "0.11.5", 58 | "pino-pretty": "4.0.0", 59 | "prettier": "2.0.5", 60 | "rimraf": "3.0.2", 61 | "ts-jest": "26.0.0", 62 | "typescript": "3.9.3" 63 | }, 64 | "repository": { 65 | "type": "git", 66 | "url": "git+ssh://git@github.com:benjsicam/nestjs-graphql-microservices.git" 67 | }, 68 | "author": "Benj Sicam (https://github.com/benjsicam)", 69 | "license": "MIT", 70 | "bugs": { 71 | "url": "https://github.com/benjsicam/nestjs-graphql-microservices/issues" 72 | }, 73 | "homepage": "https://github.com/benjsicam/nestjs-graphql-microservices#readme" 74 | } 75 | -------------------------------------------------------------------------------- /microservices/posts-svc/src/_proto/README.md: -------------------------------------------------------------------------------- 1 | ## Note on Maintenence 2 | 3 | Maintenance of these files should be done under the _proto folder on the root of the project. Use `npm run copy:protos` to copy updates to this directory. -------------------------------------------------------------------------------- /microservices/posts-svc/src/_proto/comment.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package comment; 4 | 5 | import "commons.proto"; 6 | 7 | message Comment { 8 | string id = 1; 9 | string text = 2; 10 | string author = 3; 11 | string post = 4; 12 | string createdAt = 5; 13 | string updatedAt = 6; 14 | int32 version = 7; 15 | } 16 | 17 | message CommentEdge { 18 | Comment node = 1; 19 | string cursor = 2; 20 | } 21 | 22 | message CreateCommentInput { 23 | string text = 1; 24 | string author = 2; 25 | string post = 3; 26 | } 27 | 28 | message UpdateCommentInput { 29 | string id = 1; 30 | Comment data = 2; 31 | } 32 | 33 | message FindCommentsPayload { 34 | repeated CommentEdge edges = 1; 35 | commons.PageInfo pageInfo = 2; 36 | } 37 | 38 | service CommentsService { 39 | rpc find (commons.Query) returns (FindCommentsPayload) {} 40 | rpc findById (commons.Id) returns (Comment) {} 41 | rpc findOne (commons.Query) returns (Comment) {} 42 | rpc count (commons.Query) returns (commons.Count) {} 43 | rpc create (CreateCommentInput) returns (Comment) {} 44 | rpc update (UpdateCommentInput) returns (Comment) {} 45 | rpc destroy (commons.Query) returns (commons.Count) {} 46 | } 47 | -------------------------------------------------------------------------------- /microservices/posts-svc/src/_proto/commons.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package commons; 4 | 5 | message Id { 6 | string id = 1; 7 | } 8 | 9 | message Query { 10 | repeated string select = 1; 11 | string where = 2; 12 | repeated string orderBy = 3; 13 | int32 limit = 4; 14 | string before = 5; 15 | string after = 6; 16 | } 17 | 18 | message PageInfo { 19 | string startCursor = 1; 20 | string endCursor = 2; 21 | bool hasNextPage = 3; 22 | bool hasPreviousPage = 4; 23 | } 24 | 25 | message Count { 26 | int32 count = 1; 27 | } 28 | -------------------------------------------------------------------------------- /microservices/posts-svc/src/_proto/mailer.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package mailer; 4 | 5 | message SendMailInput { 6 | string template = 1; 7 | string to = 2; 8 | bytes data = 3; 9 | } 10 | 11 | message SendMailPayload { 12 | bool isSent = 1; 13 | } 14 | 15 | service MailerService { 16 | rpc send (SendMailInput) returns (SendMailPayload) {} 17 | } 18 | -------------------------------------------------------------------------------- /microservices/posts-svc/src/_proto/post.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package post; 4 | 5 | import "commons.proto"; 6 | 7 | message Post { 8 | string id = 1; 9 | string title = 2; 10 | string body = 3; 11 | bool published = 4; 12 | string author = 5; 13 | string createdAt = 6; 14 | string updatedAt = 7; 15 | int32 version = 8; 16 | } 17 | 18 | message PostEdge { 19 | Post node = 1; 20 | string cursor = 2; 21 | } 22 | 23 | message CreatePostInput { 24 | string title = 2; 25 | string body = 3; 26 | bool published = 4; 27 | string author = 5; 28 | } 29 | 30 | message UpdatePostInput { 31 | string id = 1; 32 | Post data = 2; 33 | } 34 | 35 | message FindPostsPayload { 36 | repeated PostEdge edges = 1; 37 | commons.PageInfo pageInfo = 2; 38 | } 39 | 40 | service PostsService { 41 | rpc find (commons.Query) returns (FindPostsPayload) {} 42 | rpc findById (commons.Id) returns (Post) {} 43 | rpc findOne (commons.Query) returns (Post) {} 44 | rpc count (commons.Query) returns (commons.Count) {} 45 | rpc create (CreatePostInput) returns (Post) {} 46 | rpc update (UpdatePostInput) returns (Post) {} 47 | rpc destroy (commons.Query) returns (commons.Count) {} 48 | } 49 | -------------------------------------------------------------------------------- /microservices/posts-svc/src/_proto/user.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package user; 4 | 5 | import "commons.proto"; 6 | 7 | message User { 8 | string id = 1; 9 | string name = 2; 10 | string email = 3; 11 | string password = 4; 12 | int32 age = 5; 13 | string createdAt = 6; 14 | string updatedAt = 7; 15 | int32 version = 8; 16 | } 17 | 18 | message UserEdge { 19 | User node = 1; 20 | string cursor = 2; 21 | } 22 | 23 | message CreateUserInput { 24 | string name = 1; 25 | string email = 2; 26 | string password = 3; 27 | int32 age = 4; 28 | } 29 | 30 | message UpdateUserInput { 31 | string id = 1; 32 | User data = 2; 33 | } 34 | 35 | message FindUsersPayload { 36 | repeated UserEdge edges = 1; 37 | commons.PageInfo pageInfo = 2; 38 | } 39 | 40 | service UsersService { 41 | rpc find (commons.Query) returns (FindUsersPayload) {} 42 | rpc findById (commons.Id) returns (User) {} 43 | rpc findOne (commons.Query) returns (User) {} 44 | rpc count (commons.Query) returns (commons.Count) {} 45 | rpc create (CreateUserInput) returns (User) {} 46 | rpc update (UpdateUserInput) returns (User) {} 47 | rpc destroy (commons.Query) returns (commons.Count) {} 48 | } 49 | -------------------------------------------------------------------------------- /microservices/posts-svc/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { ConfigModule, ConfigService } from '@nestjs/config' 3 | import { SequelizeModule, SequelizeModuleOptions } from '@nestjs/sequelize' 4 | 5 | import { Op, OperatorsAliases } from 'sequelize' 6 | import { LoggerModule, PinoLogger } from 'nestjs-pino' 7 | 8 | import { PostsModule } from './posts/posts.module' 9 | 10 | const operatorsAliases: OperatorsAliases = { 11 | _and: Op.and, 12 | _or: Op.or, 13 | _eq: Op.eq, 14 | _ne: Op.ne, 15 | _is: Op.is, 16 | _not: Op.not, 17 | _col: Op.col, 18 | _gt: Op.gt, 19 | _gte: Op.gte, 20 | _lt: Op.lt, 21 | _lte: Op.lte, 22 | _between: Op.between, 23 | _notBetween: Op.notBetween, 24 | _all: Op.all, 25 | _in: Op.in, 26 | _notIn: Op.notIn, 27 | _like: Op.like, 28 | _notLike: Op.notLike, 29 | _startsWith: Op.startsWith, 30 | _endsWith: Op.endsWith, 31 | _substring: Op.substring, 32 | _iLike: Op.iLike, 33 | _notILike: Op.notILike, 34 | _regexp: Op.regexp, 35 | _notRegexp: Op.notRegexp, 36 | _iRegexp: Op.iRegexp, 37 | _notIRegexp: Op.notIRegexp, 38 | _any: Op.any, 39 | _contains: Op.contains, 40 | _contained: Op.contained, 41 | _overlap: Op.overlap, 42 | _adjacent: Op.adjacent, 43 | _strictLeft: Op.strictLeft, 44 | _strictRight: Op.strictRight, 45 | _noExtendRight: Op.noExtendRight, 46 | _noExtendLeft: Op.noExtendLeft, 47 | _values: Op.values 48 | } 49 | 50 | @Module({ 51 | imports: [ 52 | ConfigModule.forRoot(), 53 | LoggerModule.forRootAsync({ 54 | imports: [ConfigModule], 55 | useFactory: async (configService: ConfigService) => ({ 56 | pinoHttp: { 57 | safe: true, 58 | prettyPrint: configService.get('NODE_ENV') !== 'production' 59 | } 60 | }), 61 | inject: [ConfigService] 62 | }), 63 | SequelizeModule.forRootAsync({ 64 | imports: [ConfigModule, LoggerModule], 65 | useFactory: async (configService: ConfigService, logger: PinoLogger): Promise => ({ 66 | dialect: 'postgres', 67 | host: configService.get('DB_HOST'), 68 | port: configService.get('DB_PORT'), 69 | username: configService.get('DB_USERNAME'), 70 | password: configService.get('DB_PASSWORD'), 71 | database: configService.get('DB_DATABASE'), 72 | logging: logger.info.bind(logger), 73 | typeValidation: true, 74 | benchmark: true, 75 | native: true, 76 | operatorsAliases, 77 | autoLoadModels: true, 78 | synchronize: configService.get('DB_SYNC'), 79 | define: { 80 | timestamps: true, 81 | underscored: true, 82 | version: true, 83 | schema: configService.get('DB_SCHEMA') 84 | } 85 | }), 86 | inject: [ConfigService, PinoLogger] 87 | }), 88 | PostsModule 89 | ] 90 | }) 91 | export class AppModule {} 92 | -------------------------------------------------------------------------------- /microservices/posts-svc/src/commons/commons.interface.ts: -------------------------------------------------------------------------------- 1 | export interface Iid { 2 | id: string 3 | } 4 | 5 | export interface IQuery { 6 | select?: string[] 7 | where?: string 8 | orderBy?: string[] 9 | limit?: number 10 | before?: string 11 | after?: string 12 | } 13 | 14 | export interface ICount { 15 | count: number 16 | } 17 | -------------------------------------------------------------------------------- /microservices/posts-svc/src/commons/cursor-pagination.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IEdge { 2 | node: T 3 | cursor: string 4 | } 5 | 6 | export interface IPageInfo { 7 | startCursor: string 8 | endCursor: string 9 | hasNextPage: boolean 10 | hasPreviousPage: boolean 11 | } 12 | 13 | export interface IFindPayload { 14 | edges: IEdge[] 15 | pageInfo: IPageInfo 16 | } 17 | -------------------------------------------------------------------------------- /microservices/posts-svc/src/commons/find-and-paginate.interface.ts: -------------------------------------------------------------------------------- 1 | import { WhereOptions, FindAttributeOptions } from 'sequelize/types' 2 | 3 | export interface IFindAndPaginateOptions { 4 | attributes: FindAttributeOptions 5 | where: WhereOptions 6 | order: string[] 7 | limit: number 8 | before: string 9 | after: string 10 | } 11 | 12 | export interface ICursor { 13 | before: string 14 | after: string 15 | hasNext: boolean 16 | hasPrevious: boolean 17 | } 18 | 19 | export interface IFindAndPaginateResult { 20 | results: T[] 21 | cursors: ICursor 22 | } 23 | -------------------------------------------------------------------------------- /microservices/posts-svc/src/main.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path' 2 | 3 | import { NestFactory } from '@nestjs/core' 4 | import { Transport, MicroserviceOptions } from '@nestjs/microservices' 5 | import { Logger } from 'nestjs-pino' 6 | 7 | import { LoggerService, INestMicroservice } from '@nestjs/common' 8 | import { AppModule } from './app.module' 9 | 10 | async function main() { 11 | const app: INestMicroservice = await NestFactory.createMicroservice(AppModule, { 12 | transport: Transport.GRPC, 13 | options: { 14 | url: `${process.env.GRPC_HOST}:${process.env.GRPC_PORT}`, 15 | package: 'post', 16 | protoPath: join(__dirname, './_proto/post.proto'), 17 | loader: { 18 | keepCase: true, 19 | enums: String, 20 | oneofs: true, 21 | arrays: true 22 | } 23 | } 24 | }) 25 | 26 | app.useLogger(app.get(Logger)) 27 | 28 | return app.listenAsync() 29 | } 30 | 31 | main() 32 | -------------------------------------------------------------------------------- /microservices/posts-svc/src/posts/post.dto.ts: -------------------------------------------------------------------------------- 1 | export class PostDto { 2 | readonly id?: string 3 | readonly title?: string 4 | readonly body?: string 5 | readonly published?: boolean 6 | readonly author?: string 7 | readonly createdAt?: string 8 | readonly updatedAt?: string 9 | readonly version?: number 10 | } 11 | -------------------------------------------------------------------------------- /microservices/posts-svc/src/posts/post.model.ts: -------------------------------------------------------------------------------- 1 | import * as paginate from 'sequelize-cursor-pagination' 2 | import { Column, Model, Table, DataType, Index } from 'sequelize-typescript' 3 | 4 | @Table({ 5 | modelName: 'post', 6 | tableName: 'posts' 7 | }) 8 | export class Post extends Model { 9 | @Column({ 10 | primaryKey: true, 11 | type: DataType.UUID, 12 | defaultValue: DataType.UUIDV1, 13 | comment: 'The identifier for the post record.' 14 | }) 15 | id: string 16 | 17 | @Index('post_title') 18 | @Column({ 19 | type: DataType.TEXT, 20 | comment: 'The post title or topic.', 21 | allowNull: false 22 | }) 23 | title: string 24 | 25 | @Column({ 26 | type: DataType.TEXT, 27 | comment: 'The post body or contents.' 28 | }) 29 | body: string 30 | 31 | @Index('post_published') 32 | @Column({ 33 | type: DataType.BOOLEAN, 34 | comment: 'Denotes if the post is published or not.' 35 | }) 36 | published: boolean 37 | 38 | @Index('post_author') 39 | @Column({ 40 | type: DataType.UUID, 41 | comment: 'Ref: User. The author of the post.' 42 | }) 43 | author: string 44 | } 45 | 46 | paginate({ 47 | methodName: 'findAndPaginate', 48 | primaryKeyField: 'id' 49 | })(Post) 50 | -------------------------------------------------------------------------------- /microservices/posts-svc/src/posts/posts.interface.ts: -------------------------------------------------------------------------------- 1 | import { FindOptions } from 'sequelize/types' 2 | 3 | import { Post } from './post.model' 4 | import { PostDto } from './post.dto' 5 | import { IFindAndPaginateOptions, IFindAndPaginateResult } from '../commons/find-and-paginate.interface' 6 | 7 | export interface IPostUpdateInput { 8 | id: string 9 | data: PostDto 10 | } 11 | 12 | export interface IPostsService { 13 | find(query?: IFindAndPaginateOptions): Promise> 14 | findById(id: string): Promise 15 | findOne(query?: FindOptions): Promise 16 | count(query?: FindOptions): Promise 17 | create(post: PostDto): Promise 18 | update(id: string, post: PostDto): Promise 19 | destroy(query?: FindOptions): Promise 20 | } 21 | -------------------------------------------------------------------------------- /microservices/posts-svc/src/posts/posts.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { LoggerModule } from 'nestjs-pino' 3 | import { SequelizeModule } from '@nestjs/sequelize' 4 | 5 | import { Post } from './post.model' 6 | import { PostsController } from './posts.controller' 7 | import { PostsService } from './posts.service' 8 | 9 | @Module({ 10 | imports: [LoggerModule, SequelizeModule.forFeature([Post])], 11 | providers: [{ provide: 'PostsService', useClass: PostsService }], 12 | controllers: [PostsController] 13 | }) 14 | export class PostsModule {} 15 | -------------------------------------------------------------------------------- /microservices/posts-svc/src/posts/posts.service.ts: -------------------------------------------------------------------------------- 1 | import { isEmpty } from 'lodash' 2 | import { PinoLogger } from 'nestjs-pino' 3 | import { Injectable } from '@nestjs/common' 4 | import { FindOptions } from 'sequelize/types' 5 | import { InjectModel } from '@nestjs/sequelize' 6 | 7 | import { IPostsService } from './posts.interface' 8 | import { IFindAndPaginateOptions, IFindAndPaginateResult } from '../commons/find-and-paginate.interface' 9 | 10 | import { Post } from './post.model' 11 | import { PostDto } from './post.dto' 12 | 13 | @Injectable() 14 | export class PostsService implements IPostsService { 15 | constructor(@InjectModel(Post) private readonly repo: typeof Post, private readonly logger: PinoLogger) { 16 | logger.setContext(PostsService.name) 17 | } 18 | 19 | async find(query?: IFindAndPaginateOptions): Promise> { 20 | this.logger.info('PostsService#findAll.call %o', query) 21 | 22 | // @ts-ignore 23 | const result: IFindAndPaginateResult = await this.repo.findAndPaginate({ 24 | ...query, 25 | raw: true, 26 | paranoid: false 27 | }) 28 | 29 | this.logger.info('PostsService#findAll.result %o', result) 30 | 31 | return result 32 | } 33 | 34 | async findById(id: string): Promise { 35 | this.logger.info('PostsService#findById.call %o', id) 36 | 37 | const result: Post = await this.repo.findByPk(id, { 38 | raw: true 39 | }) 40 | 41 | this.logger.info('PostsService#findById.result %o', result) 42 | 43 | return result 44 | } 45 | 46 | async findOne(query: FindOptions): Promise { 47 | this.logger.info('PostsService#findOne.call %o', query) 48 | 49 | const result: Post = await this.repo.findOne({ 50 | ...query, 51 | raw: true 52 | }) 53 | 54 | this.logger.info('PostsService#findOne.result %o', result) 55 | 56 | return result 57 | } 58 | 59 | async count(query?: FindOptions): Promise { 60 | this.logger.info('PostsService#count.call %o', query) 61 | 62 | const result: number = await this.repo.count(query) 63 | 64 | this.logger.info('PostsService#count.result %o', result) 65 | 66 | return result 67 | } 68 | 69 | async create(commentDto: PostDto): Promise { 70 | this.logger.info('PostsService#create.call %o', commentDto) 71 | 72 | const result: Post = await this.repo.create(commentDto) 73 | 74 | this.logger.info('PostsService#create.result %o', result) 75 | 76 | return result 77 | } 78 | 79 | async update(id: string, comment: PostDto): Promise { 80 | this.logger.info('PostsService#update.call %o', comment) 81 | 82 | const record: Post = await this.repo.findByPk(id) 83 | 84 | if (isEmpty(record)) throw new Error('Record not found.') 85 | 86 | const result: Post = await record.update(comment) 87 | 88 | this.logger.info('PostsService#update.result %o', result) 89 | 90 | return result 91 | } 92 | 93 | async destroy(query?: FindOptions): Promise { 94 | this.logger.info('PostsService#destroy.call %o', query) 95 | 96 | const result: number = await this.repo.destroy(query) 97 | 98 | this.logger.info('PostsService#destroy.result %o', result) 99 | 100 | return result 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /microservices/posts-svc/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /microservices/posts-svc/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "target": "ES2019", 9 | "sourceMap": true, 10 | "outDir": "./dist", 11 | "baseUrl": "./", 12 | "incremental": true 13 | }, 14 | "exclude": ["node_modules", "dist", "__tests__", "**/*.spec.ts", "**/*.test.ts"] 15 | } 16 | -------------------------------------------------------------------------------- /microservices/users-svc/.env.example: -------------------------------------------------------------------------------- 1 | # Environment 2 | NODE_ENV= 3 | 4 | # gRPC Server Options 5 | GRPC_HOST= 6 | GRPC_PORT= 7 | 8 | # DB Options 9 | DB_HOST= 10 | DB_PORT= 11 | DB_USERNAME= 12 | DB_PASSWORD= 13 | DB_DATABASE= 14 | DB_SCHEMA= 15 | DB_SYNC= 16 | 17 | # Cache Options 18 | REDIS_HOST= 19 | REDIS_PORT= 20 | -------------------------------------------------------------------------------- /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 | - "plugin:@typescript-eslint/eslint-recommended" 12 | - "plugin:@typescript-eslint/recommended" 13 | - "prettier" 14 | - "prettier/@typescript-eslint" 15 | - "plugin:prettier/recommended" 16 | - "plugin:jest/recommended" 17 | - "plugin:import/errors" 18 | - "plugin:import/warnings" 19 | - "plugin:import/typescript" 20 | root: true 21 | env: 22 | node: true 23 | jest: true 24 | rules: 25 | "@typescript-eslint/interface-name-prefix": off 26 | "@typescript-eslint/explicit-function-return-type": off 27 | "@typescript-eslint/no-explicit-any": off 28 | "@typescript-eslint/ban-ts-comment": 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: 300 42 | semi: false 43 | singleQuote: true 44 | endOfline: "lf" 45 | trailingComma: "none" 46 | 47 | 48 | -------------------------------------------------------------------------------- /microservices/users-svc/.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | cache: 3 | key: ${CI_PROJECT_NAME} 4 | paths: 5 | - node_modules/ 6 | 7 | variables: 8 | K8S_DEPLOYMENT_NAME: users-svc 9 | K8S_CONTAINER_NAME: users-svc 10 | K8S_DEV_NAMESPACE: dev 11 | K8S_STG_NAMESPACE: stg 12 | K8S_PROD_NAMESPACE: prod 13 | CONTAINER_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA 14 | 15 | stages: 16 | - setup 17 | - test 18 | - build 19 | - release 20 | - deploy 21 | 22 | setup: 23 | image: node:12-alpine 24 | stage: setup 25 | before_script: 26 | - apk add --no-cache make g++ python postgresql-dev &> /dev/null 27 | - rm -rf ./node_modules/.cache 28 | script: 29 | - npm install --prefer-offline &> /dev/null 30 | only: 31 | - merge_requests 32 | tags: 33 | - docker 34 | allow_failure: false 35 | when: always 36 | 37 | test: 38 | image: node:12-alpine 39 | stage: test 40 | dependencies: 41 | - setup 42 | before_script: 43 | - apk add --no-cache libpq 44 | script: 45 | - npm run lint 46 | - npm run test:coverage 47 | coverage: /All files[^|]*\|[^|]*\s+([\d\.]+)/ 48 | services: 49 | - name: postgres:12-alpine 50 | alias: db 51 | - name: redis:5-alpine 52 | alias: cache 53 | variables: 54 | NODE_ENV: "test" 55 | GRPC_HOST: "0.0.0.0" 56 | GRPC_PORT: "50051" 57 | DB_HOST: "db" 58 | DB_PORT: "5432" 59 | DB_USERNAME: "postgres" 60 | DB_PASSWORD: "postgres" 61 | DB_DATABASE: "postgres" 62 | DB_SCHEMA: "public" 63 | REDIS_HOST: "cache" 64 | REDIS_PORT: "6379" 65 | POSTGRES_USER: "postgres" 66 | POSTGRES_PASSWORD: "postgres" 67 | only: 68 | - dev 69 | - merge_requests 70 | tags: 71 | - docker 72 | allow_failure: false 73 | when: always 74 | 75 | build: 76 | image: node:12-alpine 77 | stage: build 78 | dependencies: 79 | - test 80 | script: 81 | - npm run build 82 | only: 83 | - dev 84 | tags: 85 | - docker 86 | artifacts: 87 | paths: 88 | - dist/ 89 | allow_failure: false 90 | when: always 91 | 92 | release: 93 | image: docker:19.03.8 94 | stage: release 95 | dependencies: 96 | - build 97 | script: 98 | - docker build -t ${CONTAINER_IMAGE} . 99 | - docker tag ${CONTAINER_IMAGE} $CI_REGISTRY_IMAGE:dev 100 | - docker tag ${CONTAINER_IMAGE} $CI_REGISTRY_IMAGE:latest 101 | - docker login -u ${CI_REGISTRY_USER} -p $CI_REGISTRY_PASSWORD ${CI_REGISTRY} 102 | - docker push ${CONTAINER_IMAGE} 103 | - docker push $CI_REGISTRY_IMAGE:dev 104 | - docker push $CI_REGISTRY_IMAGE:latest 105 | only: 106 | - dev 107 | tags: 108 | - docker 109 | allow_failure: false 110 | when: always 111 | 112 | deploy-dev: 113 | image: docker:19.03.8 114 | stage: deploy 115 | dependencies: 116 | - release 117 | script: 118 | - mkdir -p /builds/${CI_PROJECT_PATH} 119 | - echo "${KUBE_CONFIG}" | base64 -d > /builds/${CI_PROJECT_PATH}/config 120 | - docker run --rm -v /builds/${CI_PROJECT_PATH}/config:/.kube/config bitnami/kubectl:1.17.4 set image deployment/${K8S_DEPLOYMENT_NAME} ${K8S_CONTAINER_NAME}=${CONTAINER_IMAGE} -n ${K8S_DEV_NAMESPACE} 121 | only: 122 | - dev 123 | tags: 124 | - docker 125 | allow_failure: true 126 | when: always 127 | 128 | deploy-stg: 129 | image: docker:19.03.8 130 | stage: deploy 131 | dependencies: 132 | - release 133 | script: 134 | - mkdir -p /builds/${CI_PROJECT_PATH} 135 | - echo "${KUBE_CONFIG}" | base64 -d > /builds/${CI_PROJECT_PATH}/config 136 | - docker run --rm -v /builds/${CI_PROJECT_PATH}/config:/.kube/config bitnami/kubectl:1.17.4 set image deployment/${K8S_DEPLOYMENT_NAME} ${K8S_CONTAINER_NAME}=${CONTAINER_IMAGE} -n ${K8S_STG_NAMESPACE} 137 | only: 138 | - dev 139 | tags: 140 | - docker 141 | allow_failure: true 142 | when: manual 143 | 144 | deploy-prod: 145 | image: docker:19.03.8 146 | stage: deploy 147 | dependencies: 148 | - release 149 | script: 150 | - mkdir -p /builds/${CI_PROJECT_PATH} 151 | - echo "${KUBE_CONFIG}" | base64 -d > /builds/${CI_PROJECT_PATH}/config 152 | - docker run --rm -v /builds/${CI_PROJECT_PATH}/config:/.kube/config bitnami/kubectl:1.17.4 set image deployment/${K8S_DEPLOYMENT_NAME} ${K8S_CONTAINER_NAME}=${CONTAINER_IMAGE} -n ${K8S_PROD_NAMESPACE} 153 | only: 154 | - dev 155 | tags: 156 | - docker 157 | allow_failure: true 158 | when: manual 159 | -------------------------------------------------------------------------------- /microservices/users-svc/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12-alpine as build 2 | 3 | WORKDIR /usr/share/users-svc 4 | 5 | ADD dist package.json ./ 6 | 7 | RUN apk add --no-cache make g++ python postgresql-dev \ 8 | && npm install --production 9 | 10 | FROM node:12-alpine 11 | 12 | RUN apk add --no-cache libpq 13 | 14 | ADD https://github.com/grpc-ecosystem/grpc-health-probe/releases/download/v0.3.2/grpc_health_probe-linux-amd64 /bin/grpc_health_probe 15 | 16 | RUN chmod +x /bin/grpc_health_probe 17 | 18 | WORKDIR /usr/share/users-svc 19 | 20 | COPY --from=build /usr/share/users-svc . 21 | 22 | EXPOSE 50051 23 | 24 | CMD ["node", "main.js"] 25 | -------------------------------------------------------------------------------- /microservices/users-svc/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | 5 | users-svc: 6 | build: 7 | context: "." 8 | networks: 9 | - "users-svc" 10 | ports: 11 | - "50051:50051" 12 | depends_on: 13 | - "db" 14 | - "cache" 15 | environment: 16 | NODE_ENV: "test" 17 | GRPC_HOST: "0.0.0.0" 18 | GRPC_PORT: "50051" 19 | DB_HOST: "db" 20 | DB_PORT: "5432" 21 | DB_USERNAME: "postgres" 22 | DB_PASSWORD: "postgres" 23 | DB_DATABASE: "postgres" 24 | DB_SCHEMA: "public" 25 | DB_SYNC: true 26 | REDIS_HOST: "cache" 27 | REDIS_PORT: "6379" 28 | healthcheck: 29 | test: ["CMD", "/bin/grpc_health_probe", "-addr=:50051"] 30 | interval: 30s 31 | timeout: 10s 32 | retries: 5 33 | restart: "on-failure" 34 | 35 | db: 36 | image: "postgres:12-alpine" 37 | networks: 38 | - "users-svc" 39 | expose: 40 | - "5432" 41 | environment: 42 | POSTGRES_USER: "postgres" 43 | POSTGRES_PASSWORD: "postgres" 44 | healthcheck: 45 | test: ["CMD-SHELL", "sh -c 'pg_isready -U postgres'"] 46 | interval: 30s 47 | timeout: 30s 48 | retries: 3 49 | restart: "on-failure" 50 | 51 | cache: 52 | image: "redis:5-alpine" 53 | networks: 54 | - "users-svc" 55 | expose: 56 | - "6379" 57 | healthcheck: 58 | test: ["CMD-SHELL", "sh -c 'redis-cli PING'"] 59 | interval: 30s 60 | timeout: 30s 61 | retries: 3 62 | restart: "on-failure" 63 | 64 | networks: 65 | users-svc: 66 | -------------------------------------------------------------------------------- /microservices/users-svc/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | setupFilesAfterEnv: [ 3 | "jest-extended" 4 | ], 5 | coverageThreshold: { 6 | global: { 7 | branches: 90, 8 | functions: 90, 9 | lines: 90, 10 | statements: 90 11 | } 12 | }, 13 | collectCoverageFrom: [ 14 | "src/**/*.ts", 15 | "!**/node_modules/**" 16 | ], 17 | "rootDir": "src", 18 | "preset": "ts-jest", 19 | testEnvironment: "node" 20 | } 21 | -------------------------------------------------------------------------------- /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 | "lint": "eslint --max-warnings=0 '{src,__tests__}/**/*.ts'", 12 | "lint:fix": "eslint --fix '{src,__tests__}/**/*.ts'", 13 | "test": "jest", 14 | "test:watch": "jest --watch", 15 | "test:coverage": "jest --coverage", 16 | "copy:protos": "cpy ../../_proto ./src/_proto" 17 | }, 18 | "dependencies": { 19 | "@grpc/proto-loader": "0.5.4", 20 | "@nestjs/common": "7.1.1", 21 | "@nestjs/config": "0.5.0", 22 | "@nestjs/core": "7.1.1", 23 | "@nestjs/microservices": "7.1.1", 24 | "@nestjs/sequelize": "0.1.0", 25 | "aigle": "1.14.1", 26 | "grpc": "1.24.2", 27 | "lodash": "4.17.15", 28 | "nestjs-pino": "1.2.0", 29 | "pg": "8.2.1", 30 | "pg-hstore": "2.3.3", 31 | "pg-native": "3.0.0", 32 | "pino": "6.3.0", 33 | "reflect-metadata": "0.1.13", 34 | "sequelize": "5.21.11", 35 | "sequelize-cursor-pagination": "1.7.0", 36 | "sequelize-typescript": "1.1.0" 37 | }, 38 | "devDependencies": { 39 | "@nestjs/cli": "7.2.0", 40 | "@nestjs/schematics": "7.0.0", 41 | "@nestjs/testing": "7.1.1", 42 | "@types/faker": "4.1.12", 43 | "@types/jest": "25.2.3", 44 | "@types/lodash": "4.14.153", 45 | "@types/node": "14.0.5", 46 | "@types/sequelize": "4.28.9", 47 | "@typescript-eslint/eslint-plugin": "3.0.2", 48 | "@typescript-eslint/parser": "3.0.2", 49 | "cpy-cli": "3.1.1", 50 | "eslint": "7.1.0", 51 | "eslint-config-prettier": "6.11.0", 52 | "eslint-plugin-import": "2.20.2", 53 | "eslint-plugin-jest": "23.13.2", 54 | "eslint-plugin-prettier": "3.1.3", 55 | "faker": "4.1.0", 56 | "jest": "26.0.1", 57 | "jest-extended": "0.11.5", 58 | "pino-pretty": "4.0.0", 59 | "prettier": "2.0.5", 60 | "rimraf": "3.0.2", 61 | "ts-jest": "26.0.0", 62 | "typescript": "3.9.3" 63 | }, 64 | "repository": { 65 | "type": "git", 66 | "url": "git+ssh://git@github.com:benjsicam/nestjs-graphql-microservices.git" 67 | }, 68 | "author": "Benj Sicam (https://github.com/benjsicam)", 69 | "license": "MIT", 70 | "bugs": { 71 | "url": "https://github.com/benjsicam/nestjs-graphql-microservices/issues" 72 | }, 73 | "homepage": "https://github.com/benjsicam/nestjs-graphql-microservices#readme" 74 | } 75 | -------------------------------------------------------------------------------- /microservices/users-svc/src/_proto/README.md: -------------------------------------------------------------------------------- 1 | ## Note on Maintenence 2 | 3 | Maintenance of these files should be done under the _proto folder on the root of the project. Use `npm run copy:protos` to copy updates to this directory. -------------------------------------------------------------------------------- /microservices/users-svc/src/_proto/comment.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package comment; 4 | 5 | import "commons.proto"; 6 | 7 | message Comment { 8 | string id = 1; 9 | string text = 2; 10 | string author = 3; 11 | string post = 4; 12 | string createdAt = 5; 13 | string updatedAt = 6; 14 | int32 version = 7; 15 | } 16 | 17 | message CommentEdge { 18 | Comment node = 1; 19 | string cursor = 2; 20 | } 21 | 22 | message CreateCommentInput { 23 | string text = 1; 24 | string author = 2; 25 | string post = 3; 26 | } 27 | 28 | message UpdateCommentInput { 29 | string id = 1; 30 | Comment data = 2; 31 | } 32 | 33 | message FindCommentsPayload { 34 | repeated CommentEdge edges = 1; 35 | commons.PageInfo pageInfo = 2; 36 | } 37 | 38 | service CommentsService { 39 | rpc find (commons.Query) returns (FindCommentsPayload) {} 40 | rpc findById (commons.Id) returns (Comment) {} 41 | rpc findOne (commons.Query) returns (Comment) {} 42 | rpc count (commons.Query) returns (commons.Count) {} 43 | rpc create (CreateCommentInput) returns (Comment) {} 44 | rpc update (UpdateCommentInput) returns (Comment) {} 45 | rpc destroy (commons.Query) returns (commons.Count) {} 46 | } 47 | -------------------------------------------------------------------------------- /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 Query { 10 | repeated string select = 1; 11 | string where = 2; 12 | repeated string orderBy = 3; 13 | int32 limit = 4; 14 | string before = 5; 15 | string after = 6; 16 | } 17 | 18 | message PageInfo { 19 | string startCursor = 1; 20 | string endCursor = 2; 21 | bool hasNextPage = 3; 22 | bool hasPreviousPage = 4; 23 | } 24 | 25 | message Count { 26 | int32 count = 1; 27 | } 28 | -------------------------------------------------------------------------------- /microservices/users-svc/src/_proto/mailer.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package mailer; 4 | 5 | message SendMailInput { 6 | string template = 1; 7 | string to = 2; 8 | bytes data = 3; 9 | } 10 | 11 | message SendMailPayload { 12 | bool isSent = 1; 13 | } 14 | 15 | service MailerService { 16 | rpc send (SendMailInput) returns (SendMailPayload) {} 17 | } 18 | -------------------------------------------------------------------------------- /microservices/users-svc/src/_proto/post.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package post; 4 | 5 | import "commons.proto"; 6 | 7 | message Post { 8 | string id = 1; 9 | string title = 2; 10 | string body = 3; 11 | bool published = 4; 12 | string author = 5; 13 | string createdAt = 6; 14 | string updatedAt = 7; 15 | int32 version = 8; 16 | } 17 | 18 | message PostEdge { 19 | Post node = 1; 20 | string cursor = 2; 21 | } 22 | 23 | message CreatePostInput { 24 | string title = 2; 25 | string body = 3; 26 | bool published = 4; 27 | string author = 5; 28 | } 29 | 30 | message UpdatePostInput { 31 | string id = 1; 32 | Post data = 2; 33 | } 34 | 35 | message FindPostsPayload { 36 | repeated PostEdge edges = 1; 37 | commons.PageInfo pageInfo = 2; 38 | } 39 | 40 | service PostsService { 41 | rpc find (commons.Query) returns (FindPostsPayload) {} 42 | rpc findById (commons.Id) returns (Post) {} 43 | rpc findOne (commons.Query) returns (Post) {} 44 | rpc count (commons.Query) returns (commons.Count) {} 45 | rpc create (CreatePostInput) returns (Post) {} 46 | rpc update (UpdatePostInput) returns (Post) {} 47 | rpc destroy (commons.Query) returns (commons.Count) {} 48 | } 49 | -------------------------------------------------------------------------------- /microservices/users-svc/src/_proto/user.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package user; 4 | 5 | import "commons.proto"; 6 | 7 | message User { 8 | string id = 1; 9 | string name = 2; 10 | string email = 3; 11 | string password = 4; 12 | int32 age = 5; 13 | string createdAt = 6; 14 | string updatedAt = 7; 15 | int32 version = 8; 16 | } 17 | 18 | message UserEdge { 19 | User node = 1; 20 | string cursor = 2; 21 | } 22 | 23 | message CreateUserInput { 24 | string name = 1; 25 | string email = 2; 26 | string password = 3; 27 | int32 age = 4; 28 | } 29 | 30 | message UpdateUserInput { 31 | string id = 1; 32 | User data = 2; 33 | } 34 | 35 | message FindUsersPayload { 36 | repeated UserEdge edges = 1; 37 | commons.PageInfo pageInfo = 2; 38 | } 39 | 40 | service UsersService { 41 | rpc find (commons.Query) returns (FindUsersPayload) {} 42 | rpc findById (commons.Id) returns (User) {} 43 | rpc findOne (commons.Query) returns (User) {} 44 | rpc count (commons.Query) returns (commons.Count) {} 45 | rpc create (CreateUserInput) returns (User) {} 46 | rpc update (UpdateUserInput) returns (User) {} 47 | rpc destroy (commons.Query) returns (commons.Count) {} 48 | } 49 | -------------------------------------------------------------------------------- /microservices/users-svc/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { ConfigModule, ConfigService } from '@nestjs/config' 3 | import { SequelizeModule, SequelizeModuleOptions } from '@nestjs/sequelize' 4 | 5 | import { Op, OperatorsAliases } from 'sequelize' 6 | import { LoggerModule, PinoLogger } from 'nestjs-pino' 7 | 8 | import { UsersModule } from './users/users.module' 9 | 10 | const operatorsAliases: OperatorsAliases = { 11 | _and: Op.and, 12 | _or: Op.or, 13 | _eq: Op.eq, 14 | _ne: Op.ne, 15 | _is: Op.is, 16 | _not: Op.not, 17 | _col: Op.col, 18 | _gt: Op.gt, 19 | _gte: Op.gte, 20 | _lt: Op.lt, 21 | _lte: Op.lte, 22 | _between: Op.between, 23 | _notBetween: Op.notBetween, 24 | _all: Op.all, 25 | _in: Op.in, 26 | _notIn: Op.notIn, 27 | _like: Op.like, 28 | _notLike: Op.notLike, 29 | _startsWith: Op.startsWith, 30 | _endsWith: Op.endsWith, 31 | _substring: Op.substring, 32 | _iLike: Op.iLike, 33 | _notILike: Op.notILike, 34 | _regexp: Op.regexp, 35 | _notRegexp: Op.notRegexp, 36 | _iRegexp: Op.iRegexp, 37 | _notIRegexp: Op.notIRegexp, 38 | _any: Op.any, 39 | _contains: Op.contains, 40 | _contained: Op.contained, 41 | _overlap: Op.overlap, 42 | _adjacent: Op.adjacent, 43 | _strictLeft: Op.strictLeft, 44 | _strictRight: Op.strictRight, 45 | _noExtendRight: Op.noExtendRight, 46 | _noExtendLeft: Op.noExtendLeft, 47 | _values: Op.values 48 | } 49 | 50 | @Module({ 51 | imports: [ 52 | ConfigModule.forRoot(), 53 | LoggerModule.forRootAsync({ 54 | imports: [ConfigModule], 55 | useFactory: async (configService: ConfigService) => ({ 56 | pinoHttp: { 57 | safe: true, 58 | prettyPrint: configService.get('NODE_ENV') !== 'production' 59 | } 60 | }), 61 | inject: [ConfigService] 62 | }), 63 | SequelizeModule.forRootAsync({ 64 | imports: [ConfigModule, LoggerModule], 65 | useFactory: async (configService: ConfigService, logger: PinoLogger): Promise => ({ 66 | dialect: 'postgres', 67 | host: configService.get('DB_HOST'), 68 | port: configService.get('DB_PORT'), 69 | username: configService.get('DB_USERNAME'), 70 | password: configService.get('DB_PASSWORD'), 71 | database: configService.get('DB_DATABASE'), 72 | logging: logger.info.bind(logger), 73 | typeValidation: true, 74 | benchmark: true, 75 | native: true, 76 | operatorsAliases, 77 | autoLoadModels: true, 78 | synchronize: configService.get('DB_SYNC'), 79 | define: { 80 | timestamps: true, 81 | underscored: true, 82 | version: true, 83 | schema: configService.get('DB_SCHEMA') 84 | } 85 | }), 86 | inject: [ConfigService, PinoLogger] 87 | }), 88 | UsersModule 89 | ] 90 | }) 91 | export class AppModule {} 92 | -------------------------------------------------------------------------------- /microservices/users-svc/src/commons/commons.interface.ts: -------------------------------------------------------------------------------- 1 | export interface Iid { 2 | id: string 3 | } 4 | 5 | export interface IQuery { 6 | select?: string[] 7 | where?: string 8 | orderBy?: string[] 9 | limit?: number 10 | before?: string 11 | after?: string 12 | } 13 | 14 | export interface ICount { 15 | count: number 16 | } 17 | -------------------------------------------------------------------------------- /microservices/users-svc/src/commons/cursor-pagination.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IEdge { 2 | node: T 3 | cursor: string 4 | } 5 | 6 | export interface IPageInfo { 7 | startCursor: string 8 | endCursor: string 9 | hasNextPage: boolean 10 | hasPreviousPage: boolean 11 | } 12 | 13 | export interface IFindPayload { 14 | edges: IEdge[] 15 | pageInfo: IPageInfo 16 | } 17 | -------------------------------------------------------------------------------- /microservices/users-svc/src/commons/find-and-paginate.interface.ts: -------------------------------------------------------------------------------- 1 | import { WhereOptions, FindAttributeOptions } from 'sequelize/types' 2 | 3 | export interface IFindAndPaginateOptions { 4 | attributes: FindAttributeOptions 5 | where: WhereOptions 6 | order: string[] 7 | limit: number 8 | before: string 9 | after: string 10 | } 11 | 12 | export interface ICursor { 13 | before: string 14 | after: string 15 | hasNext: boolean 16 | hasPrevious: boolean 17 | } 18 | 19 | export interface IFindAndPaginateResult { 20 | results: T[] 21 | cursors: ICursor 22 | } 23 | -------------------------------------------------------------------------------- /microservices/users-svc/src/main.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path' 2 | 3 | import { NestFactory } from '@nestjs/core' 4 | import { Transport, MicroserviceOptions } from '@nestjs/microservices' 5 | import { Logger } from 'nestjs-pino' 6 | 7 | import { LoggerService, INestMicroservice } from '@nestjs/common' 8 | import { AppModule } from './app.module' 9 | 10 | async function main() { 11 | const app: INestMicroservice = await NestFactory.createMicroservice(AppModule, { 12 | transport: Transport.GRPC, 13 | options: { 14 | url: `${process.env.GRPC_HOST}:${process.env.GRPC_PORT}`, 15 | package: 'user', 16 | protoPath: join(__dirname, './_proto/user.proto'), 17 | loader: { 18 | keepCase: true, 19 | enums: String, 20 | oneofs: true, 21 | arrays: true 22 | } 23 | } 24 | }) 25 | 26 | app.useLogger(app.get(Logger)) 27 | 28 | return app.listenAsync() 29 | } 30 | 31 | main() 32 | -------------------------------------------------------------------------------- /microservices/users-svc/src/users/user.dto.ts: -------------------------------------------------------------------------------- 1 | export class UserDto { 2 | readonly id?: string 3 | readonly name?: string 4 | readonly email?: string 5 | readonly password?: string 6 | readonly age?: number 7 | readonly createdAt?: string 8 | readonly updatedAt?: string 9 | readonly version?: number 10 | } 11 | -------------------------------------------------------------------------------- /microservices/users-svc/src/users/user.model.ts: -------------------------------------------------------------------------------- 1 | import * as paginate from 'sequelize-cursor-pagination' 2 | import { Column, Model, Table, DataType, Index } from 'sequelize-typescript' 3 | 4 | @Table({ 5 | modelName: 'user', 6 | tableName: 'users' 7 | }) 8 | export class User extends Model { 9 | @Column({ 10 | primaryKey: true, 11 | type: DataType.UUID, 12 | defaultValue: DataType.UUIDV1, 13 | comment: 'The identifier for the user record.' 14 | }) 15 | id: string 16 | 17 | @Index('user_name') 18 | @Column({ 19 | type: DataType.TEXT, 20 | comment: "The user's name." 21 | }) 22 | name: string 23 | 24 | @Index('user_email') 25 | @Column({ 26 | type: DataType.TEXT, 27 | comment: "The user's email." 28 | }) 29 | email: string 30 | 31 | @Column({ 32 | type: DataType.TEXT, 33 | comment: "The user's password." 34 | }) 35 | password: string 36 | 37 | @Column({ 38 | type: DataType.INTEGER, 39 | comment: "The user's age." 40 | }) 41 | age: number 42 | } 43 | 44 | paginate({ 45 | methodName: 'findAndPaginate', 46 | primaryKeyField: 'id' 47 | })(User) 48 | -------------------------------------------------------------------------------- /microservices/users-svc/src/users/users.interface.ts: -------------------------------------------------------------------------------- 1 | import { FindOptions } from 'sequelize/types' 2 | 3 | import { User } from './user.model' 4 | import { UserDto } from './user.dto' 5 | import { IFindAndPaginateOptions, IFindAndPaginateResult } from '../commons/find-and-paginate.interface' 6 | 7 | export interface IUsersService { 8 | find(query?: IFindAndPaginateOptions): Promise> 9 | findById(id: string): Promise 10 | findOne(query?: FindOptions): Promise 11 | count(query?: FindOptions): Promise 12 | create(comment: UserDto): Promise 13 | update(id: string, comment: UserDto): Promise 14 | destroy(query?: FindOptions): Promise 15 | } 16 | -------------------------------------------------------------------------------- /microservices/users-svc/src/users/users.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { LoggerModule } from 'nestjs-pino' 3 | import { SequelizeModule } from '@nestjs/sequelize' 4 | 5 | import { User } from './user.model' 6 | import { UsersController } from './users.controller' 7 | import { UsersService } from './users.service' 8 | 9 | @Module({ 10 | imports: [LoggerModule, SequelizeModule.forFeature([User])], 11 | providers: [{ provide: 'UsersService', useClass: UsersService }], 12 | controllers: [UsersController] 13 | }) 14 | export class UsersModule {} 15 | -------------------------------------------------------------------------------- /microservices/users-svc/src/users/users.service.ts: -------------------------------------------------------------------------------- 1 | import { isEmpty } from 'lodash' 2 | import { PinoLogger } from 'nestjs-pino' 3 | import { Injectable } from '@nestjs/common' 4 | import { FindOptions } from 'sequelize/types' 5 | import { InjectModel } from '@nestjs/sequelize' 6 | 7 | import { IUsersService } from './users.interface' 8 | import { IFindAndPaginateOptions, IFindAndPaginateResult } from '../commons/find-and-paginate.interface' 9 | 10 | import { User } from './user.model' 11 | import { UserDto } from './user.dto' 12 | 13 | @Injectable() 14 | export class UsersService implements IUsersService { 15 | constructor(@InjectModel(User) private readonly repo: typeof User, private readonly logger: PinoLogger) { 16 | logger.setContext(UsersService.name) 17 | } 18 | 19 | async find(query?: IFindAndPaginateOptions): Promise> { 20 | this.logger.info('UsersService#findAll.call %o', query) 21 | 22 | // @ts-ignore 23 | const result: IFindAndPaginateResult = await this.repo.findAndPaginate({ 24 | ...query, 25 | raw: true, 26 | paranoid: false 27 | }) 28 | 29 | this.logger.info('UsersService#findAll.result %o', result) 30 | 31 | return result 32 | } 33 | 34 | async findById(id: string): Promise { 35 | this.logger.info('UsersService#findById.call %o', id) 36 | 37 | const result: User = await this.repo.findByPk(id, { 38 | raw: true 39 | }) 40 | 41 | this.logger.info('UsersService#findById.result %o', result) 42 | 43 | return result 44 | } 45 | 46 | async findOne(query: FindOptions): Promise { 47 | this.logger.info('UsersService#findOne.call %o', query) 48 | 49 | const result: User = await this.repo.findOne({ 50 | ...query, 51 | raw: true 52 | }) 53 | 54 | this.logger.info('UsersService#findOne.result %o', result) 55 | 56 | return result 57 | } 58 | 59 | async count(query?: FindOptions): Promise { 60 | this.logger.info('UsersService#count.call %o', query) 61 | 62 | const result: number = await this.repo.count(query) 63 | 64 | this.logger.info('UsersService#count.result %o', result) 65 | 66 | return result 67 | } 68 | 69 | async create(user: UserDto): Promise { 70 | this.logger.info('UsersService#create.call %o', user) 71 | 72 | const result: User = await this.repo.create(user) 73 | 74 | this.logger.info('UsersService#create.result %o', result) 75 | 76 | return result 77 | } 78 | 79 | async update(id: string, user: UserDto): Promise { 80 | this.logger.info('UsersService#update.call %o', user) 81 | 82 | const record: User = await this.repo.findByPk(id) 83 | 84 | if (isEmpty(record)) throw new Error('Record not found.') 85 | 86 | const result: User = await record.update(user) 87 | 88 | this.logger.info('UsersService#update.result %o', result) 89 | 90 | return result 91 | } 92 | 93 | async destroy(query?: FindOptions): Promise { 94 | this.logger.info('UsersService#destroy.call %o', query) 95 | 96 | const result: number = await this.repo.destroy(query) 97 | 98 | this.logger.info('UsersService#destroy.result %o', result) 99 | 100 | return result 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /microservices/users-svc/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /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": "ES2019", 9 | "sourceMap": true, 10 | "outDir": "./dist", 11 | "baseUrl": "./", 12 | "incremental": true 13 | }, 14 | "exclude": ["node_modules", "dist", "__tests__", "**/*.spec.ts", "**/*.test.ts"] 15 | } 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nestjs-graphql-microservices", 3 | "version": "1.0.0", 4 | "description": "A sample GraphQL 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", 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-graphql-microservices.git" 18 | }, 19 | "author": "Benj Sicam (https://github.com/benjsicam)", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/benjsicam/nestjs-graphql-microservices/issues" 23 | }, 24 | "homepage": "https://github.com/benjsicam/nestjs-graphql-microservices#readme" 25 | } 26 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd api-gateway && npm run copy:protos && npm run build && cd - 4 | cd microservices/comments-svc && npm run copy:protos && npm run build && cd - 5 | cd microservices/posts-svc && npm run copy:protos && npm run build && cd - 6 | cd microservices/users-svc && npm run copy:protos && npm run build && cd - 7 | cd microservices/mailer-svc && npm run copy:protos && npm run build && cd - 8 | docker-compose build --no-cache 9 | -------------------------------------------------------------------------------- /scripts/generate-jwt-secret.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | openssl rand -base64 32 3 | -------------------------------------------------------------------------------- /scripts/generate-proto-docs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker run --rm \ 4 | -v $PWD/docs/proto:/out \ 5 | -v $PWD/_proto:/protos \ 6 | pseudomuto/protoc-gen-doc --doc_opt=markdown,docs.md -------------------------------------------------------------------------------- /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/mailer-svc && npm i && cd - 6 | cd microservices/posts-svc && npm i && cd - 7 | cd microservices/users-svc && npm i && cd - 8 | -------------------------------------------------------------------------------- /scripts/lint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd api-gateway && npm run lint:fix && cd - 4 | cd microservices/comments-svc && npm run lint:fix && cd - 5 | cd microservices/posts-svc && npm run lint:fix && cd - 6 | cd microservices/users-svc && npm run lint:fix && cd - 7 | cd microservices/mailer-svc && npm run lint:fix && cd - 8 | --------------------------------------------------------------------------------