mirror of https://github.com/grpc/grpc-node.git
refactor(grpc-reflection): file cleanup and enabled ts strict mode
This commit is contained in:
parent
215078f49a
commit
3b4f92ee62
|
@ -9,14 +9,9 @@ const INCLUDE_PATH = path.join(__dirname, '../proto/sample/vendor');
|
||||||
|
|
||||||
const server = new grpc.Server();
|
const server = new grpc.Server();
|
||||||
const packageDefinition = protoLoader.loadSync(PROTO_PATH, { includeDirs: [INCLUDE_PATH] });
|
const packageDefinition = protoLoader.loadSync(PROTO_PATH, { includeDirs: [INCLUDE_PATH] });
|
||||||
const reflection = new ReflectionService(packageDefinition);
|
const reflection = new ReflectionService(packageDefinition, { services: ['sample.SampleService'] });
|
||||||
reflection.addToServer(server);
|
reflection.addToServer(server);
|
||||||
|
|
||||||
server.bindAsync('localhost:5000', grpc.ServerCredentials.createInsecure(), () => {
|
server.bindAsync('localhost:5000', grpc.ServerCredentials.createInsecure(), () => {
|
||||||
server.start();
|
server.start();
|
||||||
});
|
});
|
||||||
|
|
||||||
// const protoDescriptor = grpc.loadPackageDefinition(packageDefinition);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,10 @@ service SampleService {
|
||||||
rpc Hello2 (HelloRequest) returns (CommonMessage) {}
|
rpc Hello2 (HelloRequest) returns (CommonMessage) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
service IgnoreService {
|
||||||
|
rpc Hello (HelloRequest) returns (HelloResponse) {}
|
||||||
|
}
|
||||||
|
|
||||||
message HelloRequest {
|
message HelloRequest {
|
||||||
string hello = 1;
|
string hello = 1;
|
||||||
HelloNested nested = 2;
|
HelloNested nested = 2;
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
/** Options to create a reflection server */
|
||||||
|
export interface ReflectionServerOptions {
|
||||||
|
/** whitelist of fully-qualified service names to expose. (Default: expose all) */
|
||||||
|
services?: string[];
|
||||||
|
}
|
|
@ -101,7 +101,7 @@ export const visit = (file: FileDescriptorProto, visitor: Visitor): void => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const packageName = file.getPackage();
|
const packageName = file.getPackage() || '';
|
||||||
file.getEnumTypeList().forEach((type) => processEnum(packageName, file, type));
|
file.getEnumTypeList().forEach((type) => processEnum(packageName, file, type));
|
||||||
file.getMessageTypeList().forEach((type) => processMessage(packageName, file, type));
|
file.getMessageTypeList().forEach((type) => processMessage(packageName, file, type));
|
||||||
file.getServiceList().forEach((service) => processService(packageName, file, service));
|
file.getServiceList().forEach((service) => processService(packageName, file, service));
|
|
@ -7,14 +7,15 @@ import {
|
||||||
import * as grpc from '@grpc/grpc-js';
|
import * as grpc from '@grpc/grpc-js';
|
||||||
import * as protoLoader from '@grpc/proto-loader';
|
import * as protoLoader from '@grpc/proto-loader';
|
||||||
|
|
||||||
import { ExtensionNumberResponse__Output } from './generated/grpc/reflection/v1/ExtensionNumberResponse';
|
import { ExtensionNumberResponse__Output } from '../generated/grpc/reflection/v1/ExtensionNumberResponse';
|
||||||
import { FileDescriptorResponse__Output } from './generated/grpc/reflection/v1/FileDescriptorResponse';
|
import { FileDescriptorResponse__Output } from '../generated/grpc/reflection/v1/FileDescriptorResponse';
|
||||||
import { ListServiceResponse__Output } from './generated/grpc/reflection/v1/ListServiceResponse';
|
import { ListServiceResponse__Output } from '../generated/grpc/reflection/v1/ListServiceResponse';
|
||||||
import { ServerReflectionRequest } from './generated/grpc/reflection/v1/ServerReflectionRequest';
|
import { ServerReflectionRequest } from '../generated/grpc/reflection/v1/ServerReflectionRequest';
|
||||||
import { ServerReflectionResponse } from './generated/grpc/reflection/v1/ServerReflectionResponse';
|
import { ServerReflectionResponse } from '../generated/grpc/reflection/v1/ServerReflectionResponse';
|
||||||
import { visit } from './protobuf-visitor';
|
import { visit } from './common/protobuf-visitor';
|
||||||
import { scope } from './utils';
|
import { scope } from './common/utils';
|
||||||
import { PROTO_LOADER_OPTS } from './constants';
|
import { PROTO_LOADER_OPTS } from './common/constants';
|
||||||
|
import { ReflectionServerOptions } from './common/interfaces';
|
||||||
|
|
||||||
export class ReflectionError extends Error {
|
export class ReflectionError extends Error {
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -37,22 +38,27 @@ export class ReflectionError extends Error {
|
||||||
export class ReflectionV1Implementation {
|
export class ReflectionV1Implementation {
|
||||||
|
|
||||||
/** The full list of proto files (including imported deps) that the gRPC server includes */
|
/** The full list of proto files (including imported deps) that the gRPC server includes */
|
||||||
private fileDescriptorSet = new FileDescriptorSet();
|
private readonly fileDescriptorSet = new FileDescriptorSet();
|
||||||
|
|
||||||
/** An index of proto files by file name (eg. 'sample.proto') */
|
/** An index of proto files by file name (eg. 'sample.proto') */
|
||||||
private fileNameIndex: Record<string, FileDescriptorProto> = {};
|
private readonly fileNameIndex: Record<string, FileDescriptorProto> = {};
|
||||||
|
|
||||||
/** An index of proto files by type extension relationship
|
/** An index of proto files by type extension relationship
|
||||||
*
|
*
|
||||||
* extensionIndex[<pkg>.<msg>][<field#>] contains a reference to the file containing an
|
* extensionIndex[<pkg>.<msg>][<field#>] contains a reference to the file containing an
|
||||||
* extension for the type "<pkg>.<msg>" and field number "<field#>"
|
* extension for the type "<pkg>.<msg>" and field number "<field#>"
|
||||||
*/
|
*/
|
||||||
private extensionIndex: Record<string, Record<number, FileDescriptorProto>> = {};
|
private readonly extensionIndex: Record<string, Record<number, FileDescriptorProto>> = {};
|
||||||
|
|
||||||
/** An index of fully qualified symbol names (eg. 'sample.Message') to the files that contain them */
|
/** An index of fully qualified symbol names (eg. 'sample.Message') to the files that contain them */
|
||||||
private symbolMap: Record<string, FileDescriptorProto> = {};
|
private readonly symbolMap: Record<string, FileDescriptorProto> = {};
|
||||||
|
|
||||||
|
/** Options that the user provided for this service */
|
||||||
|
private readonly options?: ReflectionServerOptions;
|
||||||
|
|
||||||
|
constructor(root: protoLoader.PackageDefinition, options?: ReflectionServerOptions) {
|
||||||
|
this.options = options;
|
||||||
|
|
||||||
constructor(root: protoLoader.PackageDefinition) {
|
|
||||||
Object.values(root).forEach(({ fileDescriptorProtos }) => {
|
Object.values(root).forEach(({ fileDescriptorProtos }) => {
|
||||||
// Add file descriptors to the FileDescriptorSet.
|
// Add file descriptors to the FileDescriptorSet.
|
||||||
// We use the Array check here because a ServiceDefinition could have a method named the same thing
|
// We use the Array check here because a ServiceDefinition could have a method named the same thing
|
||||||
|
@ -88,10 +94,10 @@ export class ReflectionV1Implementation {
|
||||||
extension: (fqn, file, ext) => {
|
extension: (fqn, file, ext) => {
|
||||||
index(fqn, file);
|
index(fqn, file);
|
||||||
|
|
||||||
const extendeeName = ext.getExtendee();
|
const extendeeName = ext.getExtendee() || '';
|
||||||
this.extensionIndex[extendeeName] = {
|
this.extensionIndex[extendeeName] = {
|
||||||
...(this.extensionIndex[extendeeName] || {}),
|
...(this.extensionIndex[extendeeName] || {}),
|
||||||
[ext.getNumber()]: file,
|
[ext.getNumber() || -1]: file,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
@ -126,25 +132,26 @@ export class ReflectionV1Implementation {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (referencedFile !== sourceFile) {
|
const fname = referencedFile.getName();
|
||||||
sourceFile.addDependency(referencedFile.getName());
|
if (referencedFile !== sourceFile && fname) {
|
||||||
|
sourceFile.addDependency(fname);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this.fileDescriptorSet.getFileList().forEach((file) =>
|
this.fileDescriptorSet.getFileList().forEach((file) =>
|
||||||
visit(file, {
|
visit(file, {
|
||||||
field: (fqn, file, field) => addReference(field.getTypeName(), file, scope(fqn)),
|
field: (fqn, file, field) => addReference(field.getTypeName() || '', file, scope(fqn)),
|
||||||
extension: (fqn, file, ext) => addReference(ext.getTypeName(), file, scope(fqn)),
|
extension: (fqn, file, ext) => addReference(ext.getTypeName() || '', file, scope(fqn)),
|
||||||
method: (fqn, file, method) => {
|
method: (fqn, file, method) => {
|
||||||
addReference(method.getInputType(), file, scope(fqn));
|
addReference(method.getInputType() || '', file, scope(fqn));
|
||||||
addReference(method.getOutputType(), file, scope(fqn));
|
addReference(method.getOutputType() || '', file, scope(fqn));
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
addToServer(server: Pick<grpc.Server, 'addService'>) {
|
addToServer(server: Pick<grpc.Server, 'addService'>) {
|
||||||
const protoPath = path.join(__dirname, '../proto/grpc/reflection/v1/reflection.proto');
|
const protoPath = path.join(__dirname, '../../proto/grpc/reflection/v1/reflection.proto');
|
||||||
const pkgDefinition = protoLoader.loadSync(protoPath, PROTO_LOADER_OPTS);
|
const pkgDefinition = protoLoader.loadSync(protoPath, PROTO_LOADER_OPTS);
|
||||||
const pkg = grpc.loadPackageDefinition(pkgDefinition) as any;
|
const pkg = grpc.loadPackageDefinition(pkgDefinition) as any;
|
||||||
|
|
||||||
|
@ -180,8 +187,10 @@ export class ReflectionV1Implementation {
|
||||||
} else if (message.fileByFilename !== undefined) {
|
} else if (message.fileByFilename !== undefined) {
|
||||||
response.fileDescriptorResponse = this.fileByFilename(message.fileByFilename);
|
response.fileDescriptorResponse = this.fileByFilename(message.fileByFilename);
|
||||||
} else if (message.fileContainingExtension !== undefined) {
|
} else if (message.fileContainingExtension !== undefined) {
|
||||||
const { containingType, extensionNumber } = message.fileContainingExtension;
|
response.fileDescriptorResponse = this.fileContainingExtension(
|
||||||
response.fileDescriptorResponse = this.fileContainingExtension(containingType, extensionNumber);
|
message.fileContainingExtension?.containingType || '',
|
||||||
|
message.fileContainingExtension?.extensionNumber || -1
|
||||||
|
);
|
||||||
} else if (message.allExtensionNumbersOfType) {
|
} else if (message.allExtensionNumbersOfType) {
|
||||||
response.allExtensionNumbersResponse = this.allExtensionNumbersOfType(message.allExtensionNumbersOfType);
|
response.allExtensionNumbersResponse = this.allExtensionNumbersOfType(message.allExtensionNumbersOfType);
|
||||||
} else {
|
} else {
|
||||||
|
@ -224,7 +233,12 @@ export class ReflectionV1Implementation {
|
||||||
)
|
)
|
||||||
.flat();
|
.flat();
|
||||||
|
|
||||||
return { service: services.map((service) => ({ name: service })) };
|
const whitelist = new Set(this.options?.services ?? undefined);
|
||||||
|
const exposedServices = this.options?.services ?
|
||||||
|
services.filter(service => whitelist.has(service))
|
||||||
|
: services;
|
||||||
|
|
||||||
|
return { service: exposedServices.map((service) => ({ name: service })) };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Find the proto file(s) that declares the given fully-qualified symbol name
|
/** Find the proto file(s) that declares the given fully-qualified symbol name
|
|
@ -3,10 +3,10 @@ import * as path from 'path';
|
||||||
import * as grpc from '@grpc/grpc-js';
|
import * as grpc from '@grpc/grpc-js';
|
||||||
import * as protoLoader from '@grpc/proto-loader';
|
import * as protoLoader from '@grpc/proto-loader';
|
||||||
|
|
||||||
import { ServerReflectionRequest } from './generated/grpc/reflection/v1/ServerReflectionRequest';
|
import { ServerReflectionRequest } from '../generated/grpc/reflection/v1/ServerReflectionRequest';
|
||||||
import { ServerReflectionResponse } from './generated/grpc/reflection/v1/ServerReflectionResponse';
|
import { ServerReflectionResponse } from '../generated/grpc/reflection/v1/ServerReflectionResponse';
|
||||||
import { PROTO_LOADER_OPTS } from './constants';
|
import { PROTO_LOADER_OPTS } from './common/constants';
|
||||||
import { ReflectionV1Implementation } from './reflection-v1-implementation';
|
import { ReflectionV1Implementation } from './reflection-v1';
|
||||||
|
|
||||||
|
|
||||||
/** Analyzes a gRPC server and exposes methods to reflect on it
|
/** Analyzes a gRPC server and exposes methods to reflect on it
|
||||||
|
@ -18,12 +18,12 @@ import { ReflectionV1Implementation } from './reflection-v1-implementation';
|
||||||
* For example: if files 'a.proto' and 'b.proto' are both for the same package 'c' then
|
* For example: if files 'a.proto' and 'b.proto' are both for the same package 'c' then
|
||||||
* we will always return a reference to a combined 'c.proto' instead of the 2 files.
|
* we will always return a reference to a combined 'c.proto' instead of the 2 files.
|
||||||
*
|
*
|
||||||
* @remarks as the v1 and v1alpha specs are identical, this implementation extends v1
|
* @privateRemarks as the v1 and v1alpha specs are identical, this implementation extends
|
||||||
* and just exposes it at the v1alpha package instead
|
* reflection-v1 and exposes it at the v1alpha package instead
|
||||||
*/
|
*/
|
||||||
export class ReflectionV1AlphaImplementation extends ReflectionV1Implementation {
|
export class ReflectionV1AlphaImplementation extends ReflectionV1Implementation {
|
||||||
addToServer(server: Pick<grpc.Server, 'addService'>) {
|
addToServer(server: Pick<grpc.Server, 'addService'>) {
|
||||||
const protoPath = path.join(__dirname, '../proto/grpc/reflection/v1alpha/reflection.proto');
|
const protoPath = path.join(__dirname, '../../proto/grpc/reflection/v1alpha/reflection.proto');
|
||||||
const pkgDefinition = protoLoader.loadSync(protoPath, PROTO_LOADER_OPTS);
|
const pkgDefinition = protoLoader.loadSync(protoPath, PROTO_LOADER_OPTS);
|
||||||
const pkg = grpc.loadPackageDefinition(pkgDefinition) as any;
|
const pkg = grpc.loadPackageDefinition(pkgDefinition) as any;
|
||||||
|
|
|
@ -1,13 +1,9 @@
|
||||||
import * as grpc from '@grpc/grpc-js';
|
import * as grpc from '@grpc/grpc-js';
|
||||||
import * as protoLoader from '@grpc/proto-loader';
|
import * as protoLoader from '@grpc/proto-loader';
|
||||||
|
|
||||||
import { ReflectionV1Implementation } from './reflection-v1-implementation';
|
import { ReflectionV1Implementation } from './implementations/reflection-v1';
|
||||||
import { ReflectionV1AlphaImplementation } from './reflection-v1alpha';
|
import { ReflectionV1AlphaImplementation } from './implementations/reflection-v1alpha';
|
||||||
|
import { ReflectionServerOptions } from './implementations/common/interfaces';
|
||||||
interface ReflectionServerOptions {
|
|
||||||
/** whitelist of fully-qualified service names to expose. (Default: expose all) */
|
|
||||||
services?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Analyzes a gRPC package and exposes endpoints providing information about
|
/** Analyzes a gRPC package and exposes endpoints providing information about
|
||||||
* it according to the gRPC Server Reflection API Specification
|
* it according to the gRPC Server Reflection API Specification
|
||||||
|
@ -31,21 +27,8 @@ export class ReflectionService {
|
||||||
private readonly v1Alpha: ReflectionV1AlphaImplementation;
|
private readonly v1Alpha: ReflectionV1AlphaImplementation;
|
||||||
|
|
||||||
constructor(pkg: protoLoader.PackageDefinition, options?: ReflectionServerOptions) {
|
constructor(pkg: protoLoader.PackageDefinition, options?: ReflectionServerOptions) {
|
||||||
|
this.v1 = new ReflectionV1Implementation(pkg, options);
|
||||||
if (options.services) {
|
this.v1Alpha = new ReflectionV1AlphaImplementation(pkg, options);
|
||||||
const whitelist = new Set(options.services);
|
|
||||||
|
|
||||||
for (const key in Object.keys(pkg)) {
|
|
||||||
const value = pkg[key];
|
|
||||||
const isService = value.format !== 'Protocol Buffer 3 DescriptorProto' && value.format !== 'Protocol Buffer 3 EnumDescriptorProto';
|
|
||||||
if (isService && !whitelist.has(key)) {
|
|
||||||
delete pkg[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.v1 = new ReflectionV1Implementation(pkg);
|
|
||||||
this.v1Alpha = new ReflectionV1AlphaImplementation(pkg);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addToServer(server: Pick<grpc.Server, 'addService'>) {
|
addToServer(server: Pick<grpc.Server, 'addService'>) {
|
||||||
|
|
|
@ -3,14 +3,12 @@ import * as path from 'path';
|
||||||
import { FileDescriptorProto } from 'google-protobuf/google/protobuf/descriptor_pb';
|
import { FileDescriptorProto } from 'google-protobuf/google/protobuf/descriptor_pb';
|
||||||
import * as protoLoader from '@grpc/proto-loader';
|
import * as protoLoader from '@grpc/proto-loader';
|
||||||
|
|
||||||
import { ReflectionV1Implementation } from '../src/reflection-v1-implementation';
|
import { ReflectionV1Implementation } from '../src/implementations/reflection-v1';
|
||||||
|
|
||||||
describe('GrpcReflectionService', () => {
|
describe('GrpcReflectionService', () => {
|
||||||
let reflectionService: ReflectionV1Implementation;
|
let reflectionService: ReflectionV1Implementation;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
console.log(path.join(__dirname, '../proto/sample/sample.proto'));
|
|
||||||
console.log([path.join(__dirname, '../proto/sample/vendor')]);
|
|
||||||
const root = protoLoader.loadSync(path.join(__dirname, '../proto/sample/sample.proto'), {
|
const root = protoLoader.loadSync(path.join(__dirname, '../proto/sample/sample.proto'), {
|
||||||
includeDirs: [path.join(__dirname, '../proto/sample/vendor')]
|
includeDirs: [path.join(__dirname, '../proto/sample/vendor')]
|
||||||
});
|
});
|
||||||
|
@ -20,6 +18,18 @@ describe('GrpcReflectionService', () => {
|
||||||
|
|
||||||
describe('listServices()', () => {
|
describe('listServices()', () => {
|
||||||
it('lists all services', () => {
|
it('lists all services', () => {
|
||||||
|
const { service: services } = reflectionService.listServices('*');
|
||||||
|
assert.equal(services.length, 2);
|
||||||
|
assert(services.find((s) => s.name === 'sample.SampleService'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('whitelists services properly', () => {
|
||||||
|
const root = protoLoader.loadSync(path.join(__dirname, '../proto/sample/sample.proto'), {
|
||||||
|
includeDirs: [path.join(__dirname, '../proto/sample/vendor')]
|
||||||
|
});
|
||||||
|
|
||||||
|
reflectionService = new ReflectionV1Implementation(root, { services: ['sample.SampleService'] });
|
||||||
|
|
||||||
const { service: services } = reflectionService.listServices('*');
|
const { service: services } = reflectionService.listServices('*');
|
||||||
assert.equal(services.length, 1);
|
assert.equal(services.length, 1);
|
||||||
assert(services.find((s) => s.name === 'sample.SampleService'));
|
assert(services.find((s) => s.name === 'sample.SampleService'));
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import * as assert from 'assert';
|
import * as assert from 'assert';
|
||||||
|
|
||||||
import { scope } from '../src/utils';
|
import { scope } from '../src/implementations/common/utils';
|
||||||
|
|
||||||
describe('scope', () => {
|
describe('scope', () => {
|
||||||
it('traverses upwards in the package scope', () => {
|
it('traverses upwards in the package scope', () => {
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
"noImplicitReturns": true,
|
"noImplicitReturns": true,
|
||||||
"pretty": true,
|
"pretty": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"strictNullChecks": false,
|
"strict": true,
|
||||||
"lib": ["es2017"],
|
"lib": ["es2017"],
|
||||||
"outDir": "build",
|
"outDir": "build",
|
||||||
"target": "es2017",
|
"target": "es2017",
|
||||||
|
|
Loading…
Reference in New Issue