├── .dockerignore ├── .gitignore ├── .travis.yml ├── Dockerfile ├── LICENSE ├── README.md ├── cli.js ├── index.js ├── package-lock.json ├── package.json └── test ├── google └── api │ └── annotations.proto ├── index.spec.js ├── test.proto └── test_implementation.js /.dockerignore: -------------------------------------------------------------------------------- 1 | *.log 2 | node_modules 3 | .DS_Store 4 | example 5 | coverage 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | 3 | jobs: 4 | include: 5 | - stage: build docker image 6 | script: 7 | - echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin 8 | - docker build -t grpc-dynamic-gateway . 9 | - docker images 10 | - docker tag grpc-dynamic-gateway $DOCKER_USERNAME/grpc-dynamic-gateway 11 | - docker push $DOCKER_USERNAME/grpc-dynamic-gateway 12 | - stage: test 13 | script: docker run --rm -it --entrypoint=/usr/local/bin/npm konsumer/grpc-dynamic-gateway test -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:boron 2 | 3 | WORKDIR /usr/src/app 4 | COPY . /usr/src/app 5 | RUN npm install 6 | RUN git clone https://github.com/googleapis/googleapis.git /tmp/proto && mv /tmp/proto/google / 7 | 8 | EXPOSE 8080 9 | VOLUME /api.proto 10 | 11 | ENTRYPOINT ["node", "/usr/src/app/cli.js"] 12 | CMD ["/api.proto"] 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 David Konsumer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # grpc-dynamic-gateway 2 | 3 | [![NPM](https://nodei.co/npm/grpc-dynamic-gateway.png?compact=true)](https://nodei.co/npm/grpc-dynamic-gateway/) 4 | 5 | This will allow you to provide a REST-like JSON interface for your gRPC protobuf interface. [grpc-gateway](https://github.com/grpc-ecosystem/grpc-gateway) requires you to genrate a static version of your interface in go, then compile it. This will allow you to run a JSON proxy for your grpc server without generating/compiling. 6 | 7 | * Install with `npm i -g grpc-dynamic-gateway` 8 | * Start with `grpc-dynamic-gateway DEFINITION.proto` 9 | 10 | You can see an example project [here](https://github.com/konsumer/grpcnode/tree/master/example) that shows how to use all the CLI tools, with no code other than your endpoint implementation. 11 | 12 | 13 | # cli 14 | 15 | ``` 16 | Usage: grpc-dynamic-gateway [options] DEFINITION.proto [DEFINITION2.proto...] 17 | 18 | Options: 19 | -?, --help, -h Show help [boolean] 20 | --port, -p The port to serve your JSON proxy on [default: 8080] 21 | --grpc, -g The host & port to connect to, where your gprc-server is 22 | running [default: "localhost:50051"] 23 | -I, --include Path to resolve imports from. 24 | Support multi include path, but you have to put the proto files 25 | root in first include. 26 | --ca SSL CA cert for gRPC 27 | --key SSL client key for gRPC 28 | --cert SSL client certificate for gRPC 29 | --mountpoint, -m URL to mount server on [default: "/"] 30 | --quiet, -q Suppress logs [boolean] 31 | ``` 32 | 33 | # in code 34 | 35 | You can use it in your code, too, as express/connect/etc middleware. 36 | 37 | `npm i -S grpc-dynamic-gateway` 38 | 39 | ```js 40 | const grpcGateway = require('grpc-dynamic-gateway') 41 | const express = require('express') 42 | const bodyParser = require('body-parser') 43 | 44 | const app = express() 45 | app.use(bodyParser.json()) 46 | app.use(bodyParser.urlencoded({ extended: false })) 47 | 48 | // load the proxy on / URL 49 | app.use('/', grpcGateway(['api.proto'], '0.0.0.0:5051')) 50 | 51 | const port = process.env.PORT || 8080 52 | app.listen(port, () => { 53 | console.log(`Listening on http://0.0.0.0:${port}`) 54 | }) 55 | ``` 56 | 57 | # ssl 58 | 59 | With SSL, you will need the Cert Authority certificate, client & server signed certificate and keys. 60 | 61 | 62 | I generated/signed my demo keys like this: 63 | 64 | ``` 65 | openssl genrsa -passout pass:1111 -des3 -out ca.key 4096 66 | openssl req -passin pass:1111 -new -x509 -days 365 -key ca.key -out ca.crt -subj "/C=US/ST=Oregon/L=Portland/O=Test/OU=CertAuthority/CN=localhost" 67 | openssl genrsa -passout pass:1111 -des3 -out server.key 4096 68 | openssl req -passin pass:1111 -new -key server.key -out server.csr -subj "/C=US/ST=Oregon/L=Portland/O=Test/OU=Server/CN=localhost" 69 | openssl x509 -req -passin pass:1111 -days 365 -in server.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out server.crt 70 | openssl rsa -passin pass:1111 -in server.key -out server.key 71 | openssl genrsa -passout pass:1111 -des3 -out client.key 4096 72 | openssl req -passin pass:1111 -new -key client.key -out client.csr -subj "/C=US/ST=Oregon/L=Portland/O=Test/OU=Client/CN=localhost" 73 | openssl x509 -passin pass:1111 -req -days 365 -in client.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out client.crt 74 | openssl rsa -passin pass:1111 -in client.key -out client.key 75 | ``` 76 | 77 | Then use it like this: 78 | 79 | ``` 80 | grpc-dynamic-gateway --ca=ca.crt --key=client.key --cert=client.crt api.proto 81 | ``` 82 | 83 | You can use SSL in code, like this: 84 | 85 | ```js 86 | const grpc = require('grpc') 87 | const credentials = grpc.credentials.createSsl( 88 | fs.readFileSync(yourca), 89 | fs.readFileSync(yourkey), 90 | fs.readFileSync(yourcert) 91 | ) 92 | app.use('/', grpcGateway(['api.proto'], '0.0.0.0:5051', credentials)) 93 | ``` 94 | 95 | # OpenAPI (a.k.a Swagger) 96 | 97 | [Protoc](https://github.com/google/protobuf) can generate a OpenAPI description of your RPC endpoints, if you have [protoc-gen-openapiv2](https://github.com/grpc-ecosystem/grpc-gateway/tree/master/protoc-gen-openapiv2) installed: 98 | 99 | ``` 100 | protoc DEFINITION.proto --openapiv2_out=logtostderr=true:. 101 | ``` 102 | 103 | # docker 104 | 105 | There is one required port, and a volume that will make it easier: 106 | 107 | - `/api.proto` - your proto file 108 | - `8080` - the exposed port 109 | 110 | There is also an optional environment variable: `GRPC_HOST` which should resolve to your grpc server (default `0.0.0.0:5051`) 111 | 112 | So to run it, try this: 113 | 114 | ``` 115 | docker run -v $(pwd)/your.proto:/api.proto -p 8080:8080 -e "GRPC_HOST=0.0.0.0:5051" -rm -it konsumer/grpc-dynamic-gateway 116 | ``` 117 | 118 | If you want to do something different, the exposed `CMD` is the same as `grpc-dynamic-gateway` CLI, above. 119 | -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict' 3 | 4 | const fs = require('fs') 5 | const grpcGateway = require('./index.js') 6 | const yargs = require('yargs') 7 | const express = require('express') 8 | const bodyParser = require('body-parser') 9 | const grpc = require('grpc') 10 | 11 | const argv = yargs.usage('Usage: $0 [options] DEFINITION.proto [DEFINITION2.proto...]') 12 | .help('?') 13 | .alias('?', 'help') 14 | .alias('?', 'h') 15 | 16 | .default('port', process.env.PORT || 8080) 17 | .describe('port', 'The port to serve your JSON proxy on') 18 | .alias('port', 'p') 19 | 20 | .default('grpc', process.env.GRPC_HOST || 'localhost:50051') 21 | .describe('grpc', 'The host & port to connect to, where your gprc-server is running') 22 | .alias('grpc', 'g') 23 | 24 | .describe('I', 'Path to resolve imports from') 25 | .alias('I', 'include') 26 | 27 | .describe('ca', 'SSL CA cert for gRPC') 28 | .describe('key', 'SSL client key for gRPC') 29 | .describe('cert', 'SSL client certificate for gRPC') 30 | 31 | .default('mountpoint', '/') 32 | .describe('mountpoint', 'URL to mount server on') 33 | .alias('mountpoint', 'm') 34 | 35 | .boolean('quiet') 36 | .describe('quiet', 'Suppress logs') 37 | .alias('quiet', 'q') 38 | 39 | .argv 40 | 41 | if (!argv._.length) { 42 | yargs.showHelp() 43 | process.exit(1) 44 | } 45 | 46 | let credentials 47 | if (argv.ca || argv.key || argv.cert) { 48 | if (!(argv.ca && argv.key && argv.cert)) { 49 | console.log('SSL requires --ca, --key, & --cert\n') 50 | yargs.showHelp() 51 | process.exit(1) 52 | } 53 | credentials = grpc.credentials.createSsl( 54 | fs.readFileSync(argv.ca), 55 | fs.readFileSync(argv.key), 56 | fs.readFileSync(argv.cert) 57 | ) 58 | } else { 59 | credentials = grpc.credentials.createInsecure() 60 | } 61 | 62 | const app = express() 63 | app.use(bodyParser.json()) 64 | app.use(bodyParser.urlencoded({ extended: false })) 65 | app.use(argv.mountpoint, grpcGateway(argv._, argv.grpc, credentials, !argv.quiet, argv.include)) 66 | app.listen(argv.port, () => { 67 | if (!argv.quiet) { 68 | console.log(`Listening on http://localhost:${argv.port}, proxying to gRPC on ${argv.grpc}`) 69 | } 70 | }) 71 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // TODO: socket.io for streams 4 | 5 | const requiredGrpc = require('grpc') 6 | const protoLoader = require('@grpc/proto-loader') 7 | const express = require('express') 8 | const colors = require('chalk') 9 | const fs = require('fs') 10 | const path = require('path'); 11 | const schema = require('protocol-buffers-schema') 12 | const colorize = require('json-colorizer') 13 | const yaml = require('js-yaml'); 14 | 15 | const supportedMethods = ['get', 'put', 'post', 'delete', 'patch'] // supported HTTP methods 16 | const paramRegex = /{(\w+)}/g // regex to find gRPC params in url 17 | 18 | const lowerFirstChar = str => str.charAt(0).toLowerCase() + str.slice(1) 19 | 20 | /** 21 | * generate middleware to proxy to gRPC defined by proto files 22 | * @param {string[]} protoFiles Filenames of protobuf-file 23 | * @param {string} grpcLocation HOST:PORT of gRPC server 24 | * @param {ChannelCredentials} gRPC credential context (default: grpc.credentials.createInsecure()) 25 | * @param {string} include Path to find all includes 26 | * @return {Function} Middleware 27 | */ 28 | const middleware = (protoFiles, grpcLocation, credentials = requiredGrpc.credentials.createInsecure(), debug = true, include = process.cwd(), grpc = requiredGrpc) => { 29 | const router = express.Router() 30 | const clients = {} 31 | include = (Array.isArray(include) ? include : [include]).map(function (value, index, array) { 32 | if (value.endsWith('/')) { 33 | value = value.substring(0, include.length - 1) // remove"/" 34 | } 35 | return value 36 | }) 37 | protoFiles = protoFiles.map(function (value, index, array) { 38 | if (value.startsWith(include)) { 39 | value = value.substring(include.length + 1) 40 | } 41 | return value 42 | }) 43 | 44 | const protos = protoFiles.map(p => { 45 | const packageDefinition = include ? protoLoader.loadSync(p, { includeDirs: include }) : protoLoader.loadSync(p) 46 | return grpc.loadPackageDefinition(packageDefinition) 47 | }) 48 | 49 | const protoHttpRules = protoFiles.map(p => `${include[0]}/${p}`).map(p => { 50 | const protoDescriptionFile = p.replace(path.extname(p), '.yaml') 51 | var rules = {} 52 | if (fs.existsSync(protoDescriptionFile)) { 53 | const protoDescription = yaml.safeLoad(fs.readFileSync(protoDescriptionFile, 'utf8')); 54 | if (protoDescription['type'] == 'google.api.Service') { 55 | for (const rule of protoDescription['http']['rules']) { 56 | if ('selector' in rule) { 57 | rules[rule['selector']] = rule 58 | } else { 59 | console.warn(`Ignore proto description http rule = ${rule}`) 60 | } 61 | } 62 | } else { 63 | console.warn(`Ignore proto description type = ${protoDescription['type']}`) 64 | } 65 | } 66 | return rules 67 | }) 68 | 69 | protoFiles 70 | .map(p => `${include[0]}/${p}`) 71 | .map(p => schema.parse(fs.readFileSync(p))) 72 | .forEach((sch, si) => { 73 | const pkg = sch.package 74 | if (!sch.services) { return } 75 | sch.services.forEach(s => { 76 | const svc = s.name 77 | getPkg(clients, pkg, true)[svc] = new (getPkg(protos[si], pkg, false))[svc](grpcLocation, credentials) 78 | s.methods.forEach(m => { 79 | const fullName = pkg + '.' + svc + '.' + m.name 80 | const httpRule = fullName in protoHttpRules[si] ? protoHttpRules[si][fullName] : m.options['google.api.http'] 81 | if (httpRule) { 82 | supportedMethods.forEach(httpMethod => { 83 | if (httpRule[httpMethod]) { 84 | if (debug) console.log(colors.green(httpMethod.toUpperCase()), colors.blue(httpRule[httpMethod])) 85 | router[httpMethod](convertUrl(httpRule[httpMethod]), (req, res) => { 86 | const params = convertParams(req, httpRule[httpMethod]) 87 | const meta = convertHeaders(req.headers, grpc) 88 | if (debug) { 89 | const ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress 90 | console.log(`GATEWAY: ${colors.yellow((new Date()).toISOString())} (${colors.cyan(ip)}): /${colors.blue(pkg.replace(/\./g, colors.white('.')))}.${colors.blue(svc)}/${colors.cyan(m.name)}(${colorize(params)})`) 91 | } 92 | 93 | try { 94 | getPkg(clients, pkg, false)[svc][lowerFirstChar(m.name)](params, meta, (err, ans) => { 95 | // TODO: PRIORITY:MEDIUM - improve error-handling 96 | // TODO: PRIORITY:HIGH - double-check JSON mapping is identical to grpc-gateway 97 | if (err) { 98 | console.error(colors.red(`${svc}.${m.name}`, err.message)) 99 | console.trace() 100 | return res.status(500).json({ code: err.code, message: err.message }) 101 | } 102 | res.json(convertBody(ans, httpRule.body, httpRule[httpMethod])) 103 | }) 104 | } catch (err) { 105 | console.error(colors.red(`${svc}.${m.name}: `, err.message)) 106 | console.trace() 107 | } 108 | }) 109 | } 110 | }) 111 | } 112 | }) 113 | }) 114 | }) 115 | return router 116 | } 117 | 118 | const getPkg = (client, pkg, create = false) => { 119 | if (!((pkg || '').indexOf('.') !== -1) && client[pkg] !== undefined) { 120 | return client[pkg] 121 | } 122 | 123 | if (((pkg || '').indexOf('.') !== -1) && client[pkg] !== undefined) { 124 | return client[pkg] 125 | } 126 | 127 | const ls = pkg.split('.') 128 | let obj = client 129 | ls.forEach(function (name) { 130 | if (create) { 131 | obj[name] = obj[name] || {} 132 | } 133 | obj = obj[name] 134 | }) 135 | return obj 136 | } 137 | 138 | /** 139 | * Parse express request params & query into params for grpc client 140 | * @param {Request} req Express request object 141 | * @param {string} url gRPC url field (ie "/v1/hi/{name}") 142 | * @return {Object} params for gRPC client 143 | */ 144 | const convertParams = (req, url) => { 145 | const gparams = getParamsList(req, url) 146 | const flat = req.body 147 | gparams.forEach(p => { 148 | if (req.query && req.query[p]) { 149 | flat[p] = req.query[p] 150 | } 151 | if (req.params && req.params[p]) { 152 | flat[p] = req.params[p] 153 | } 154 | }) 155 | const tree = {}; 156 | Object.keys(flat).forEach(k => { 157 | putParamInObjectHierarchy(k.split('.'), tree, flat[k]) 158 | }); 159 | return tree; 160 | } 161 | 162 | /** 163 | * Help put the URL Params in the proper object structure 164 | * @param {Array} keyArray The param name split by '.' 165 | * @param {*} targetObj The current selected branch of the object tree 166 | * @param {*} value The param value 167 | */ 168 | const putParamInObjectHierarchy = (keyArray, targetObj, value) => { 169 | if (keyArray.length > 1) { 170 | const k = keyArray.shift(); 171 | targetObj[k] = targetObj[k] || {} 172 | putParamInObjectHierarchy(keyArray, targetObj[k], value) 173 | } 174 | else { 175 | targetObj[keyArray[0]] = value 176 | } 177 | } 178 | 179 | /** 180 | * Convert gRPC URL expression into express 181 | * @param {string} url gRPC URL expression 182 | * @return {string} express URL expression 183 | */ 184 | const convertUrl = (url) => ( 185 | // TODO: PRIORITY:LOW - use types to generate regex for numbers & strings in params 186 | url.replace(paramRegex, ':$1') 187 | ) 188 | 189 | /** 190 | * Convert gRPC response to output, based on gRPC body field 191 | * @param {Object} value gRPC response object 192 | * @param {string} bodyMap gRPC body field 193 | * @return {mixed} mapped output for `res.send()` 194 | */ 195 | const convertBody = (value, bodyMap) => { 196 | bodyMap = bodyMap || '*' 197 | if (bodyMap === '*') { 198 | return value 199 | } else { 200 | return value[bodyMap] 201 | } 202 | } 203 | 204 | /** 205 | * Get a list of params from a gRPC URL 206 | * @param {string} url gRPC URL 207 | * @return {string[]} Array of params 208 | */ 209 | const getParamsList = (req, url) => { 210 | let out = [] 211 | if (req.query) { 212 | out = Object.keys(req.query) 213 | } 214 | let m 215 | while ((m = paramRegex.exec(url)) !== null) { 216 | if (m.index === paramRegex.lastIndex) { 217 | paramRegex.lastIndex++ 218 | } 219 | out.push(m[1]) 220 | } 221 | return out 222 | } 223 | 224 | /** 225 | * Convert headers into gRPC meta 226 | * @param {object} headers Headers: {name: value} 227 | * @return {meta} grpc meta object 228 | */ 229 | const convertHeaders = (headers, grpc) => { 230 | grpc = grpc || requiredGrpc 231 | headers = headers || {} 232 | const metadata = new grpc.Metadata() 233 | Object.keys(headers).forEach(h => { metadata.set(h, headers[h]) }) 234 | return metadata 235 | } 236 | 237 | // interface 238 | module.exports = middleware 239 | module.exports.convertParams = convertParams 240 | module.exports.convertUrl = convertUrl 241 | module.exports.convertBody = convertBody 242 | module.exports.getParamsList = getParamsList 243 | module.exports.convertHeaders = convertHeaders 244 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grpc-dynamic-gateway", 3 | "version": "0.4.4", 4 | "description": "Like grpc-gateway, but written in node and dynamic.", 5 | "main": "index.js", 6 | "bin": { 7 | "grpc-dynamic-gateway": "cli.js" 8 | }, 9 | "scripts": { 10 | "test": "jest", 11 | "test:update": "npm test -- --updateSnapshot", 12 | "test:watch": "npm test -- --watch", 13 | "release": "release-it -p -n" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/konsumer/grpc-dynamic-gateway.git" 18 | }, 19 | "keywords": [ 20 | "grpc", 21 | "express", 22 | "middleware", 23 | "swagger", 24 | "rest", 25 | "json", 26 | "protobuf" 27 | ], 28 | "author": "David Konsumer ", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/konsumer/grpc-dynamic-gateway/issues" 32 | }, 33 | "homepage": "https://github.com/konsumer/grpc-dynamic-gateway#readme", 34 | "dependencies": { 35 | "@grpc/proto-loader": "0.5.1", 36 | "body-parser": "^1.18.2", 37 | "chalk": "^3.0.0", 38 | "express": "^4.16.3", 39 | "grpc": "^1.24.3", 40 | "js-yaml": "^3.14.0", 41 | "json-colorizer": "^2.0.0", 42 | "protocol-buffers-schema": "^3.3.2", 43 | "yargs": "^14.0.0" 44 | }, 45 | "devDependencies": { 46 | "jest": "^26.1.0", 47 | "release-it": "^13.6.5" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /test/google/api/annotations.proto: -------------------------------------------------------------------------------- 1 | // dummy file for RPCs -------------------------------------------------------------------------------- /test/index.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* global describe, it, expect */ 4 | const { convertParams, convertUrl, convertBody, getParamsList, convertHeaders } = require('..') 5 | 6 | describe('gRPC Dynamic Gateway', () => { 7 | describe('convertParams()', () => { 8 | it('should handle /v1/hi/{name}', () => { 9 | const req = { 10 | body: { 11 | v1: true, 12 | v2: false 13 | }, 14 | params: { 15 | name: 'Cool' 16 | } 17 | } 18 | const result = convertParams(req, '/v1/hi/{name}') 19 | expect(result.name).toBe('Cool') 20 | expect(result.v1).toBe(true) 21 | expect(result.v2).toBe(false) 22 | }) 23 | }) 24 | 25 | describe('convertUrl()', () => { 26 | it('should correctly convert /v1/hi/{name} into express URL', () => { 27 | const result = convertUrl('/v1/hi/{name}') 28 | expect(result).toBe('/v1/hi/:name') 29 | }) 30 | 31 | it('should correctly convert /{version}/hi/{name}/{cool} into express URL', () => { 32 | const result = convertUrl('/{version}/hi/{name}/{cool}') 33 | expect(result).toBe('/:version/hi/:name/:cool') 34 | }) 35 | }) 36 | 37 | describe('convertBody()', () => { 38 | it('should handle {cool: true}, *', () => { 39 | const result = convertBody({ cool: true }, '*') 40 | expect(result.cool).toBe(true) 41 | }) 42 | 43 | it('should handle {cool: true}, cool', () => { 44 | const result = convertBody({ cool: true }, 'cool') 45 | expect(result).toBe(true) 46 | }) 47 | }) 48 | 49 | describe('getParamsList()', () => { 50 | it('should find params in /v1/hi/{name}?tester=Cool', () => { 51 | const req = { 52 | query: { 53 | tester: 'Cool' 54 | } 55 | } 56 | const result = getParamsList(req, '/v1/hi/{name}') 57 | expect(result).toContain('name') 58 | expect(result).toContain('tester') 59 | }) 60 | 61 | it('should find params in /{version}/hi/{name}/{cool}?tester=Cool', () => { 62 | const req = { 63 | query: { 64 | tester: 'Cool' 65 | } 66 | } 67 | const result = getParamsList(req, '/{version}/hi/{name}/{cool}') 68 | expect(result).toContain('name') 69 | expect(result).toContain('cool') 70 | expect(result).toContain('tester') 71 | }) 72 | }) 73 | 74 | describe('convertHeaders()', () => { 75 | it('should convert a Authorize token in header to a metadata object', () => { 76 | const result = convertHeaders({ 'Authorize': 'Bearer DUMMY_TOKEN' }) 77 | expect(result.constructor.name).toBe('Metadata') 78 | expect(result._internal_repr.authorize).toContain('Bearer DUMMY_TOKEN') 79 | }) 80 | }) 81 | }) 82 | -------------------------------------------------------------------------------- /test/test.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package helloworld; 3 | 4 | import "google/api/annotations.proto"; 5 | 6 | service Greeter { 7 | rpc sayHello (HelloRequest) returns (HelloReply) { 8 | option (google.api.http) = { 9 | post: "/v1/hi" 10 | body: "*" 11 | }; 12 | } 13 | } 14 | 15 | message HelloRequest { 16 | string name = 1; 17 | } 18 | 19 | message HelloReply { 20 | string message = 1; 21 | } 22 | -------------------------------------------------------------------------------- /test/test_implementation.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | function sayHello (call, callback) { 4 | const message = `Hello ${call.request.name}` 5 | callback(null, { message }) 6 | } 7 | 8 | module.exports = { 9 | helloworld: { 10 | Greeter: { 11 | sayHello 12 | } 13 | } 14 | } 15 | --------------------------------------------------------------------------------