├── .eslintrc.json ├── .github ├── badges │ └── udemy.svg └── workflows │ └── udemy.yml ├── .gitignore ├── .nycrc.json ├── README.md ├── blog ├── client │ └── index.js ├── docker-compose.yml ├── proto │ └── blog.proto └── server │ ├── index.js │ └── service_impl.js ├── calculator ├── client │ └── index.js ├── proto │ ├── avg.proto │ ├── calculator.proto │ ├── max.proto │ ├── primes.proto │ ├── sqrt.proto │ └── sum.proto └── server │ ├── index.js │ └── service_impl.js ├── constants.js ├── greet ├── client │ └── index.js ├── proto │ └── greet.proto └── server │ ├── index.js │ └── service_impl.js ├── package-lock.json ├── package.json ├── scripts ├── README.md ├── gen.ps1 ├── gen.sh ├── ssl.ps1 ├── ssl.ps1.conf └── ssl.sh ├── ssl └── README.md └── test ├── blog ├── create_test.js ├── db_helper.js ├── delete_test.js ├── list_test.js ├── read_test.js └── update_test.js ├── calculator └── calculator_test.js ├── greet └── greet_test.js └── grpc_helper.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es2021": true 6 | }, 7 | "extends": [ 8 | "google" 9 | ], 10 | "parserOptions": { 11 | "ecmaVersion": "latest" 12 | }, 13 | "ignorePatterns": ["*_pb.js"], 14 | "rules": { 15 | "require-jsdoc" : "off" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.github/badges/udemy.svg: -------------------------------------------------------------------------------- 1 | udemy4.33 -------------------------------------------------------------------------------- /.github/workflows/udemy.yml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_dispatch: 3 | schedule: 4 | - cron: '0 0 1 * *' 5 | 6 | jobs: 7 | generate_badge: 8 | runs-on: ubuntu-latest 9 | name: badge_generation 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v2 13 | - name: Badge generation 14 | uses: Clement-Jean/udemy-badge-generator@v1 15 | env: 16 | UDEMY_TOKEN: ${{ secrets.UDEMY_TOKEN }} 17 | UDEMY_COURSE: ${{ secrets.UDEMY_COURSE }} 18 | - name: Commit the badge (if it changed) 19 | run: | 20 | if [[ `git status --porcelain` ]]; then 21 | git config --global user.name 'Clement Jean' 22 | git config --global user.email 'Clement-Jean@users.noreply.github.com' 23 | git add .github/badges/udemy.svg 24 | git commit -m "Autogenerated Udemy rating badge" 25 | git push 26 | fi 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | ## SSL 133 | *.crt 134 | *.key 135 | *.csr 136 | *.pem 137 | 138 | ## Mac 139 | .DS_Store 140 | 141 | ## Proto 142 | *_pb.js -------------------------------------------------------------------------------- /.nycrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "all": true, 3 | "include": [ 4 | "**/*.js" 5 | ], 6 | "exclude": [ 7 | "test/*", 8 | "constants.js", 9 | "**/*_pb.js", 10 | "**/index.js" 11 | ] 12 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gRPC NodeJS 2 | 3 | ![Udemy](.github/badges/udemy.svg) 4 | 5 | ## COUPON: `START_OCT_2025` 6 | 7 | ## Build 8 | 9 | ### Arm 10 | 11 | ``` 12 | npm_config_target_arch=x64 npm install 13 | ``` 14 | 15 | ### Not Arm 16 | 17 | ``` 18 | npm install 19 | ``` 20 | -------------------------------------------------------------------------------- /blog/client/index.js: -------------------------------------------------------------------------------- 1 | const grpc = require('@grpc/grpc-js'); 2 | const service = require('../proto/blog_grpc_pb'); 3 | const {Blog, BlogId} = require('../proto/blog_pb'); 4 | const {DATA, ERROR, END} = require('../../constants'); 5 | const {Empty} = require('google-protobuf/google/protobuf/empty_pb'); 6 | 7 | function createBlog(client) { 8 | console.log('---createBlog was invoked---'); 9 | return new Promise((resolve, reject) => { 10 | const req = new Blog() 11 | .setAuthorId('Clement') 12 | .setTitle('My First Blog') 13 | .setContent('Content of the first blog'); 14 | 15 | client.createBlog(req, (err, res) => { 16 | if (err) { 17 | reject(err); 18 | } 19 | 20 | console.log(`Blog was created: ${res}`); 21 | resolve(res.getId()); 22 | }); 23 | }); 24 | } 25 | 26 | function readBlog(client, id) { 27 | console.log('---readBlog was invoked---'); 28 | 29 | return new Promise((resolve, reject) => { 30 | const req = new BlogId().setId(id); 31 | 32 | client.readBlog(req, (err, res) => { 33 | if (err) { 34 | reject(err); 35 | } 36 | 37 | console.log(`Blog was read: ${res}`); 38 | resolve(); 39 | }); 40 | }); 41 | } 42 | 43 | function updateBlog(client, id) { 44 | console.log('---updateBlog was invoked---'); 45 | return new Promise((resolve, reject) => { 46 | const req = new Blog() 47 | .setId(id) 48 | .setAuthorId('not Clement') 49 | .setTitle('My First Blog (edited)') 50 | .setContent('Content of the first blog, with some awesome additions!'); 51 | 52 | client.updateBlog(req, (err, _) => { 53 | if (err) { 54 | reject(err); 55 | } 56 | 57 | console.log('Blog was updated!'); 58 | resolve(); 59 | }); 60 | }); 61 | } 62 | 63 | function listBlogs(client) { 64 | console.log('---listBlog was invoked---'); 65 | return new Promise((resolve, reject) => { 66 | const req = new Empty(); 67 | 68 | const call = client.listBlogs(req); 69 | 70 | call.on(DATA, (res) => { 71 | console.log(res); 72 | }); 73 | 74 | call.on(ERROR, (err) => { 75 | reject(err); 76 | }); 77 | 78 | call.on(END, () => { 79 | resolve(); 80 | }); 81 | }); 82 | } 83 | 84 | function deleteBlog(client, id) { 85 | console.log('---deleteBlog was invoked---'); 86 | 87 | return new Promise((resolve, reject) => { 88 | const req = new BlogId().setId(id); 89 | 90 | client.deleteBlog(req, (err, _) => { 91 | if (err) { 92 | reject(err); 93 | } 94 | 95 | console.log(`Blog was deleted!`); 96 | resolve(); 97 | }); 98 | }); 99 | } 100 | 101 | async function main() { 102 | const client = new service.BlogServiceClient( 103 | '0.0.0.0:50051', 104 | grpc.credentials.createInsecure(), 105 | ); 106 | 107 | const id = await createBlog(client); 108 | await readBlog(client, id); 109 | // readBlog(client, 'aNonExistingId'); 110 | await updateBlog(client, id); 111 | await listBlogs(client); 112 | await deleteBlog(client, id); 113 | } 114 | 115 | main(); 116 | -------------------------------------------------------------------------------- /blog/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.1' 2 | 3 | services: 4 | mongo: 5 | image: mongo 6 | restart: always 7 | ports: 8 | - 27017:27017 9 | environment: 10 | MONGO_INITDB_ROOT_USERNAME: root 11 | MONGO_INITDB_ROOT_PASSWORD: root 12 | 13 | mongo-express: 14 | image: mongo-express 15 | restart: always 16 | ports: 17 | - 8081:8081 18 | environment: 19 | ME_CONFIG_MONGODB_ADMINUSERNAME: root 20 | ME_CONFIG_MONGODB_ADMINPASSWORD: root 21 | ME_CONFIG_MONGODB_URL: mongodb://root:root@mongo:27017/ -------------------------------------------------------------------------------- /blog/proto/blog.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package blog; 4 | 5 | import "google/protobuf/empty.proto"; 6 | 7 | message Blog { 8 | string id = 1; 9 | string author_id = 2; 10 | string title = 3; 11 | string content = 4; 12 | } 13 | 14 | message BlogId { 15 | string id = 1; 16 | } 17 | 18 | service BlogService { 19 | // Requests the creation for a Blog 20 | // Returns Status.INTERNAL if the blog couldn't be created due to Db error 21 | // Returns the created Blog's Id 22 | rpc CreateBlog(Blog) returns (BlogId); 23 | 24 | // Requests access to the content of a Blog by sending an Id 25 | // Returns Status.NOT_FOUND if the Id doesn't match any Blog in Db 26 | // Returns Blog content 27 | rpc ReadBlog(BlogId) returns (Blog); 28 | 29 | // Requests the update of a Blog in Db 30 | // Returns Status.NOT_FOUND if the Id doesn't match any Blog in Db 31 | // Returns Status.INTERNAL if the blog couldn't be updated due to Db error 32 | // Returns Empty 33 | rpc UpdateBlog(Blog) returns (google.protobuf.Empty); 34 | 35 | // Requests the delete of a Blog in Db by giving its Id 36 | // Returns Status.NOT_FOUND if the Id doesn't match any Blog in Db 37 | // Returns Status.INTERNAL if the blog couldn't be deleted due to Db error 38 | // Returns Empty 39 | rpc DeleteBlog(BlogId) returns (google.protobuf.Empty); 40 | 41 | // Requests access to all the Blogs in Db 42 | // Returns stream of Blogs 43 | rpc ListBlogs(google.protobuf.Empty) returns (stream Blog); 44 | } -------------------------------------------------------------------------------- /blog/server/index.js: -------------------------------------------------------------------------------- 1 | const grpc = require('@grpc/grpc-js'); 2 | const {MongoClient} = require('mongodb'); 3 | const service = require('../proto/blog_grpc_pb'); 4 | const serviceImpl = require('./service_impl'); 5 | 6 | const addr = '0.0.0.0:50051'; 7 | const uri = 'mongodb://root:root@localhost:27017/'; 8 | const mongoClient = new MongoClient(uri, { 9 | connectTimeoutMS: 1000, 10 | serverSelectionTimeoutMS: 1000, 11 | }); 12 | 13 | global.collection = undefined; 14 | 15 | async function cleanup(server) { 16 | console.log('Cleanup'); 17 | 18 | if (server) { 19 | await mongoClient.close(); 20 | server.forceShutdown(); 21 | } 22 | } 23 | 24 | async function main() { 25 | process.on('SIGINT', () => { 26 | console.log('Caught interrupt signal'); 27 | cleanup(server); 28 | }); 29 | 30 | await mongoClient.connect(); 31 | 32 | const database = mongoClient.db('blogdb'); 33 | collection = database.collection('blog'); 34 | 35 | const creds = grpc.ServerCredentials.createInsecure(); 36 | server = new grpc.Server(); 37 | server.addService(service.BlogServiceService, serviceImpl); 38 | server.bindAsync(addr, creds, (err, _) => { 39 | if (err) { 40 | return cleanup(server); 41 | } 42 | 43 | server.start(); 44 | }); 45 | 46 | console.log('Listening on: ' + addr); 47 | } 48 | 49 | main().catch(cleanup); 50 | -------------------------------------------------------------------------------- /blog/server/service_impl.js: -------------------------------------------------------------------------------- 1 | const grpc = require('@grpc/grpc-js'); 2 | const {Blog, BlogId} = require('../proto/blog_pb'); 3 | const {ObjectId} = require('mongodb'); 4 | const {Empty} = require('google-protobuf/google/protobuf/empty_pb'); 5 | 6 | function blogToDocument(blog) { 7 | return { 8 | author_id: blog.getAuthorId(), 9 | title: blog.getTitle(), 10 | content: blog.getContent(), 11 | }; 12 | } 13 | 14 | function documentToBlog(doc) { 15 | return new Blog() 16 | .setId(doc._id.toString()) 17 | .setAuthorId(doc.author_id) 18 | .setTitle(doc.title) 19 | .setContent(doc.content); 20 | } 21 | 22 | const internal = (err, callback) => callback({ 23 | code: grpc.status.INTERNAL, 24 | message: err.toString(), 25 | }); 26 | 27 | function checkOID(id, callback) { 28 | try { 29 | return new ObjectId(id); 30 | } catch (err) { 31 | callback({ 32 | code: grpc.status.INVALID_ARGUMENT, 33 | message: 'Invalid OID', 34 | }); 35 | } 36 | } 37 | 38 | function checkNotAcknowledged(res, callback) { 39 | if (!res.acknowledged) { 40 | callback({ 41 | code: grpc.status.INTERNAL, 42 | message: `Operation wasn\'t acknowledged`, 43 | }); 44 | } 45 | } 46 | 47 | function checkNotFound(res, callback) { 48 | if (!res || res.matchedCount == 0 || res.deletedCount == 0) { 49 | callback({ 50 | code: grpc.status.NOT_FOUND, 51 | message: 'Could not find blog', 52 | }); 53 | } 54 | } 55 | 56 | exports.createBlog = async (call, callback) => { 57 | const data = blogToDocument(call.request); 58 | 59 | await collection.insertOne(data).then((res) => { 60 | checkNotAcknowledged(res, callback); 61 | const id = res.insertedId.toString(); 62 | const blogId = new BlogId().setId(id); 63 | 64 | callback(null, blogId); 65 | }).catch((err) => internal(err, callback)); 66 | }; 67 | 68 | exports.readBlog = async (call, callback) => { 69 | const oid = checkOID(call.request.getId(), callback); 70 | 71 | await collection.findOne({_id: oid}).then((res) => { 72 | checkNotFound(res, callback); 73 | callback(null, documentToBlog(res)); 74 | }).catch((err) => internal(err, callback)); 75 | }; 76 | 77 | exports.updateBlog = async (call, callback) => { 78 | const oid = checkOID(call.request.getId(), callback); 79 | 80 | await collection.updateOne( 81 | {_id: oid}, 82 | {$set: blogToDocument(call.request)}, 83 | ).then((res) => { 84 | checkNotFound(res, callback); 85 | checkNotAcknowledged(res, callback); 86 | callback(null, new Empty()); 87 | }).catch((err) => internal(err, callback)); 88 | }; 89 | 90 | exports.listBlogs = async (call, _) => 91 | await collection.find() 92 | .map((doc) => documentToBlog(doc)) 93 | .forEach((blog) => call.write(blog)) 94 | .then(() => call.end()) 95 | .catch((err) => call.destroy({ 96 | code: grpc.status.INTERNAL, 97 | message: 'A message', 98 | })); 99 | 100 | exports.deleteBlog = async (call, callback) => { 101 | const oid = checkOID(call.request.getId(), callback); 102 | 103 | await collection.deleteOne({_id: oid}).then((res) => { 104 | checkNotFound(res, callback); 105 | checkNotAcknowledged(res, callback); 106 | callback(null, new Empty()); 107 | }).catch((err) => internal(err, callback)); 108 | }; 109 | -------------------------------------------------------------------------------- /calculator/client/index.js: -------------------------------------------------------------------------------- 1 | const grpc = require('@grpc/grpc-js'); 2 | const service = require('../proto/calculator_grpc_pb'); 3 | const {SumRequest} = require('../proto/sum_pb'); 4 | const {PrimeRequest} = require('../proto/primes_pb'); 5 | const {AvgRequest} = require('../proto/avg_pb'); 6 | const {MaxRequest} = require('../proto/max_pb'); 7 | const {SqrtRequest} = require('../proto/sqrt_pb'); 8 | const {DATA} = require('../../constants'); 9 | 10 | // eslint-disable-next-line no-unused-vars 11 | function doSum(client) { 12 | console.log('doSum was invoked'); 13 | const req = new SumRequest() 14 | .setFirstNumber(1) 15 | .setSecondNumber(1); 16 | 17 | client.sum(req, (err, res) => { 18 | if (err) { 19 | return console.log(err); 20 | } 21 | 22 | console.log(`Sum: ${res.getResult()}`); 23 | }); 24 | } 25 | 26 | // eslint-disable-next-line no-unused-vars 27 | function doPrimes(client) { 28 | console.log('doPrimes was invoked'); 29 | const req = new PrimeRequest(); 30 | 31 | req.setNumber(12390392840); 32 | const call = client.primes(req); 33 | 34 | call.on(DATA, (res) => { 35 | console.log(`Primes: ${res.getResult()}`); 36 | }); 37 | } 38 | 39 | // eslint-disable-next-line no-unused-vars 40 | function doAvg(client) { 41 | console.log('doAvg was invoked'); 42 | const numbers = [...Array(11).keys()].slice(1); 43 | const call = client.avg((err, res) => { 44 | if (err) { 45 | return console.error(err); 46 | } 47 | 48 | console.log(`Avg: ${res.getResult()}`); 49 | }); 50 | 51 | numbers.map((number) => { 52 | return new AvgRequest().setNumber(number); 53 | }).forEach((req) => call.write(req)); 54 | 55 | call.end(); 56 | } 57 | 58 | // eslint-disable-next-line no-unused-vars 59 | function doMax(client) { 60 | console.log('doMax was invoked'); 61 | const numbers = [4, 7, 2, 19, 4, 6, 32]; 62 | const call = client.max(); 63 | 64 | call.on(DATA, (res) => { 65 | console.log(`Max: ${res.getResult()}`); 66 | }); 67 | 68 | numbers.map((number) => { 69 | return new MaxRequest().setNumber(number); 70 | }).forEach((req) => call.write(req)); 71 | 72 | call.end(); 73 | } 74 | 75 | // eslint-disable-next-line no-unused-vars 76 | function doSqrt(client, n) { 77 | console.log('doSqrt was invoked'); 78 | const req = new SqrtRequest(); 79 | 80 | req.setNumber(n); 81 | client.sqrt(req, (err, res) => { 82 | if (err) { 83 | return console.log(err); 84 | } 85 | 86 | console.log(`Sqrt: ${res.getResult()}`); 87 | }); 88 | } 89 | 90 | function main() { 91 | const client = new service.CalculatorServiceClient( 92 | '0.0.0.0:50051', 93 | grpc.credentials.createInsecure(), 94 | ); 95 | 96 | // doSum(client); 97 | // doPrimes(client); 98 | // doAvg(client); 99 | // doMax(client); 100 | // doSqrt(client, 25); 101 | // doSqrt(client, -1); 102 | client.close(); 103 | } 104 | 105 | main(); 106 | -------------------------------------------------------------------------------- /calculator/proto/avg.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package calculator; 4 | 5 | message AvgRequest { 6 | int32 number = 1; 7 | } 8 | 9 | message AvgResponse { 10 | double result = 1; 11 | } -------------------------------------------------------------------------------- /calculator/proto/calculator.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package calculator; 4 | 5 | import "avg.proto"; 6 | import "max.proto"; 7 | import "primes.proto"; 8 | import "sqrt.proto"; 9 | import "sum.proto"; 10 | 11 | service CalculatorService { 12 | rpc Sum(SumRequest) returns (SumResponse); 13 | rpc Primes(PrimeRequest) returns (stream PrimeResponse); 14 | rpc Avg(stream AvgRequest) returns (AvgResponse); 15 | rpc Max(stream MaxRequest) returns (stream MaxResponse); 16 | rpc Sqrt(SqrtRequest) returns (SqrtResponse); 17 | } -------------------------------------------------------------------------------- /calculator/proto/max.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package calculator; 4 | 5 | message MaxRequest { 6 | int32 number = 1; 7 | } 8 | 9 | message MaxResponse { 10 | int32 result = 1; 11 | } -------------------------------------------------------------------------------- /calculator/proto/primes.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package calculator; 4 | 5 | message PrimeRequest { 6 | int64 number = 1; 7 | } 8 | 9 | message PrimeResponse { 10 | int64 result = 1; 11 | } -------------------------------------------------------------------------------- /calculator/proto/sqrt.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package calculator; 4 | 5 | message SqrtRequest { 6 | int32 number = 1; // could be uint32 number = 1; 7 | } 8 | 9 | message SqrtResponse { 10 | double result = 1; 11 | } -------------------------------------------------------------------------------- /calculator/proto/sum.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package calculator; 4 | 5 | message SumRequest { 6 | int32 first_number = 1; 7 | int32 second_number = 2; 8 | } 9 | 10 | message SumResponse { 11 | int32 result = 1; 12 | } -------------------------------------------------------------------------------- /calculator/server/index.js: -------------------------------------------------------------------------------- 1 | const grpc = require('@grpc/grpc-js'); 2 | const service = require('../proto/calculator_grpc_pb'); 3 | const serviceImpl = require('./service_impl'); 4 | 5 | const addr = '0.0.0.0:50051'; 6 | 7 | async function cleanup(server) { 8 | console.log('Cleanup'); 9 | 10 | if (server) { 11 | server.forceShutdown(); 12 | } 13 | } 14 | 15 | function main() { 16 | const server = new grpc.Server(); 17 | 18 | process.on('SIGINT', () => { 19 | console.log('Caught interrupt signal'); 20 | cleanup(server); 21 | }); 22 | 23 | const creds = grpc.ServerCredentials.createInsecure(); 24 | 25 | server.addService(service.CalculatorServiceService, serviceImpl); 26 | server.bindAsync(addr, creds, (err, _) => { 27 | if (err) { 28 | return cleanup(server); 29 | } 30 | 31 | server.start(); 32 | }); 33 | 34 | console.log('Listening on: ' + addr); 35 | } 36 | 37 | main(); 38 | -------------------------------------------------------------------------------- /calculator/server/service_impl.js: -------------------------------------------------------------------------------- 1 | const grpc = require('@grpc/grpc-js'); 2 | const {DATA, END} = require('../../constants'); 3 | const {SumResponse} = require('../proto/sum_pb'); 4 | const {PrimeResponse} = require('../proto/primes_pb'); 5 | const {AvgResponse} = require('../proto/avg_pb'); 6 | const {MaxResponse} = require('../proto/max_pb'); 7 | const {SqrtResponse} = require('../proto/sqrt_pb'); 8 | 9 | exports.sum = (call, callback) => { 10 | console.log('Sum was invoked'); 11 | const res = new SumResponse() 12 | .setResult( 13 | call.request.getFirstNumber() + call.request.getSecondNumber(), 14 | ); 15 | 16 | callback(null, res); 17 | }; 18 | 19 | exports.primes = (call, _) => { 20 | console.log('Primes was invoked'); 21 | const res = new PrimeResponse(); 22 | let number = call.request.getNumber(); 23 | let divisor = 2; 24 | 25 | while (number > 1) { 26 | if (number % divisor == 0) { 27 | res.setResult(divisor); 28 | call.write(res); 29 | number /= divisor; 30 | } else { 31 | ++divisor; 32 | } 33 | } 34 | 35 | call.end(); 36 | }; 37 | 38 | exports.avg = (call, callback) => { 39 | console.log('Avg was invoked'); 40 | let count = 0.0; 41 | let total = 0.0; 42 | 43 | call.on(DATA, (req) => { 44 | total += req.getNumber(); 45 | ++count; 46 | }); 47 | 48 | call.on(END, () => { 49 | const res = new AvgResponse() 50 | .setResult(total / count); 51 | 52 | callback(null, res); 53 | }); 54 | }; 55 | 56 | exports.max = (call, _) => { 57 | console.log('Max was invoked'); 58 | let max = 0; 59 | 60 | call.on(DATA, (req) => { 61 | console.log('Received request: ' + req); 62 | const number = req.getNumber(); 63 | 64 | if (number > max) { 65 | const res = new MaxResponse() 66 | .setResult(number); 67 | 68 | console.log('Sending response: ' + res); 69 | call.write(res); 70 | max = number; 71 | } 72 | }); 73 | 74 | call.on(END, () => call.end()); 75 | }; 76 | 77 | exports.sqrt = (call, callback) => { 78 | console.log('Sqrt was invoked'); 79 | const number = call.request.getNumber(); 80 | 81 | if (number < 0) { 82 | callback({ 83 | code: grpc.status.INVALID_ARGUMENT, 84 | message: `Number cannot be negative, received: ${number}`, 85 | }); 86 | } 87 | 88 | const res = new SqrtResponse() 89 | .setResult(Math.sqrt(number)); 90 | 91 | callback(null, res); 92 | }; 93 | -------------------------------------------------------------------------------- /constants.js: -------------------------------------------------------------------------------- 1 | exports.DATA = 'data'; 2 | exports.ERROR = 'error'; 3 | exports.END = 'end'; 4 | -------------------------------------------------------------------------------- /greet/client/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const grpc = require('@grpc/grpc-js'); 3 | const {GreetRequest} = require('../proto/greet_pb'); 4 | const {GreetServiceClient} = require('../proto/greet_grpc_pb'); 5 | const {DATA} = require('../../constants'); 6 | 7 | // eslint-disable-next-line no-unused-vars 8 | function doGreet(client) { 9 | console.log('doGreet was invoked'); 10 | const req = new GreetRequest() 11 | .setFirstName('Clement'); 12 | 13 | client.greet(req, (err, res) => { 14 | if (err) { 15 | return console.log(err); 16 | } 17 | 18 | console.log(`Greet: ${res.getResult()}`); 19 | }); 20 | } 21 | 22 | // eslint-disable-next-line no-unused-vars 23 | function doGreetManyTimes(client) { 24 | console.log('doGreetManyTimes was invoked'); 25 | const req = new GreetRequest() 26 | .setFirstName('Clement'); 27 | const call = client.greetManyTimes(req); 28 | 29 | call.on(DATA, (res) => { 30 | console.log(`GreetManyTimes: ${res.getResult()}`); 31 | }); 32 | } 33 | 34 | // eslint-disable-next-line no-unused-vars 35 | function doLongGreet(client) { 36 | console.log('doLongGreet was invoked'); 37 | const names = ['Clement', 'Marie', 'Test']; 38 | const call = client.longGreet((err, res) => { 39 | if (err) { 40 | return console.error(err); 41 | } 42 | 43 | console.log(`LongGreet: ${res.getResult()}`); 44 | }); 45 | 46 | names.map((name) => { 47 | return new GreetRequest().setFirstName(name); 48 | }).forEach((req) => call.write(req)); 49 | 50 | call.end(); 51 | } 52 | 53 | // eslint-disable-next-line no-unused-vars 54 | function doGreetEveryone(client) { 55 | console.log('doGreetEveryone was invoked'); 56 | const names = ['Clement', 'Marie', 'Test']; 57 | const call = client.greetEveryone(); 58 | 59 | call.on(DATA, (res) => { 60 | console.log(`GreetEveryone: ${res.getResult()}`); 61 | }); 62 | 63 | names.map((name) => { 64 | return new GreetRequest().setFirstName(name); 65 | }).forEach((req) => call.write(req)); 66 | 67 | call.end(); 68 | } 69 | 70 | // eslint-disable-next-line no-unused-vars 71 | function doGreetWithDeadline(client, ms) { 72 | console.log('doGreetWithDeadline was invoked'); 73 | const req = new GreetRequest() 74 | .setFirstName('Clement'); 75 | 76 | client.greetWithDeadline(req, { 77 | deadline: new Date(Date.now() + ms), 78 | }, (err, res) => { 79 | if (err) { 80 | return console.log(err); 81 | } 82 | 83 | console.log(`GreetWithDeadline: ${res.getResult()}`); 84 | }); 85 | } 86 | 87 | function main() { 88 | const tls = true; 89 | let creds; 90 | 91 | if (tls) { 92 | const rootCert = fs.readFileSync('./ssl/ca.crt'); 93 | 94 | creds = grpc.ChannelCredentials.createSsl(rootCert); 95 | } else { 96 | creds = grpc.ChannelCredentials.createInsecure(); 97 | } 98 | 99 | const client = new GreetServiceClient( 100 | 'localhost:50051', 101 | creds, 102 | ); 103 | 104 | doGreet(client); 105 | // doGreetManyTimes(client); 106 | // doLongGreet(client); 107 | // doGreetEveryone(client); 108 | // doGreetWithDeadline(client, 5000); 109 | // doGreetWithDeadline(client, 1000); 110 | client.close(); 111 | } 112 | 113 | main(); 114 | -------------------------------------------------------------------------------- /greet/proto/greet.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package greet; 4 | 5 | message GreetRequest { 6 | string first_name = 1; 7 | } 8 | 9 | message GreetResponse { 10 | string result = 1; 11 | } 12 | 13 | service GreetService { 14 | rpc Greet(GreetRequest) returns (GreetResponse); 15 | rpc GreetManyTimes(GreetRequest) returns (stream GreetResponse); 16 | rpc LongGreet(stream GreetRequest) returns (GreetResponse); 17 | rpc GreetEveryone(stream GreetRequest) returns (stream GreetResponse); 18 | rpc GreetWithDeadline(GreetRequest) returns (GreetResponse); 19 | } -------------------------------------------------------------------------------- /greet/server/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const grpc = require('@grpc/grpc-js'); 3 | const service = require('../proto/greet_grpc_pb'); 4 | const serviceImpl = require('./service_impl'); 5 | 6 | const addr = '0.0.0.0:50051'; 7 | 8 | function cleanup(server) { 9 | console.log('Cleanup'); 10 | 11 | if (server) { 12 | server.forceShutdown(); 13 | } 14 | } 15 | 16 | function main() { 17 | const server = new grpc.Server(); 18 | 19 | process.on('SIGINT', () => { 20 | console.log('Caught interrupt signal'); 21 | cleanup(server); 22 | }); 23 | 24 | const tls = true; 25 | let creds; 26 | 27 | if (tls) { 28 | const rootCert = fs.readFileSync('./ssl/ca.crt'); 29 | const certChain = fs.readFileSync('./ssl/server.crt'); 30 | const privateKey = fs.readFileSync('./ssl/server.pem'); 31 | 32 | creds = grpc.ServerCredentials.createSsl(rootCert, [{ 33 | cert_chain: certChain, 34 | private_key: privateKey, 35 | }]); 36 | } else { 37 | creds = grpc.ServerCredentials.createInsecure(); 38 | } 39 | 40 | server.addService(service.GreetServiceService, serviceImpl); 41 | server.bindAsync(addr, creds, (err, _) => { 42 | if (err) { 43 | return cleanup(server); 44 | } 45 | 46 | server.start(); 47 | }); 48 | 49 | console.log('Listening on: ' + addr); 50 | } 51 | 52 | main(); 53 | -------------------------------------------------------------------------------- /greet/server/service_impl.js: -------------------------------------------------------------------------------- 1 | const pb = require('../proto/greet_pb'); 2 | const {DATA, END} = require('../../constants'); 3 | 4 | exports.greet = (call, callback) => { 5 | console.log('Greet was invoked'); 6 | const res = new pb.GreetResponse() 7 | .setResult(`Hello ${call.request.getFirstName()}`); 8 | 9 | callback(null, res); 10 | }; 11 | 12 | exports.greetManyTimes = (call, _) => { 13 | console.log('GreetManyTimes was invoked'); 14 | const res = new pb.GreetResponse(); 15 | 16 | for (let i = 0; i < 10; ++i) { 17 | res.setResult(`Hello ${call.request.getFirstName()} - number ${i}`); 18 | call.write(res); 19 | } 20 | 21 | call.end(); 22 | }; 23 | 24 | exports.longGreet = (call, callback) => { 25 | console.log('LongGreet was invoked'); 26 | let greet = ''; 27 | 28 | call.on(DATA, (req) => { 29 | greet += `Hello ${req.getFirstName()}\n`; 30 | }); 31 | 32 | call.on(END, () => { 33 | const res = new pb.GreetResponse() 34 | .setResult(greet); 35 | 36 | callback(null, res); 37 | }); 38 | }; 39 | 40 | exports.greetEveryone = (call, _) => { 41 | console.log('GreetEveryone was invoked'); 42 | call.on(DATA, (req) => { 43 | console.log(`Received request: ${req}`); 44 | const res = new pb.GreetResponse() 45 | .setResult(`Hello ${req.getFirstName()}`); 46 | 47 | console.log(`Sending response: ${res}`); 48 | call.write(res); 49 | }); 50 | 51 | call.on(END, () => call.end()); 52 | }; 53 | 54 | const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); 55 | 56 | exports.greetWithDeadline = async (call, callback) => { 57 | console.log('GreetWithDeadline was invoked'); 58 | 59 | for (let i = 0; i < 3; ++i) { 60 | if (call.cancelled) { 61 | return console.log('The client cancelled the request!'); 62 | } 63 | await sleep(1000); 64 | } 65 | 66 | const res = new pb.GreetResponse() 67 | .setResult(`Hello ${call.request.getFirstName()}`); 68 | 69 | callback(null, res); 70 | }; 71 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grpc-node-js-course", 3 | "version": "1.0.0", 4 | "description": "", 5 | "type": "commonjs", 6 | "scripts": { 7 | "pb:gen": "./scripts/gen.sh greet calculator blog", 8 | "pb:win:gen": "powershell -ExecutionPolicy unrestricted ./scripts/gen.ps1 greet calculator blog", 9 | 10 | "greet:server": "node greet/server/index.js", 11 | "greet:client": "node greet/client/index.js", 12 | "greet:pb:gen": "./scripts/gen.sh greet", 13 | "greet:pb:win:gen": "powershell -ExecutionPolicy unrestricted ./scripts/gen.ps1 greet", 14 | 15 | "calculator:server": "node calculator/server/index.js", 16 | "calculator:client": "node calculator/client/index.js", 17 | "calculator:pb:gen": "./scripts/gen.sh calculator", 18 | "calculator:pb:win:gen": "powershell -ExecutionPolicy unrestricted ./scripts/gen.ps1 calculator", 19 | 20 | "blog:db": "cd blog; docker-compose up; cd ..", 21 | "blog:server": "node blog/server/index.js", 22 | "blog:client": "node blog/client/index.js", 23 | "blog:pb:gen": "./scripts/gen.sh blog", 24 | "blog:pb:win:gen": "powershell -ExecutionPolicy unrestricted ./scripts/gen.ps1 blog", 25 | 26 | "ssl:gen": "cd ssl && ../scripts/ssl.sh && cd ..", 27 | "ssl:win:gen": "cd ssl && powershell -ExecutionPolicy unrestricted ../scripts/ssl.ps1 && cd ..", 28 | 29 | "clean": "find . -type f -name '*_pb.js' -not -path './node_modules/**/*_pb.js' -delete && rm ssl/*.crt ssl/*.key ssl/*.csr ssl/*.pem", 30 | "clean:win": "powershell \"Get-ChildItem -Filter *_pb.js -Recurse $pwd | Foreach-Object { if ($_.FullName -inotmatch 'node_modules') { rm $_.FullName } }\" && powershell Remove-Item ssl/*.crt, ssl/*.key, ssl/*.csr, ssl/*.pem", 31 | "bump": "ncu -u && npm install", 32 | "lint": "npx eslint --fix ./", 33 | "test": "nyc mocha --recursive --timeout 60000" 34 | }, 35 | "repository": { 36 | "type": "git", 37 | "url": "github.com/Clement-Jean/grpc-node-course" 38 | }, 39 | "author": "Clement Jean", 40 | "license": "ISC", 41 | "dependencies": { 42 | "@grpc/grpc-js": "^1.10.8", 43 | "google-protobuf": "^3.21.2", 44 | "mongodb": "6.7" 45 | }, 46 | "devDependencies": { 47 | "eslint": "^9.3.0", 48 | "eslint-config-google": "^0.14.0", 49 | "grpc-tools": "^1.12.4", 50 | "mocha": "^10.4.0", 51 | "mongodb-memory-server": "^9.3.0", 52 | "nyc": "^15.1.0" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /scripts/README.md: -------------------------------------------------------------------------------- 1 | # SSL 2 | 3 | ## OpenSSL 4 | 5 | Check if you already have OpenSSL installed: 6 | 7 | ```shell 8 | openssl version 9 | ``` 10 | 11 | ### ⚠️ If error 12 | 13 | #### `Windows - Chocolatey` 14 | 15 | ```shell 16 | choco install openssl 17 | ``` 18 | 19 | #### `Otherwise` 20 | 21 | Please follow instructions [here](https://github.com/openssl/openssl) to install it. 22 | 23 | ## Run 24 | 25 | ### `Linux/MacOS` 26 | 27 | ```shell 28 | chmod +x ssl.sh 29 | ./ssl.sh 30 | ``` 31 | 32 | ### `Windows - Powershell` 33 | 34 | ```powershell 35 | .\ssl.ps1 36 | ``` 37 | 38 | or, if you have a `SecurityError`: 39 | 40 | ```powershell 41 | powershell -ExecutionPolicy unrestricted .\ssl.ps1 42 | ``` 43 | -------------------------------------------------------------------------------- /scripts/gen.ps1: -------------------------------------------------------------------------------- 1 | foreach ( $project in $args ) { 2 | # Generate gRPC and Protobuf code for ${PROJECT}/${PROJECT}.proto 3 | # (eg: greet/greet.proto) 4 | ./node_modules/.bin/grpc_tools_node_protoc -I $project/proto ` 5 | --js_out=import_style=commonjs:$project/proto ` 6 | --grpc_out=grpc_js:$project/proto ` 7 | $project/proto/$project.proto; 8 | 9 | # Generate only Protobuf code for all the other .proto files (if any) 10 | # (eg: calculator/sum.proto) 11 | Get-ChildItem -Filter *.proto -Recurse $project/proto -Exclude $project.proto | ForEach-Object { ` 12 | $file = "./$project/proto/" + $_.Name; ` 13 | ./node_modules/.bin/grpc_tools_node_protoc -I $project/proto ` 14 | --js_out=import_style=commonjs:$project/proto ` 15 | $file ` 16 | } 17 | } -------------------------------------------------------------------------------- /scripts/gen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # USAGE: gen.sh PATH(s) 3 | # PATH: project path which contains a proto directory and 4 | # a .proto named after the project (eg: blog, blog.proto) 5 | # and optionally some other .proto files 6 | 7 | argc=$# 8 | argv=("$@") 9 | 10 | for (( j = 0; j < argc; ++j )); do 11 | # Generate gRPC and Protobuf code for ${PROJECT}/${PROJECT}.proto 12 | # (eg: greet/greet.proto) 13 | ./node_modules/.bin/grpc_tools_node_protoc -I ${argv[j]}/proto/ \ 14 | --js_out=import_style=commonjs:${argv[j]}/proto/ \ 15 | --grpc_out=grpc_js:${argv[j]}/proto/ \ 16 | ${argv[j]}/proto/${argv[j]}.proto; 17 | 18 | # Generate only Protobuf code for all the other .proto files (if any) 19 | # (eg: calculator/sum.proto) 20 | ./node_modules/.bin/grpc_tools_node_protoc -I ${argv[j]}/proto/ \ 21 | --js_out=import_style=commonjs:${argv[j]}/proto/ \ 22 | $(find ${argv[j]}/proto/ -type f -name "*.proto" -not -path "${argv[j]}/proto/${argv[j]}.proto") 23 | done -------------------------------------------------------------------------------- /scripts/ssl.ps1: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env pwsh 2 | # Output files 3 | # ca.key: Certificate Authority private key file (this shouldn't be shared in real-life) 4 | # ca.crt: Certificate Authority trust certificate (this should be shared with users in real-life) 5 | # server.key: Server private key, password protected (this shouldn't be shared) 6 | # server.csr: Server certificate signing request (this should be shared with the CA owner) 7 | # server.crt: Server certificate signed by the CA (this would be sent back by the CA owner) - keep on server 8 | # server.pem: Conversion of server.key into a format gRPC likes (this shouldn't be shared) 9 | 10 | # Summary 11 | # Private files: ca.key, server.key, server.pem, server.crt 12 | # "Share" files: ca.crt (needed by the client), server.csr (needed by the CA) 13 | 14 | # Changes these CN's to match your hosts in your environment if needed. 15 | $SERVER_CN="localhost" 16 | $MY_SUBJECT="/CN=$SERVER_CN" 17 | 18 | # Step 1: Generate Certificate Authority + Trust Certificate (ca.crt) 19 | openssl genrsa -passout pass:1111 -des3 -out ca.key 4096 20 | openssl req -passin pass:1111 -new -x509 -days 365 -key ca.key -out ca.crt -subj "/CN=ca" 21 | 22 | # Step 2: Generate the Server Private Key (server.key) 23 | openssl genrsa -passout pass:1111 -des3 -out server.key 4096 24 | 25 | # Step 3: Get a certificate signing request from the CA (server.csr) 26 | openssl req -passin pass:1111 -new -key server.key -out server.csr -subj $MY_SUBJECT 27 | 28 | # Step 4: Sign the certificate with the CA we created (it's called self signing) - server.crt 29 | openssl x509 -req -extfile ../scripts/ssl.ps1.conf -passin pass:1111 -days 365 -in server.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out server.crt 30 | 31 | # Step 5: Convert the server certificate to .pem format (server.pem) - usable by gRPC 32 | openssl pkcs8 -topk8 -nocrypt -passin pass:1111 -in server.key -out server.pem -------------------------------------------------------------------------------- /scripts/ssl.ps1.conf: -------------------------------------------------------------------------------- 1 | subjectAltName=DNS:localhost -------------------------------------------------------------------------------- /scripts/ssl.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Output files 3 | # ca.key: Certificate Authority private key file (this shouldn't be shared in real-life) 4 | # ca.crt: Certificate Authority trust certificate (this should be shared with users in real-life) 5 | # server.key: Server private key, password protected (this shouldn't be shared) 6 | # server.csr: Server certificate signing request (this should be shared with the CA owner) 7 | # server.crt: Server certificate signed by the CA (this would be sent back by the CA owner) - keep on server 8 | # server.pem: Conversion of server.key into a format gRPC likes (this shouldn't be shared) 9 | 10 | # Summary 11 | # Private files: ca.key, server.key, server.pem, server.crt 12 | # "Share" files: ca.crt (needed by the client), server.csr (needed by the CA) 13 | 14 | # Changes these CN's to match your hosts in your environment if needed. 15 | SERVER_CN=localhost 16 | MY_SUBJECT="/CN=${SERVER_CN}" 17 | 18 | # Step 1: Generate Certificate Authority + Trust Certificate (ca.crt) 19 | openssl genrsa -passout pass:1111 -des3 -out ca.key 4096 20 | openssl req -passin pass:1111 -new -x509 -days 365 -key ca.key -out ca.crt -subj "/CN=ca" 21 | 22 | # Step 2: Generate the Server Private Key (server.key) 23 | openssl genrsa -passout pass:1111 -des3 -out server.key 4096 24 | 25 | # Step 3: Get a certificate signing request from the CA (server.csr) 26 | openssl req -passin pass:1111 -new -key server.key -out server.csr -subj ${MY_SUBJECT} 27 | 28 | # Step 4: Sign the certificate with the CA we created (it's called self signing) - server.crt 29 | openssl x509 -req -extfile <(printf "subjectAltName=DNS:${SERVER_CN}") -passin pass:1111 -days 365 -in server.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out server.crt 30 | 31 | # Step 5: Convert the server certificate to .pem format (server.pem) - usable by gRPC 32 | openssl pkcs8 -topk8 -nocrypt -passin pass:1111 -in server.key -out server.pem -------------------------------------------------------------------------------- /ssl/README.md: -------------------------------------------------------------------------------- 1 | Where the certs live -------------------------------------------------------------------------------- /test/blog/create_test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const {Blog} = require('../../blog/proto/blog_pb'); 3 | const impl = require('../../blog/server/service_impl'); 4 | const service = require('../../blog/proto/blog_grpc_pb'); 5 | const {TestGrpcHelper} = require('../grpc_helper'); 6 | const {TestDbHelper} = require('./db_helper'); 7 | 8 | describe('Blog Server - Create', () => { 9 | const grpcHelper = new TestGrpcHelper(); 10 | const dbHelper = new TestDbHelper(); 11 | 12 | before(async () => { 13 | await dbHelper.start(); 14 | grpcHelper.start( 15 | service.BlogServiceClient, 16 | service.BlogServiceService, 17 | impl, 18 | ); 19 | }); 20 | 21 | after(async () => { 22 | await dbHelper.stop(); 23 | grpcHelper.stop(); 24 | }); 25 | 26 | it('CreateBlog', (done) => { 27 | const req = new Blog() 28 | .setAuthorId('Clement') 29 | .setTitle('Blog') 30 | .setContent('Content'); 31 | 32 | grpcHelper.client.createBlog(req, (err, res) => { 33 | assert.ifError(err); 34 | assert.notDeepEqual(res.getId(), ''); 35 | done(); 36 | }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /test/blog/db_helper.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const {MongoMemoryServer} = require('mongodb-memory-server'); 3 | const {MongoClient} = require('mongodb'); 4 | 5 | global.collection = undefined; 6 | 7 | exports.TestDbHelper = class { 8 | constructor() { 9 | this.mongoServer = null; 10 | this.connection = null; 11 | } 12 | 13 | async start() { 14 | this.mongoServer = await MongoMemoryServer.create(); 15 | const mongoUri = this.mongoServer.getUri(); 16 | 17 | this.connection = await MongoClient.connect(mongoUri); 18 | 19 | const dbName = this.mongoServer.instanceInfo.dbName; 20 | const db = this.connection.db(dbName); 21 | 22 | collection = db.collection('blog'); 23 | } 24 | 25 | async stop() { 26 | await collection.drop(); 27 | await this.connection.close(); 28 | await this.mongoServer.stop(); 29 | } 30 | 31 | async insertDoc(document) { 32 | const res = await collection.insertOne(document); 33 | 34 | assert.notStrictEqual(res, null); 35 | assert.notStrictEqual(res.acknowledged, false); 36 | return res; 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /test/blog/delete_test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const grpc = require('@grpc/grpc-js'); 3 | const {BlogId} = require('../../blog/proto/blog_pb'); 4 | const service = require('../../blog/proto/blog_grpc_pb'); 5 | const impl = require('../../blog/server/service_impl'); 6 | const {TestGrpcHelper} = require('../grpc_helper'); 7 | const {TestDbHelper} = require('./db_helper'); 8 | 9 | describe('Blog Server - Delete', () => { 10 | const grpcHelper = new TestGrpcHelper(); 11 | const dbHelper = new TestDbHelper(); 12 | 13 | before(async () => { 14 | await dbHelper.start(); 15 | grpcHelper.start( 16 | service.BlogServiceClient, 17 | service.BlogServiceService, 18 | impl, 19 | ); 20 | }); 21 | 22 | after(async () => { 23 | await dbHelper.stop(); 24 | grpcHelper.stop(); 25 | }); 26 | 27 | it('DeleteBlog', (done) => { 28 | dbHelper.insertDoc({ 29 | author_id: 'Clement', 30 | title: 'Title', 31 | content: 'Content', 32 | }).then((res) => { 33 | const id = res.insertedId.toString(); 34 | const req = new BlogId() 35 | .setId(id); 36 | 37 | grpcHelper.client.deleteBlog(req, async (err, _) => { 38 | assert.ifError(err); 39 | await collection.deleteOne({_id: id}); 40 | done(); 41 | }); 42 | }).catch(assert.ifError); 43 | }); 44 | 45 | it('DeleteBlog invalid ID', (done) => { 46 | const req = new BlogId(); 47 | 48 | grpcHelper.client.deleteBlog(req, (err, _) => { 49 | assert.deepStrictEqual(err.code, grpc.status.INVALID_ARGUMENT); 50 | done(); 51 | }); 52 | }); 53 | 54 | it('DeleteBlog not found', (done) => { 55 | const req = new BlogId() 56 | .setId('624d731f9920e5a155d5338b'); 57 | 58 | grpcHelper.client.deleteBlog(req, (err, _) => { 59 | assert.deepStrictEqual(err.code, grpc.status.NOT_FOUND); 60 | done(); 61 | }); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /test/blog/list_test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const impl = require('../../blog/server/service_impl'); 3 | const service = require('../../blog/proto/blog_grpc_pb'); 4 | const {TestGrpcHelper} = require('../grpc_helper'); 5 | const {TestDbHelper} = require('./db_helper'); 6 | const {Empty} = require('google-protobuf/google/protobuf/empty_pb'); 7 | const {DATA, ERROR, END} = require('../../constants'); 8 | 9 | describe('Blog Server - List', () => { 10 | const grpcHelper = new TestGrpcHelper(); 11 | const dbHelper = new TestDbHelper(); 12 | 13 | before(async () => { 14 | await dbHelper.start(); 15 | grpcHelper.start( 16 | service.BlogServiceClient, 17 | service.BlogServiceService, 18 | impl, 19 | ); 20 | }); 21 | 22 | after(async () => { 23 | await dbHelper.stop(); 24 | grpcHelper.stop(); 25 | }); 26 | 27 | it('ListBlog', (done) => { 28 | const blogs = [ 29 | { 30 | author_id: 'Clement', 31 | title: 'Title', 32 | content: 'Content', 33 | }, 34 | { 35 | author_id: 'Marie', 36 | title: 'Title', 37 | content: 'Content', 38 | }, 39 | ]; 40 | 41 | blogs.forEach(async (blog) => await dbHelper.insertDoc(blog)); 42 | 43 | let i = 0; 44 | const call = grpcHelper.client.listBlogs(new Empty()); 45 | 46 | call.on(ERROR, assert.ifError); 47 | call.on(DATA, () => ++i); 48 | call.on(END, async () => { 49 | assert.strictEqual(i, blogs.length); 50 | await collection.deleteMany({}); 51 | done(); 52 | }); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /test/blog/read_test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const grpc = require('@grpc/grpc-js'); 3 | const {BlogId} = require('../../blog/proto/blog_pb'); 4 | const impl = require('../../blog/server/service_impl'); 5 | const service = require('../../blog/proto/blog_grpc_pb'); 6 | const {TestGrpcHelper} = require('../grpc_helper'); 7 | const {TestDbHelper} = require('./db_helper'); 8 | 9 | describe('Blog Server - Read', () => { 10 | const grpcHelper = new TestGrpcHelper(); 11 | const dbHelper = new TestDbHelper(); 12 | 13 | before(async () => { 14 | await dbHelper.start(); 15 | grpcHelper.start( 16 | service.BlogServiceClient, 17 | service.BlogServiceService, 18 | impl, 19 | ); 20 | }); 21 | 22 | after(async () => { 23 | await dbHelper.stop(); 24 | grpcHelper.stop(); 25 | }); 26 | 27 | it('ReadBlog with invalid ID', (done) => { 28 | const req = new BlogId(); 29 | 30 | grpcHelper.client.readBlog(req, (err, _) => { 31 | assert.deepStrictEqual(err.code, grpc.status.INVALID_ARGUMENT); 32 | done(); 33 | }); 34 | }); 35 | 36 | it('ReadBlog with valid ID', (done) => { 37 | dbHelper.insertDoc({ 38 | author_id: 'Clement', 39 | title: 'Title', 40 | content: 'Content', 41 | }).then((res) => { 42 | const id = res.insertedId.toString(); 43 | const req = new BlogId().setId(id); 44 | 45 | grpcHelper.client.readBlog(req, async (err, res) => { 46 | assert.ifError(err); 47 | assert.deepStrictEqual(res.getId(), id); 48 | await collection.deleteOne({_id: id}); 49 | done(); 50 | }); 51 | }).catch(assert.ifError); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /test/blog/update_test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const grpc = require('@grpc/grpc-js'); 3 | const {Blog} = require('../../blog/proto/blog_pb'); 4 | const service = require('../../blog/proto/blog_grpc_pb'); 5 | const impl = require('../../blog/server/service_impl'); 6 | const {TestGrpcHelper} = require('../grpc_helper'); 7 | const {TestDbHelper} = require('./db_helper'); 8 | 9 | describe('Blog Server - Update', () => { 10 | const grpcHelper = new TestGrpcHelper(); 11 | const dbHelper = new TestDbHelper(); 12 | 13 | before(async () => { 14 | await dbHelper.start(); 15 | grpcHelper.start( 16 | service.BlogServiceClient, 17 | service.BlogServiceService, 18 | impl, 19 | ); 20 | }); 21 | 22 | after(async () => { 23 | await dbHelper.stop(); 24 | grpcHelper.stop(); 25 | }); 26 | 27 | it('UpdateBlog', (done) => { 28 | dbHelper.insertDoc({ 29 | author_id: 'Clement', 30 | title: 'Title', 31 | content: 'Content', 32 | }).then((res) => { 33 | const id = res.insertedId.toString(); 34 | const req = new Blog() 35 | .setId(id) 36 | .setAuthorId('not Clement') 37 | .setTitle('not Blog') 38 | .setContent('not Content'); 39 | 40 | grpcHelper.client.updateBlog(req, async (err, _) => { 41 | assert.ifError(err); 42 | await collection.deleteOne({_id: id}); 43 | done(); 44 | }); 45 | }).catch(assert.ifError); 46 | }); 47 | 48 | it('UpdateBlog invalid ID', (done) => { 49 | const req = new Blog() 50 | .setAuthorId('not Clement') 51 | .setTitle('not Blog') 52 | .setContent('not Content'); 53 | 54 | grpcHelper.client.updateBlog(req, (err, _) => { 55 | assert.deepStrictEqual(err.code, grpc.status.INVALID_ARGUMENT); 56 | done(); 57 | }); 58 | }); 59 | 60 | it('UpdateBlog not found', (done) => { 61 | const req = new Blog() 62 | .setId('624d731f9920e5a155d5338b') 63 | .setAuthorId('not Clement') 64 | .setTitle('not Blog') 65 | .setContent('not Content'); 66 | 67 | grpcHelper.client.updateBlog(req, (err, _) => { 68 | assert.deepStrictEqual(err.code, grpc.status.NOT_FOUND); 69 | done(); 70 | }); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /test/calculator/calculator_test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const grpc = require('@grpc/grpc-js'); 3 | const impl = require('../../calculator/server/service_impl'); 4 | const service = require('../../calculator/proto/calculator_grpc_pb'); 5 | const {SumRequest} = require('../../calculator/proto/sum_pb'); 6 | const {PrimeRequest} = require('../../calculator/proto/primes_pb'); 7 | const {AvgRequest} = require('../../calculator/proto/avg_pb'); 8 | const {MaxRequest} = require('../../calculator/proto/max_pb'); 9 | const {SqrtRequest} = require('../../calculator/proto/sqrt_pb'); 10 | const {TestGrpcHelper} = require('../grpc_helper'); 11 | 12 | describe('Calulator Server', () => { 13 | const grpcHelper = new TestGrpcHelper(); 14 | 15 | before(() => { 16 | grpcHelper.start( 17 | service.CalculatorServiceClient, 18 | service.CalculatorServiceService, 19 | impl, 20 | ); 21 | }); 22 | 23 | after(() => { 24 | grpcHelper.stop(); 25 | }); 26 | 27 | it('Sum', (done) => { 28 | const req = new SumRequest(); 29 | 30 | req.setFirstNumber(1); 31 | req.setSecondNumber(1); 32 | grpcHelper.client.sum(req, (err, res) => { 33 | assert.ifError(err); 34 | assert.strictEqual(res.getResult(), 2); 35 | done(); 36 | }); 37 | }); 38 | 39 | it('Primes', (done) => { 40 | const req = new PrimeRequest(); 41 | 42 | req.setNumber(567890); 43 | 44 | const call = grpcHelper.client.primes(req); 45 | const responses = []; 46 | 47 | call.on('data', (res) => { 48 | responses.push(res.getResult()); 49 | }); 50 | 51 | call.on('error', assert.ifError); 52 | 53 | call.on('end', () => { 54 | assert.deepStrictEqual(4, responses.length); 55 | const expected = [2, 5, 109, 521]; 56 | 57 | for (let i = 0; i < responses.length; ++i) { 58 | assert(expected.includes(responses[i])) 59 | } 60 | done(); 61 | }); 62 | }); 63 | 64 | it('Avg', (done) => { 65 | const names = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; 66 | const call = grpcHelper.client.avg((err, res) => { 67 | assert.ifError(err); 68 | assert.deepStrictEqual( 69 | 5.5, 70 | res.getResult(), 71 | ); 72 | done(); 73 | }); 74 | 75 | names.map((number) => { 76 | return new AvgRequest().setNumber(number); 77 | }).forEach((req) => call.write(req)); 78 | 79 | call.end(); 80 | }); 81 | 82 | it('Max', (done) => { 83 | const numbers = [4, 7, 2, 19, 4, 6, 32]; 84 | const call = grpcHelper.client.max(); 85 | let max = numbers[0]; 86 | 87 | call.on('data', (res) => { 88 | max = res.getResult(); 89 | }); 90 | 91 | call.on('error', assert.ifError); 92 | 93 | call.on('end', () => { 94 | assert.strictEqual(max, 32); 95 | done(); 96 | }); 97 | 98 | numbers.map((number) => { 99 | return new MaxRequest().setNumber(number); 100 | }).forEach((req) => call.write(req)); 101 | 102 | call.end(); 103 | }); 104 | 105 | it('Sqrt negative number', (done) => { 106 | const req = new SqrtRequest(); 107 | 108 | req.setNumber(-11); 109 | grpcHelper.client.sqrt(req, (err, _) => { 110 | assert(err); 111 | assert.strictEqual(err.code, grpc.status.INVALID_ARGUMENT); 112 | assert.strictEqual( 113 | err.details, 114 | 'Number cannot be negative, received: -11', 115 | ); 116 | done(); 117 | }); 118 | }); 119 | }); 120 | -------------------------------------------------------------------------------- /test/greet/greet_test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const grpc = require('@grpc/grpc-js'); 3 | const impl = require('../../greet/server/service_impl'); 4 | const service = require('../../greet/proto/greet_grpc_pb'); 5 | const {GreetRequest} = require('../../greet/proto/greet_pb'); 6 | const {TestGrpcHelper} = require('../grpc_helper'); 7 | 8 | describe('Greet Server', () => { 9 | const grpcHelper = new TestGrpcHelper(); 10 | 11 | before(() => { 12 | grpcHelper.start( 13 | service.GreetServiceClient, 14 | service.GreetServiceService, 15 | impl, 16 | ); 17 | }); 18 | 19 | after(() => { 20 | grpcHelper.stop(); 21 | }); 22 | 23 | it('Greet', (done) => { 24 | const req = new GreetRequest(); 25 | 26 | req.setFirstName('Clement'); 27 | grpcHelper.client.greet(req, (err, res) => { 28 | assert.ifError(err); 29 | assert.strictEqual(res.getResult(), 'Hello Clement'); 30 | done(); 31 | }); 32 | }); 33 | 34 | it('GreetManyTimes', (done) => { 35 | const name = 'Clement'; 36 | const req = new GreetRequest(); 37 | 38 | req.setFirstName(name); 39 | 40 | const call = grpcHelper.client.greetManyTimes(req); 41 | const responses = []; 42 | 43 | call.on('data', (res) => { 44 | responses.push(res.getResult()); 45 | }); 46 | 47 | call.on('error', assert.ifError); 48 | 49 | call.on('end', () => { 50 | assert.deepStrictEqual(10, responses.length); 51 | 52 | const expected = [ 53 | `Hello ${name} - number 0`, 54 | `Hello ${name} - number 1`, 55 | `Hello ${name} - number 2`, 56 | `Hello ${name} - number 3`, 57 | `Hello ${name} - number 4`, 58 | `Hello ${name} - number 5`, 59 | `Hello ${name} - number 6`, 60 | `Hello ${name} - number 7`, 61 | `Hello ${name} - number 8`, 62 | `Hello ${name} - number 9` 63 | ] 64 | for (let i = 0; i < 10; ++i) { 65 | assert(expected.includes(responses[i])) 66 | } 67 | done(); 68 | }); 69 | }); 70 | 71 | it('LongGreet', (done) => { 72 | const names = ['Clement', 'Marie', 'Test']; 73 | const call = grpcHelper.client.longGreet((err, res) => { 74 | assert.ifError(err); 75 | assert.deepStrictEqual( 76 | names.map((name) => `Hello ${name}\n`).join(''), 77 | res.getResult(), 78 | ); 79 | done(); 80 | }); 81 | 82 | names.map((name) => { 83 | return new GreetRequest().setFirstName(name); 84 | }).forEach((req) => call.write(req)); 85 | 86 | call.end(); 87 | }); 88 | 89 | it('GreetEveryone', (done) => { 90 | let i = 0; 91 | const names = ['Clement', 'Marie', 'Test']; 92 | const call = grpcHelper.client.greetEveryone(); 93 | 94 | call.on('data', (res) => { 95 | assert.deepStrictEqual(`Hello ${names[i]}`, res.getResult()); 96 | ++i; 97 | }); 98 | 99 | call.on('error', assert.ifError); 100 | 101 | call.on('end', () => { 102 | assert.strictEqual(i, names.length); 103 | done(); 104 | }); 105 | 106 | names.map((name) => { 107 | return new GreetRequest().setFirstName(name); 108 | }).forEach((req) => call.write(req)); 109 | 110 | call.end(); 111 | }); 112 | 113 | it('GreetWithDeadline exceeded', (done) => { 114 | const req = new GreetRequest(); 115 | 116 | req.setFirstName('Clement'); 117 | grpcHelper.client.greetWithDeadline(req, { 118 | deadline: new Date(Date.now() + 1000), 119 | }, (err, _) => { 120 | assert(err); 121 | assert.strictEqual(err.code, grpc.status.DEADLINE_EXCEEDED); 122 | assert(err.details.startsWith('Deadline exceeded')); 123 | done(); 124 | }); 125 | }); 126 | }); 127 | -------------------------------------------------------------------------------- /test/grpc_helper.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const grpc = require('@grpc/grpc-js'); 3 | 4 | exports.TestGrpcHelper = class { 5 | constructor() { 6 | this.server = null; 7 | this.client = null; 8 | } 9 | 10 | start(ClientType, serviceType, serviceImpl) { 11 | this.server = new grpc.Server(); 12 | this.server.addService(serviceType, serviceImpl); 13 | this.server.bindAsync( 14 | '0.0.0.0:0', 15 | grpc.ServerCredentials.createInsecure(), 16 | (err, port) => { 17 | assert.ifError(err); 18 | this.client = new ClientType( 19 | `0.0.0.0:${port}`, 20 | grpc.credentials.createInsecure(), 21 | ); 22 | // this.server.start(); 23 | }, 24 | ); 25 | } 26 | 27 | stop() { 28 | this.client.close(); 29 | this.server.forceShutdown(); 30 | } 31 | }; 32 | --------------------------------------------------------------------------------