forked from konsumer/grpc-dynamic-gateway
-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.ts
161 lines (151 loc) · 5.33 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
import importedGrpc from 'grpc';
import express from 'express';
import colors from 'chalk';
import fs from 'fs';
import schema from 'protocol-buffers-schema';
const supportedMethods = ['get', 'put', 'post', 'delete', 'patch']; // supported HTTP methods
const paramRegex = /{(\w+)}/g; // regex to find gRPC params in url
const lowerFirstChar = str => str.charAt(0).toLowerCase() + str.slice(1);
/**
* generate middleware to proxy to gRPC defined by proto files
* @param protoFiles Filenames of protobuf-file
* @param grpcLocation HOST:PORT of gRPC server
* @param credentials credential context (default: grpc.credentials.createInsecure())
* @param include Path to find all includes
* @return Middleware
*/
const middleware = (protoFiles: string[], grpcLocation: string, credentials: importedGrpc.ServerCredentials = importedGrpc.credentials.createInsecure(), include?: string) => {
const router = express.Router();
const clients = {};
const protos = protoFiles.map(p => include ? importedGrpc.load({ file: p, root: include }) : importedGrpc.load(p));
protoFiles
.map(p => `${include}/${p}`)
.map(p => schema.parse(fs.readFileSync(p)))
.forEach((sch, si) => {
const pkg = sch.package;
if (!sch.services) { return; }
sch.services.forEach((s) => {
const svc = s.name;
const svcarr = getPkg(clients, pkg, true);
svcarr[svc] = new (getPkg(protos[si], pkg, false))[svc](grpcLocation, credentials);
s.methods.forEach((m) => {
if (m.options['google.api.http']) {
supportedMethods.forEach((httpMethod) => {
if (m.options['google.api.http'][httpMethod]) {
console.log(colors.green(httpMethod.toUpperCase()), colors.blue(m.options['google.api.http'][httpMethod]));
router[httpMethod](convertUrl(m.options['google.api.http'][httpMethod]), (req, res) => {
const params = convertParams(req, m.options['google.api.http'][httpMethod]);
const meta = convertHeaders(req.headers, importedGrpc);
const ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
try {
getPkg(clients, pkg, false)[svc][lowerFirstChar(m.name)](params, meta, (err, ans) => {
// TODO: PRIORITY:MEDIUM - improve error-handling
// TODO: PRIORITY:HIGH - double-check JSON mapping is identical to grpc-gateway
if (err) {
console.error(colors.red(`${svc}.${m.name}`, err.message));
console.trace();
return res.status(500).json({ code: err.code, message: err.message });
}
res.json(convertBody(ans, m.options['google.api.http'].body));
});
} catch (err) {
console.error(colors.red(`${svc}.${m.name}: `, err.message));
console.trace();
}
});
}
});
}
});
});
});
return router;
};
const getPkg = (client: any, pkg: any, create: boolean = false): Object => {
const ls = pkg.split('.');
let obj = client;
ls.forEach((name) => {
if (create) {
obj[name] = obj[name] || {};
}
obj = obj[name];
});
return obj;
};
/**
* Parse express request params & query into params for grpc client
* @param req Express request object
* @param url gRPC url field (ie "/v1/hi/{name}")
* @return params for gRPC client
*/
const convertParams = (req: express.Request, url: string): any => {
const gparams = getParamsList(url);
const out = req.body;
gparams.forEach((p: any) => {
if (req.query && req.query[p]) {
out[p] = req.query[p];
}
if (req.params && req.params[p]) {
out[p] = req.params[p];
}
});
return out;
};
/**
* Convert gRPC URL expression into express
* @param url gRPC URL expression
* @return express URL expression
*/
const convertUrl = (url: string): string => {
// TODO: PRIORITY:LOW - use types to generate regex for numbers & strings in params
return url.replace(paramRegex, ':$1');
};
/**
* Convert gRPC response to output, based on gRPC body field
* @param value gRPC response object
* @param bodyMap gRPC body field
* @return mapped output for `res.send()`
*/
const convertBody = (value: any, bodyMap: string): any => {
const respBodyMap = bodyMap || '*';
if (respBodyMap === '*') {
return value;
} else {
return value[respBodyMap];
}
};
/**
* Get a list of params from a gRPC URL
* @param url gRPC URL
* @return Array of params
*/
const getParamsList = (url: string): any => {
const out: string[] = [];
let m: RegExpExecArray | null;
while ((m = paramRegex.exec(url)) !== null) {
if (m.index === paramRegex.lastIndex) {
paramRegex.lastIndex += 1;
}
out.push(m[1]);
}
return out;
};
/**
* Convert headers into gRPC meta
* @param headers Headers: {name: value}
* @return grpc meta object
*/
const convertHeaders = (headers?: any, grpc = importedGrpc): any => {
const grpcheaders = headers || {};
const metadata = new grpc.Metadata();
Object.keys(grpcheaders).forEach((h) => { metadata.set(h, grpcheaders[h]); });
return metadata;
};
export default middleware;
export {
convertParams,
convertUrl,
convertBody,
getParamsList,
convertHeaders,
};