Skip to content

Commit aa0afd5

Browse files
authored
Merge pull request #5 from postmanlabs/feat/support-for-v1-server-reflection
Support for v1 Server Reflection
2 parents ecc52a2 + 7a70aa3 commit aa0afd5

File tree

13 files changed

+2795
-86
lines changed

13 files changed

+2795
-86
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
"prepare": "yarn run compile",
4545
"pretest": "yarn run compile && yarn run fix",
4646
"posttest": "yarn run check",
47-
"protoc": "grpc_tools_node_protoc --js_out=import_style=commonjs,binary:./src --grpc_out=grpc_js:./src --ts_out=grpc_js:./src --plugin=protoc-gen-ts=./node_modules/.bin/protoc-gen-ts -I ./static/grpc/reflection/v1alpha reflection.proto",
47+
"protoc": "node scripts/compile-protoc",
4848
"build": "yarn run compile && yarn run fix && yarn run check"
4949
},
5050
"files": [

scripts/compile-protoc.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
const fs = require('fs');
2+
const {execSync} = require('child_process');
3+
4+
const supportedProtocols = ['v1alpha', 'v1'];
5+
6+
async function main() {
7+
for (const protocol of supportedProtocols) {
8+
if (fs.existsSync(`./src/reflection_providers/${protocol}`)) {
9+
fs.rmdirSync(`./src/reflection_providers/${protocol}`, {recursive: true});
10+
}
11+
12+
fs.mkdirSync(`./src/reflection_providers/${protocol}`);
13+
14+
console.log(
15+
`Compiling protocol buffers and building services + clients for protocol: ${protocol}...\n`
16+
);
17+
18+
const command = [
19+
'grpc_tools_node_protoc',
20+
`--js_out=import_style=commonjs,binary:./src/reflection_providers/${protocol}`,
21+
`--grpc_out=grpc_js:./src/reflection_providers/${protocol}`,
22+
`--ts_out=grpc_js:./src/reflection_providers/${protocol}`,
23+
'--plugin=protoc-gen-ts=./node_modules/.bin/protoc-gen-ts',
24+
`-I ./static/grpc/reflection/${protocol}`,
25+
'reflection.proto',
26+
].join(' ');
27+
28+
execSync(command, {stdio: 'inherit'});
29+
30+
console.log('Compilation done for', protocol, '\n');
31+
}
32+
33+
console.log('Protocol buffers compilation completed.');
34+
}
35+
36+
main().catch(err => {
37+
console.error('Error during protocol buffers compilation:');
38+
39+
throw err;
40+
});

src/client.ts

Lines changed: 212 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import {ChannelCredentials, Metadata, ServiceError} from '@postman/grpc-js';
2-
import {getDescriptorRootFromDescriptorSet} from './descriptor';
3-
import * as services from './reflection_grpc_pb';
41
import {
5-
ServerReflectionRequest,
6-
ServerReflectionResponse,
7-
} from './reflection_pb';
2+
ChannelCredentials,
3+
Metadata,
4+
ServiceError,
5+
status as GrpcStatus,
6+
} from '@postman/grpc-js';
7+
import {getDescriptorRootFromDescriptorSet} from './descriptor';
88
import {Root} from '@postman/protobufjs';
99
import {
1010
FileDescriptorSet,
@@ -13,26 +13,191 @@ import {
1313
} from '@postman/protobufjs/ext/descriptor';
1414
import set from 'lodash.set';
1515

16+
// Static type definitions with common structures across all reflection providers
17+
import type {ServerReflectionClient} from './reflection_providers/v1alpha/reflection_grpc_pb';
18+
import type {
19+
ServerReflectionRequest,
20+
ServerReflectionResponse,
21+
} from './reflection_providers/v1alpha/reflection_pb';
22+
23+
const supportedReflectionAPIVersions = {
24+
v1alpha: {
25+
priority: 0,
26+
serviceName: 'grpc.reflection.v1alpha.ServerReflection',
27+
client: import('./reflection_providers/v1alpha/reflection_pb'),
28+
service: import('./reflection_providers/v1alpha/reflection_grpc_pb'),
29+
},
30+
v1: {
31+
priority: 1,
32+
serviceName: 'grpc.reflection.v1.ServerReflection',
33+
client: import('./reflection_providers/v1/reflection_pb'),
34+
service: import('./reflection_providers/v1/reflection_grpc_pb'),
35+
},
36+
};
37+
1638
export class Client {
1739
metadata: Metadata;
18-
grpcClient: services.IServerReflectionClient;
1940
private fileDescriptorCache: Map<string, IFileDescriptorProto> = new Map();
41+
private url: string;
42+
private credentials: ChannelCredentials;
43+
private clientOptions: object | undefined;
44+
45+
grpcClient: ServerReflectionClient | undefined;
46+
compatibleReflectionVersion: string | undefined;
47+
private reflectionResponseCache: ServerReflectionResponse | undefined;
48+
private CompatibleServerReflectionRequest:
49+
| (new (
50+
...args: ConstructorParameters<typeof ServerReflectionRequest>
51+
) => ServerReflectionRequest)
52+
| undefined;
53+
2054
constructor(
2155
url: string,
2256
credentials: ChannelCredentials,
2357
options?: object,
2458
metadata?: Metadata
2559
) {
60+
this.url = url;
61+
this.credentials = credentials;
62+
this.clientOptions = options;
2663
this.fileDescriptorCache = new Map();
2764
this.metadata = metadata || new Metadata();
28-
this.grpcClient = new services.ServerReflectionClient(
29-
url,
30-
credentials,
31-
options
32-
);
3365
}
3466

35-
listServices(): Promise<string[]> {
67+
private async sendReflectionRequest(
68+
request: ServerReflectionRequest | ServerReflectionRequest[],
69+
client?: ServerReflectionClient
70+
): Promise<ServerReflectionResponse[]> {
71+
return new Promise((resolve, reject) => {
72+
const result: ServerReflectionResponse[] = [];
73+
74+
const grpcCall = (client || this.grpcClient!).serverReflectionInfo(
75+
this.metadata
76+
);
77+
78+
grpcCall.on('data', (response: ServerReflectionResponse) => {
79+
result.push(response);
80+
});
81+
82+
grpcCall.on('error', (error: ServiceError) => {
83+
reject(error);
84+
});
85+
86+
grpcCall.on('end', () => resolve(result));
87+
88+
if (Array.isArray(request)) {
89+
request.forEach(req => grpcCall.write(req));
90+
} else {
91+
grpcCall.write(request);
92+
}
93+
94+
grpcCall.end();
95+
});
96+
}
97+
98+
private async evaluateSupportedServerReflectionProtocol() {
99+
const evaluationPromises = [];
100+
101+
// Check version compatibility and initialize gRPC client based on that
102+
for (const version of Object.keys(supportedReflectionAPIVersions)) {
103+
type ReflectionCheckPromiseReturnType = {
104+
successful: boolean;
105+
priority: number;
106+
effect?: () => void;
107+
error?: ServiceError;
108+
};
109+
110+
evaluationPromises.push(
111+
// eslint-disable-next-line no-async-promise-executor
112+
new Promise<ReflectionCheckPromiseReturnType>(async resolve => {
113+
const protocolConfig =
114+
supportedReflectionAPIVersions[
115+
version as keyof typeof supportedReflectionAPIVersions
116+
];
117+
const {
118+
service: servicePromise,
119+
client: clientPromise,
120+
} = protocolConfig;
121+
122+
const [protocolService, protocolClient] = await Promise.all([
123+
servicePromise,
124+
clientPromise,
125+
]);
126+
127+
const grpcClientForProtocol = new protocolService.ServerReflectionClient(
128+
this.url,
129+
this.credentials,
130+
this.clientOptions
131+
);
132+
133+
const request = new protocolClient.ServerReflectionRequest();
134+
135+
request.setListServices('*');
136+
137+
try {
138+
const [reflectionResponse] = await this.sendReflectionRequest(
139+
request,
140+
grpcClientForProtocol
141+
);
142+
143+
return resolve({
144+
successful: true,
145+
priority: protocolConfig.priority,
146+
effect: () => {
147+
this.grpcClient = grpcClientForProtocol;
148+
this.compatibleReflectionVersion = version;
149+
this.CompatibleServerReflectionRequest =
150+
protocolClient.ServerReflectionRequest;
151+
this.reflectionResponseCache = reflectionResponse;
152+
},
153+
});
154+
} catch (error) {
155+
return resolve({
156+
successful: false,
157+
priority: protocolConfig.priority,
158+
error: error as ServiceError,
159+
});
160+
}
161+
})
162+
);
163+
}
164+
165+
const evaluationResults = await Promise.all(evaluationPromises);
166+
167+
const [successfulReflectionByPriority] = evaluationResults
168+
.filter(res => res.successful)
169+
.sort((res1, res2) => res2.priority - res1.priority);
170+
171+
if (!successfulReflectionByPriority) {
172+
const reflectionNotImplementedError = evaluationResults.find(res => {
173+
return res.error && res.error.code === GrpcStatus.UNIMPLEMENTED;
174+
});
175+
176+
const resultWithServiceError = evaluationResults.find(res => {
177+
// Something is actually wrong with the gRPC service
178+
return res.error && res.error.code !== GrpcStatus.UNIMPLEMENTED;
179+
});
180+
181+
throw (
182+
resultWithServiceError?.error ||
183+
reflectionNotImplementedError?.error ||
184+
new Error('No compatible reflection API found.')
185+
);
186+
}
187+
188+
// Set grpc client and other properties based on highest priority successful version
189+
successfulReflectionByPriority.effect!();
190+
}
191+
192+
async initialize() {
193+
if (this.grpcClient || this.compatibleReflectionVersion) return;
194+
195+
await this.evaluateSupportedServerReflectionProtocol();
196+
}
197+
198+
async listServices(): Promise<string[]> {
199+
await this.initialize();
200+
36201
return new Promise((resolve, reject) => {
37202
function dataCallback(response: ServerReflectionResponse) {
38203
if (response.hasListServicesResponse()) {
@@ -52,14 +217,16 @@ export class Client {
52217
reject(e);
53218
}
54219

55-
const request = new ServerReflectionRequest();
220+
if (this.reflectionResponseCache) {
221+
return dataCallback(this.reflectionResponseCache);
222+
}
223+
224+
const request = new this.CompatibleServerReflectionRequest!();
56225
request.setListServices('*');
57226

58-
const grpcCall = this.grpcClient.serverReflectionInfo(this.metadata);
59-
grpcCall.on('data', dataCallback);
60-
grpcCall.on('error', errorCallback);
61-
grpcCall.write(request);
62-
grpcCall.end();
227+
this.sendReflectionRequest(request)
228+
.then(([response]) => dataCallback(response))
229+
.catch(errorCallback);
63230
});
64231
}
65232

@@ -120,9 +287,11 @@ export class Client {
120287
return fileDescriptorMap;
121288
}
122289

123-
private getFileContainingSymbol(
290+
private async getFileContainingSymbol(
124291
symbol: string
125292
): Promise<Array<IFileDescriptorProto> | undefined> {
293+
await this.initialize();
294+
126295
const fileDescriptorCache = this.fileDescriptorCache;
127296
return new Promise((resolve, reject) => {
128297
function dataCallback(response: ServerReflectionResponse) {
@@ -154,20 +323,20 @@ export class Client {
154323
reject(e);
155324
}
156325

157-
const request = new ServerReflectionRequest();
326+
const request = new this.CompatibleServerReflectionRequest!();
158327
request.setFileContainingSymbol(symbol);
159328

160-
const grpcCall = this.grpcClient.serverReflectionInfo(this.metadata);
161-
grpcCall.on('data', dataCallback);
162-
grpcCall.on('error', errorCallback);
163-
grpcCall.write(request);
164-
grpcCall.end();
329+
this.sendReflectionRequest(request)
330+
.then(([response]) => dataCallback(response))
331+
.catch(errorCallback);
165332
});
166333
}
167334

168-
private getFilesByFilenames(
335+
private async getFilesByFilenames(
169336
symbols: string[]
170337
): Promise<Array<IFileDescriptorProto> | undefined> {
338+
await this.initialize();
339+
171340
const result: Array<IFileDescriptorProto> = [];
172341
const fileDescriptorCache = this.fileDescriptorCache;
173342
const symbolsToFetch = symbols.filter(symbol => {
@@ -203,6 +372,13 @@ export class Client {
203372
result.push(fileDescriptorProto);
204373
}
205374
});
375+
} else if (response.hasErrorResponse()) {
376+
const err = response.getErrorResponse();
377+
reject(
378+
new Error(
379+
`Error: ${err?.getErrorCode()}: ${err?.getErrorMessage()}`
380+
)
381+
);
206382
} else {
207383
reject(Error());
208384
}
@@ -212,19 +388,17 @@ export class Client {
212388
reject(e);
213389
}
214390

215-
const grpcCall = this.grpcClient.serverReflectionInfo(this.metadata);
216-
grpcCall.on('data', dataCallback);
217-
grpcCall.on('error', errorCallback);
218-
grpcCall.on('end', () => {
219-
resolve(result);
391+
const requests = symbolsToFetch.map(symbol => {
392+
const request = new this.CompatibleServerReflectionRequest!();
393+
return request.setFileByFilename(symbol);
220394
});
221395

222-
symbolsToFetch.forEach(symbol => {
223-
const request = new ServerReflectionRequest();
224-
grpcCall.write(request.setFileByFilename(symbol));
225-
});
226-
227-
grpcCall.end();
396+
this.sendReflectionRequest(requests)
397+
.then(responses => {
398+
for (const dataBit of responses) dataCallback(dataBit);
399+
resolve(result);
400+
})
401+
.catch(errorCallback);
228402
});
229403
}
230404
}

0 commit comments

Comments
 (0)