├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── package.json ├── src ├── RpcRegistry.ts ├── ServiceContainer.ts ├── SettingRegistry.ts ├── decorator │ ├── Route.ts │ ├── Service.ts │ ├── Settings.ts │ └── index.ts ├── index.ts └── interface │ └── Settings.ts ├── tsconfig.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | app/ 2 | node_modules/ 3 | package-lock.json 4 | 5 | docs/ 6 | runtime/ 7 | test/ 8 | proto/ -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | proto/ 2 | runtime/ 3 | test/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 图南 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 | [![NPM version][npm-image]][npm-url] 2 | # Installation 3 | ``` 4 | npm install grpc-server-ts --save 5 | ``` 6 | 7 | ## protobuf 8 | hello.proto 9 | ```proto 10 | syntax = "proto3"; 11 | 12 | option java_multiple_files = true; 13 | option java_package = "io.grpc.service.test"; 14 | option objc_class_prefix ="RTG"; 15 | 16 | package hello; 17 | 18 | service Hello { 19 | rpc say(stream Empty) returns (stream Word) {}; 20 | } 21 | 22 | message Empty { 23 | 24 | } 25 | 26 | message Word { 27 | string word = 1; 28 | } 29 | ``` 30 | 31 | ## define a service 32 | ```ts 33 | import { Route, Service } from "grpc-server-ts"; 34 | 35 | @Service('path_to_hello.proto') 36 | export class HelloService { 37 | @Route 38 | public async say() { 39 | return 'hello world'; 40 | } 41 | } 42 | ``` 43 | 44 | ## registry service grpc 45 | ```ts 46 | import { RpcRegistry, Settings } from 'grpc-server-ts'; 47 | 48 | @Settings(settings) 49 | class RPC extends RpcRegistry { } 50 | RPC.start(); 51 | ``` 52 | 53 | ### settings 54 | ```js 55 | { 56 | port: "3333", // listen port 57 | host: "localhost", // listen host 58 | ca: "runtime/rpc/ca.crt", // ca file path 59 | cert: "runtime/rpc/server.crt", // cert file path 60 | key: "runtime/rpc/server.key" // key file path 61 | } 62 | ``` 63 | 64 | ### Sponsor 65 | ![wechat](http://cdn-public.imxuezi.com/WX20190524-211855@2x.png?imageView2/0/w/200/q/100) 66 | 67 | ![alipay](http://cdn-public.imxuezi.com/WX20190524-212118@2x.png?imageView2/0/w/200/q/100) 68 | 69 | [wechat-pay]: http://cdn-public.imxuezi.com/WX20190524-211855@2x.png 70 | [npm-image]: https://img.shields.io/npm/v/grpc-server-ts.svg?style=flat-square 71 | [npm-url]: https://www.npmjs.com/package/grpc-server-ts -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grpc-server-ts", 3 | "version": "1.1.4", 4 | "description": "a grpc server side module built by typescrpt", 5 | "main": "app/index.js", 6 | "types": "app/index.d.ts", 7 | "scripts": { 8 | "build": "tsc --declaration -w -p .", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "homepage": "https://github.com/xuezier/grpc-server-ts", 12 | "keywords": [ 13 | "grpc", 14 | "typescript", 15 | "ts", 16 | "grpc-ts" 17 | ], 18 | "author": "xuezi", 19 | "license": "ISC", 20 | "dependencies": { 21 | "@grpc/proto-loader": "^0.3.0", 22 | "grpc": "^1.12.4", 23 | "reflect-metadata": "^0.1.12", 24 | "ts-loader": "^4.4.2", 25 | "typescript": "^2.9.2" 26 | }, 27 | "devDependencies": { 28 | "@types/node": "^10.5.4", 29 | "typedoc": "^0.12.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/RpcRegistry.ts: -------------------------------------------------------------------------------- 1 | import * as GRPC from 'grpc'; 2 | import * as FS from 'fs'; 3 | 4 | import { ServiceContainer } from './ServiceContainer'; 5 | import { SettingRegistry } from './SettingRegistry'; 6 | 7 | export class RpcRegistry { 8 | private static _port: number | string; 9 | private static _host: string; 10 | private static _ca: string; 11 | private static _cert: string; 12 | private static _key: string; 13 | 14 | private static _credentials: GRPC.ServerCredentials; 15 | 16 | static get port(): string | number { 17 | return this._port; 18 | } 19 | 20 | static get host(): string { 21 | return this._host; 22 | } 23 | 24 | static get ca(): string { 25 | return this._ca; 26 | } 27 | 28 | static get cert(): string { 29 | return this._cert; 30 | } 31 | 32 | static get key(): string { 33 | return this._key; 34 | } 35 | 36 | private static _server: GRPC.Server = new GRPC.Server(); 37 | 38 | static get server() { 39 | return this._server; 40 | } 41 | 42 | static start() { 43 | this._loadSettings(); 44 | this._registryService(); 45 | this._addTls(); 46 | this._start(); 47 | } 48 | 49 | private static _start() { 50 | let address = `${this.host}:${this.port}`; 51 | this.server.bind(address, this._credentials); 52 | this.server.start(); 53 | } 54 | 55 | private static _loadSettings() { 56 | let { port, host, ca, cert, key } = SettingRegistry.settings; 57 | this._port = port; 58 | this._host = host || '127.0.0.1'; 59 | this._ca = ca; 60 | this._cert = cert; 61 | this._key = key; 62 | } 63 | 64 | private static _registryService() { 65 | for (let serviceContainer of ServiceContainer.services) { 66 | let rpc: { [x: string]: Function } = {}; 67 | for (let key in serviceContainer.service) { 68 | let route = serviceContainer.target.prototype[key]; 69 | if (route) { 70 | let routeContainer = ServiceContainer.routes.find(r => { 71 | return (r.target.constructor === serviceContainer.target && route === r.route); 72 | }); 73 | 74 | let _func = ServiceContainer.generateRouteFunc(serviceContainer.service, route); 75 | routeContainer.func = _func; 76 | rpc[key] = routeContainer.func; 77 | } 78 | } 79 | this.server.addService(serviceContainer.service, rpc); 80 | } 81 | } 82 | 83 | private static _addTls() { 84 | if (!this.ca || !this.cert || !this.key) { 85 | this._credentials = GRPC.ServerCredentials.createInsecure(); 86 | } else { 87 | let ca = FS.readFileSync(this.ca); 88 | let cert = FS.readFileSync(this.cert); 89 | let key = FS.readFileSync(this.key); 90 | this._credentials = GRPC.ServerCredentials.createSsl(ca, [{ 91 | cert_chain: cert, 92 | private_key: key 93 | }], true); 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/ServiceContainer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Xuezi 3 | * @email xzj15859722542@hotmail.com 4 | * @create date 2018-06-14 06:11:20 5 | * @modify date 2018-06-14 06:11:20 6 | * @desc [description] 7 | */ 8 | import * as GRPC from 'grpc'; 9 | import * as protoLoader from '@grpc/proto-loader'; 10 | 11 | export class ServiceContainer { 12 | static services: { service: any, target: Function }[] = []; 13 | static routes: { target: Function, route: Function, func?: Function }[] = []; 14 | /** 15 | * @description registry grpc service 16 | * @author Xuezi 17 | * @static 18 | * @param {Function} target service constructor 19 | * @param {string} path proto file path 20 | * @memberof ServiceContainer 21 | */ 22 | static registryService(target: Function, path: string) { 23 | let packageDefinition = protoLoader.loadSync(path, { 24 | keepCase: true, 25 | longs: String, 26 | enums: String, 27 | defaults: true, 28 | oneofs: true 29 | }); 30 | let protoDescriptor = GRPC.loadPackageDefinition(packageDefinition); 31 | 32 | const packages = Object.keys(protoDescriptor); 33 | for (let packageKey of packages) { 34 | for (let key in protoDescriptor[packageKey]) { 35 | this._setServiceClient(protoDescriptor[packageKey][key], target); 36 | // if (protoDescriptor[packageKey][key].hasOwnProperty('service')) { 37 | // this.services.push({ service: protoDescriptor[packageKey][key]['service'], target }); 38 | // } 39 | } 40 | } 41 | } 42 | 43 | private static _setServiceClient(service: any, target: Function) { 44 | if(!service) { 45 | return; 46 | } 47 | 48 | if(service.name === 'ServiceClient') { 49 | this.services.push({service: service['service'], target}); 50 | } else { 51 | for(const key in service) { 52 | this._setServiceClient(service[key], target); 53 | } 54 | } 55 | } 56 | 57 | static registryRoute(target: Function, route: Function) { 58 | if (this.routes.find(r => (r.target === target && r.route === route))) { 59 | return; 60 | } 61 | 62 | this.routes.push({ target, route }); 63 | } 64 | 65 | static generateRouteFunc(...args) { 66 | return this._generateRouteFunc.apply(this, args); 67 | } 68 | 69 | private static _generateRouteFunc(service: any, route: Function): Function { 70 | let key = route.name; 71 | let rawFunc = service[key]; 72 | let {requestStream, responseStream} = rawFunc; 73 | 74 | let func; 75 | if (requestStream && responseStream) { 76 | // requestStream: true 77 | // responseStream: true 78 | func = async function(call) { 79 | call.on('data', async function(chunk) { 80 | try { 81 | let result = await route(chunk); 82 | call.write(result); 83 | } catch (e) { 84 | call.emit('error', e); 85 | } 86 | }); 87 | 88 | call.on('error', error => { 89 | console.error(error); 90 | }); 91 | 92 | call.on('end', () => { 93 | call.end && call.end(); 94 | }); 95 | }; 96 | } else if (requestStream && !responseStream) { 97 | // requestStream: true 98 | // responseStream: false 99 | func = async function(call) { 100 | let callback = arguments[1]; 101 | 102 | call.on('data', async chunk => { 103 | try { 104 | let result = await route(chunk); 105 | callback(null, result); 106 | } catch (e) { 107 | callback(e); 108 | } 109 | }); 110 | 111 | call.on('error', error => { 112 | console.error(error); 113 | }); 114 | 115 | call.on('end', () => { 116 | call.end && call.end(); 117 | }); 118 | }; 119 | } else if (!requestStream && responseStream) { 120 | // requestStream: false 121 | // responseStream: true 122 | func = async function(call) { 123 | try { 124 | let result = await route(call.request); 125 | call.write(result); 126 | } catch (e) { 127 | call.emit('error', e); 128 | } 129 | 130 | call.on('error', error => { 131 | console.error(error); 132 | }); 133 | 134 | call.on('end', () => { 135 | call.end && call.end(); 136 | }); 137 | }; 138 | } else { 139 | // requestStream: false 140 | // responseStream: false 141 | func = async function(call) { 142 | let callback = arguments[1]; 143 | 144 | try { 145 | let result = await route(call.request); 146 | callback(null,result); 147 | } catch (e) { 148 | callback(e); 149 | } 150 | }; 151 | } 152 | 153 | return func; 154 | } 155 | 156 | static getService(target: Function): any[] { 157 | let services = this.services.filter(service => service.target === target); 158 | return services; 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/SettingRegistry.ts: -------------------------------------------------------------------------------- 1 | import { Settings } from './interface/Settings'; 2 | 3 | export class SettingRegistry { 4 | static settings: Settings; 5 | 6 | static registry(settings: Settings) { 7 | this.settings = settings; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/decorator/Route.ts: -------------------------------------------------------------------------------- 1 | import { ServiceContainer } from '../ServiceContainer'; 2 | 3 | export function Route(target: any, _: string, { value: route }: { value?: any }) { 4 | if (!(route instanceof Function)) { 5 | throw new Error('Route decorator target must be a Function'); 6 | } 7 | ServiceContainer.registryRoute(target, route); 8 | } 9 | -------------------------------------------------------------------------------- /src/decorator/Service.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Xuezi 3 | * @email xzj15859722542@hotmail.com 4 | * @create date 2018-06-14 05:58:38 5 | * @modify date 2018-06-14 05:58:38 6 | * @desc [description] 7 | */ 8 | import { ServiceContainer } from '../ServiceContainer'; 9 | 10 | export function Service(path: string) { 11 | return function(target: Function) { 12 | ServiceContainer.registryService(target, path); 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /src/decorator/Settings.ts: -------------------------------------------------------------------------------- 1 | import { Settings } from '../interface/Settings'; 2 | import { SettingRegistry } from '../SettingRegistry'; 3 | 4 | export function Settings(settings: Settings) { 5 | return function(_: Function) { 6 | SettingRegistry.registry(settings); 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /src/decorator/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Route'; 2 | export * from './Service'; 3 | export * from './Settings'; 4 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './decorator'; 2 | export * from './RpcRegistry'; 3 | -------------------------------------------------------------------------------- /src/interface/Settings.ts: -------------------------------------------------------------------------------- 1 | export interface Settings { 2 | // grpc bind port 3 | port: string | number; 4 | 5 | // grpc bind host 6 | host?: string; 7 | 8 | // ca file path string 9 | ca?: string; 10 | 11 | // cert file path string 12 | cert?: string; 13 | 14 | // key file path string 15 | key?: string; 16 | } 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "diagnostics": true, 4 | "noUnusedLocals": true, 5 | "noUnusedParameters": true, 6 | "target": "es6", 7 | "lib": ["dom", "es7"], 8 | "types": ["node"], 9 | "typeRoots": [ 10 | "node_modules/@types" 11 | ], 12 | "module": "commonjs", 13 | "moduleResolution": "node", 14 | "experimentalDecorators":true, 15 | "emitDecoratorMetadata": true, 16 | "sourceMap": true, 17 | "declaration": true, 18 | "outDir": "app" 19 | }, 20 | "include": [ 21 | "src/**/*.ts" 22 | ], 23 | "exclude": [ 24 | "node_modules", 25 | "build" 26 | ] 27 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-internal-module": true, 4 | "typedef-whitespace": [ 5 | true, 6 | { 7 | "call-signature": "nospace", 8 | "index-signature": "nospace", 9 | "parameter": "nospace", 10 | "property-declaration": "nospace", 11 | "variable-declaration": "nospace" 12 | }, 13 | { 14 | "call-signature": "onespace", 15 | "index-signature": "onespace", 16 | "parameter": "onespace", 17 | "property-declaration": "onespace", 18 | "variable-declaration": "onespace" 19 | } 20 | ], 21 | "await-promise": true, 22 | "curly": [true, "ignore-same-line"], 23 | "deprecation": true, 24 | "label-position": true, 25 | "no-arg": true, 26 | "no-conditional-assignment": true, 27 | "no-duplicate-imports": true, 28 | "no-duplicate-variable": true, 29 | "no-empty": true, 30 | "no-eval": true, 31 | "no-reference-import": true, 32 | "no-return-await": true, 33 | "no-switch-case-fall-through": true, 34 | "no-unused-expression": [true, "allow-fast-null-checks", "allow-tagged-template"], 35 | "no-var-keyword": true, 36 | "radix": true, 37 | "space-within-parens": [true, 0], 38 | "triple-equals": [ 39 | true, 40 | "allow-null-check" 41 | ], 42 | "use-isnan": true, 43 | "eofline": true, 44 | "ter-indent": [ 45 | true, 46 | 2, 47 | { 48 | "SwitchCase": 1 49 | } 50 | ], 51 | "no-trailing-whitespace": true, 52 | "trailing-comma": [ 53 | true, 54 | { 55 | "multiline": "never", 56 | "singleline": "never" 57 | } 58 | ], 59 | "class-name": true, 60 | "comment-format": [ 61 | true, 62 | "check-space" 63 | ], 64 | "jsdoc-format": true, 65 | "new-parens": true, 66 | "no-angle-bracket-type-assertion": true, 67 | "no-consecutive-blank-lines": true, 68 | "no-floating-promises": true, 69 | "no-misused-new": true, 70 | "no-string-throw": true, 71 | "no-unnecessary-qualifier": true, 72 | "no-unnecessary-type-assertion": true, 73 | "one-line": [ 74 | true, 75 | "check-catch", 76 | "check-finally", 77 | "check-else", 78 | "check-open-brace", 79 | "check-whitespace" 80 | ], 81 | "one-variable-per-declaration": true, 82 | "quotemark": [ 83 | true, 84 | "single", 85 | "avoid-escape", 86 | "jsx-single" 87 | ], 88 | "semicolon": [ 89 | true, 90 | "always" 91 | ], 92 | "strict-type-predicates": true, 93 | "space-before-function-paren": [true, "never"], 94 | "unified-signatures": true, 95 | "variable-name": [ 96 | true, 97 | "ban-keywords", 98 | "check-format", 99 | "allow-leading-underscore", 100 | "allow-pascal-case" 101 | ], 102 | "whitespace": [ 103 | true, 104 | "check-branch", 105 | "check-decl", 106 | "check-operator", 107 | // "check-module", 108 | "check-rest-spread", 109 | "check-type", 110 | "check-typecast", 111 | "check-type-operator", 112 | "check-preblock" 113 | ], 114 | // TSLint ESLint rules. 115 | "no-constant-condition": [ 116 | true, 117 | { 118 | "checkLoops": false 119 | } 120 | ], 121 | "no-control-regex": true, 122 | "no-duplicate-case": true, 123 | "no-empty-character-class": true, 124 | "no-ex-assign": true, 125 | "no-extra-boolean-cast": true, 126 | "no-inner-declarations": [ 127 | true, 128 | "functions" 129 | ], 130 | "no-invalid-regexp": true, 131 | "ter-no-irregular-whitespace": true, 132 | "no-regex-spaces": true, 133 | "ter-no-sparse-arrays": true, 134 | "ter-func-call-spacing": [true, "never"], 135 | "no-unexpected-multiline": true, 136 | "valid-typeof": true, 137 | "ter-arrow-spacing": [ 138 | true, 139 | { 140 | "before": true, 141 | "after": true 142 | } 143 | ], 144 | "no-multi-spaces": true, 145 | "handle-callback-err": [ 146 | true, 147 | "^(e|err|error)$" 148 | ], 149 | "block-spacing": [ 150 | true, 151 | "always" 152 | ], 153 | "brace-style": [ 154 | true, 155 | "1tbs", 156 | { 157 | "allowSingleLine": true 158 | } 159 | ], 160 | "object-curly-spacing": [true, "always"] 161 | } 162 | } --------------------------------------------------------------------------------