@rsdk/grpc.server 4.0.0-next.0 → 4.0.0-next.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +60 -0
- package/dist/controllers/health.controller.js +4 -4
- package/dist/controllers/health.controller.js.map +1 -1
- package/dist/controllers/index.d.ts +2 -0
- package/dist/controllers/index.js +2 -0
- package/dist/controllers/index.js.map +1 -1
- package/dist/controllers/metrics.controller.js +2 -1
- package/dist/controllers/metrics.controller.js.map +1 -1
- package/dist/controllers/reflection/v1.base.controller.d.ts +32 -0
- package/dist/controllers/reflection/v1.base.controller.js +177 -0
- package/dist/controllers/reflection/v1.base.controller.js.map +1 -0
- package/dist/controllers/reflection/v1.controller.d.ts +10 -0
- package/dist/controllers/reflection/v1.controller.js +25 -0
- package/dist/controllers/reflection/v1.controller.js.map +1 -0
- package/dist/controllers/reflection/v1alpha.controller.d.ts +10 -0
- package/dist/controllers/reflection/v1alpha.controller.js +25 -0
- package/dist/controllers/reflection/v1alpha.controller.js.map +1 -0
- package/dist/grpc-logger.interceptor.d.ts +10 -0
- package/dist/grpc-logger.interceptor.js +73 -0
- package/dist/grpc-logger.interceptor.js.map +1 -0
- package/dist/grpc.config.d.ts +2 -0
- package/dist/grpc.config.js +18 -1
- package/dist/grpc.config.js.map +1 -1
- package/dist/grpc.transport.d.ts +13 -1
- package/dist/grpc.transport.js +55 -11
- package/dist/grpc.transport.js.map +1 -1
- package/dist/grpc.utils.d.ts +9 -0
- package/dist/grpc.utils.js +18 -0
- package/dist/grpc.utils.js.map +1 -0
- package/dist/reflection.module.d.ts +5 -0
- package/dist/reflection.module.js +33 -0
- package/dist/reflection.module.js.map +1 -0
- package/dist/reflection.service.d.ts +43 -0
- package/dist/reflection.service.js +151 -0
- package/dist/reflection.service.js.map +1 -0
- package/jest.config.js +11 -0
- package/package.json +17 -12
- package/src/controllers/index.ts +2 -0
- package/src/controllers/reflection/v1.base.controller.ts +173 -0
- package/src/controllers/reflection/v1.controller.ts +25 -0
- package/src/controllers/reflection/v1alpha.controller.ts +25 -0
- package/src/grpc-logger.interceptor.ts +72 -0
- package/src/grpc.config.ts +13 -0
- package/src/grpc.transport.ts +64 -15
- package/src/grpc.utils.ts +20 -0
- package/src/reflection.module.ts +26 -0
- package/src/reflection.service.spec.ts +86 -0
- package/src/reflection.service.ts +210 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ServerReflectionGrpcController,
|
|
3
|
+
ServerReflectionRequest,
|
|
4
|
+
ServerReflectionResponse,
|
|
5
|
+
} from '@rsdk/builtin-contract/dist/grpc.reflection.v1';
|
|
6
|
+
import { ServerReflectionGrpcMethods } from '@rsdk/builtin-contract/dist/grpc.reflection.v1';
|
|
7
|
+
import type { Observable } from 'rxjs';
|
|
8
|
+
|
|
9
|
+
import { BaseV1GrpcReflectionController } from './v1.base.controller';
|
|
10
|
+
|
|
11
|
+
/** Implements the 'v1' (final) version of the gRPC Reflection API spec
|
|
12
|
+
*
|
|
13
|
+
* @see {@link https://github.com/grpc/grpc/blob/master/doc/server-reflection.md}
|
|
14
|
+
*/
|
|
15
|
+
@ServerReflectionGrpcMethods()
|
|
16
|
+
export class ReflectionV1Controller
|
|
17
|
+
extends BaseV1GrpcReflectionController
|
|
18
|
+
implements ServerReflectionGrpcController
|
|
19
|
+
{
|
|
20
|
+
override serverReflectionInfo(
|
|
21
|
+
request: Observable<ServerReflectionRequest>,
|
|
22
|
+
): Observable<ServerReflectionResponse> {
|
|
23
|
+
return super.serverReflectionInfo(request);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { ServerReflectionGrpcMethods } from '@rsdk/builtin-contract/dist/grpc.reflection.v1';
|
|
2
|
+
import type {
|
|
3
|
+
ServerReflectionGrpcController,
|
|
4
|
+
ServerReflectionRequest,
|
|
5
|
+
ServerReflectionResponse,
|
|
6
|
+
} from '@rsdk/builtin-contract/dist/grpc.reflection.v1alpha';
|
|
7
|
+
import type { Observable } from 'rxjs';
|
|
8
|
+
|
|
9
|
+
import { BaseV1GrpcReflectionController } from './v1.base.controller';
|
|
10
|
+
|
|
11
|
+
/** Implements the 'v1alpha' version of the gRPC Reflection API spec
|
|
12
|
+
*
|
|
13
|
+
* @see {@link https://github.com/grpc/grpc/blob/master/doc/server-reflection.md}
|
|
14
|
+
*/
|
|
15
|
+
@ServerReflectionGrpcMethods()
|
|
16
|
+
export class ReflectionV1alphaController
|
|
17
|
+
extends BaseV1GrpcReflectionController
|
|
18
|
+
implements ServerReflectionGrpcController
|
|
19
|
+
{
|
|
20
|
+
override serverReflectionInfo(
|
|
21
|
+
request: Observable<ServerReflectionRequest>,
|
|
22
|
+
): Observable<ServerReflectionResponse> {
|
|
23
|
+
return super.serverReflectionInfo(request);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CallHandler,
|
|
3
|
+
ExecutionContext,
|
|
4
|
+
NestInterceptor,
|
|
5
|
+
} from '@nestjs/common/interfaces';
|
|
6
|
+
import { InjectLogger } from '@rsdk/core';
|
|
7
|
+
import { ILogger } from '@rsdk/logging';
|
|
8
|
+
import type { Observable } from 'rxjs';
|
|
9
|
+
import { catchError, tap, throwError } from 'rxjs';
|
|
10
|
+
|
|
11
|
+
import { GRPCConfig } from './grpc.config';
|
|
12
|
+
|
|
13
|
+
export class GrpcLoggerInterceptor implements NestInterceptor {
|
|
14
|
+
constructor(
|
|
15
|
+
@InjectLogger(GrpcLoggerInterceptor) private logger: ILogger,
|
|
16
|
+
private grpcConfig: GRPCConfig,
|
|
17
|
+
) {}
|
|
18
|
+
|
|
19
|
+
intercept(
|
|
20
|
+
context: ExecutionContext,
|
|
21
|
+
next: CallHandler<any>,
|
|
22
|
+
): Observable<any> | Promise<Observable<any>> {
|
|
23
|
+
if (!this.grpcConfig.requestLogging) {
|
|
24
|
+
return next.handle();
|
|
25
|
+
}
|
|
26
|
+
const isRpc = context.getType() === 'rpc';
|
|
27
|
+
if (!isRpc) {
|
|
28
|
+
return next.handle();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const rpc = context.switchToRpc();
|
|
32
|
+
|
|
33
|
+
const startDate = Date.now();
|
|
34
|
+
const rpcContext = rpc.getContext();
|
|
35
|
+
const serializedCtx = rpcContext?.toJSON?.() ?? rpcContext;
|
|
36
|
+
const requestForLog = {
|
|
37
|
+
data: rpc.getData(),
|
|
38
|
+
ctx: serializedCtx,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
this.logger.trace('received rpc request', {
|
|
42
|
+
request: requestForLog,
|
|
43
|
+
timestamp: startDate,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return next.handle().pipe(
|
|
47
|
+
catchError((error) => {
|
|
48
|
+
const endDate = Date.now();
|
|
49
|
+
|
|
50
|
+
this.logger.trace('handled rpc request with error', {
|
|
51
|
+
request: requestForLog,
|
|
52
|
+
startDate,
|
|
53
|
+
endDate,
|
|
54
|
+
duration: endDate - startDate,
|
|
55
|
+
error,
|
|
56
|
+
});
|
|
57
|
+
return throwError(() => error);
|
|
58
|
+
}),
|
|
59
|
+
tap((response) => {
|
|
60
|
+
const endDate = Date.now();
|
|
61
|
+
|
|
62
|
+
this.logger.trace('handled rpc request', {
|
|
63
|
+
request: requestForLog,
|
|
64
|
+
startDate,
|
|
65
|
+
endDate,
|
|
66
|
+
duration: endDate - startDate,
|
|
67
|
+
response,
|
|
68
|
+
});
|
|
69
|
+
}),
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
}
|
package/src/grpc.config.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
} from '@grpc/grpc-js/build/src/constants';
|
|
5
5
|
import { text } from '@rsdk/common';
|
|
6
6
|
import {
|
|
7
|
+
BoolParser,
|
|
7
8
|
Config,
|
|
8
9
|
ConfigSection,
|
|
9
10
|
ConfigTag,
|
|
@@ -36,4 +37,16 @@ export class GRPCConfig extends Config {
|
|
|
36
37
|
description: 'Max gRPC message size (send)',
|
|
37
38
|
})
|
|
38
39
|
readonly maxSend!: number;
|
|
40
|
+
|
|
41
|
+
@Property('GRPC_REFLECTION', new BoolParser(), {
|
|
42
|
+
defaultValue: false,
|
|
43
|
+
description: 'Enable reflection',
|
|
44
|
+
})
|
|
45
|
+
readonly reflection!: boolean;
|
|
46
|
+
|
|
47
|
+
@Property('GRPC_REQUEST_LOGGING', new BoolParser(), {
|
|
48
|
+
defaultValue: false,
|
|
49
|
+
description: 'Enable trace logging all requests',
|
|
50
|
+
})
|
|
51
|
+
readonly requestLogging!: boolean;
|
|
39
52
|
}
|
package/src/grpc.transport.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import type { Controller } from '@nestjs/common/interfaces';
|
|
2
|
+
import { APP_INTERCEPTOR } from '@nestjs/core';
|
|
2
3
|
import type { MicroserviceOptions } from '@nestjs/microservices';
|
|
3
4
|
import HealthGrpcPackage from '@rsdk/builtin-contract/dist/grpc.health.v1';
|
|
4
5
|
import MetricsGrpcPackage from '@rsdk/builtin-contract/dist/grpc.metrics.v1';
|
|
6
|
+
import ReflectionGrpcPackageV1 from '@rsdk/builtin-contract/dist/grpc.reflection.v1';
|
|
7
|
+
import ReflectionGrpcPackageV1alpha from '@rsdk/builtin-contract/dist/grpc.reflection.v1alpha';
|
|
5
8
|
import type { Constructor } from '@rsdk/common';
|
|
6
9
|
import type {
|
|
7
10
|
ConfigContext,
|
|
@@ -9,6 +12,7 @@ import type {
|
|
|
9
12
|
IErrorsSender,
|
|
10
13
|
IErrorsTransformer,
|
|
11
14
|
IMicroserviceTransport,
|
|
15
|
+
NestModuleDefinitions,
|
|
12
16
|
} from '@rsdk/core';
|
|
13
17
|
import { SequenceException } from '@rsdk/core';
|
|
14
18
|
import type { Package } from '@rsdk/grpc.loader';
|
|
@@ -17,10 +21,20 @@ import { LoggerFactory } from '@rsdk/logging';
|
|
|
17
21
|
|
|
18
22
|
import { HealthController, MetricsController } from './controllers';
|
|
19
23
|
import { GRPCConfig } from './grpc.config';
|
|
24
|
+
import { isPackageNotIncluded } from './grpc.utils';
|
|
20
25
|
import { GrpcErrorsFormatter } from './grpc-errors.formatter';
|
|
21
26
|
import { GrpcErrorsSender } from './grpc-errors.sender';
|
|
27
|
+
import { GrpcLoggerInterceptor } from './grpc-logger.interceptor';
|
|
28
|
+
import { ReflectionModule } from './reflection.module';
|
|
22
29
|
|
|
23
30
|
export class GrpcTransport implements IMicroserviceTransport {
|
|
31
|
+
private readonly systemPackages: Package[] = [
|
|
32
|
+
HealthGrpcPackage,
|
|
33
|
+
MetricsGrpcPackage,
|
|
34
|
+
ReflectionGrpcPackageV1,
|
|
35
|
+
ReflectionGrpcPackageV1alpha,
|
|
36
|
+
];
|
|
37
|
+
|
|
24
38
|
private readonly logger = LoggerFactory.create(GrpcTransport);
|
|
25
39
|
private readonly packages: Package[];
|
|
26
40
|
|
|
@@ -58,6 +72,21 @@ export class GrpcTransport implements IMicroserviceTransport {
|
|
|
58
72
|
return HealthController;
|
|
59
73
|
}
|
|
60
74
|
|
|
75
|
+
modules(): NestModuleDefinitions {
|
|
76
|
+
return [
|
|
77
|
+
ReflectionModule.forRoot(this.packages),
|
|
78
|
+
{
|
|
79
|
+
module: GrpcTransport,
|
|
80
|
+
providers: [
|
|
81
|
+
{
|
|
82
|
+
provide: APP_INTERCEPTOR,
|
|
83
|
+
useClass: GrpcLoggerInterceptor,
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
];
|
|
88
|
+
}
|
|
89
|
+
|
|
61
90
|
/**
|
|
62
91
|
* Unfortunally we can't access GRPCConfig in constructor because
|
|
63
92
|
* PlatformConfigModule is not initilalized yet
|
|
@@ -72,17 +101,7 @@ export class GrpcTransport implements IMicroserviceTransport {
|
|
|
72
101
|
}
|
|
73
102
|
|
|
74
103
|
createMicroserviceOptions(): MicroserviceOptions {
|
|
75
|
-
this.
|
|
76
|
-
|
|
77
|
-
const packages = [...this.packages];
|
|
78
|
-
|
|
79
|
-
if (!packages.some((x) => x.name === HealthGrpcPackage.name)) {
|
|
80
|
-
packages.push(HealthGrpcPackage);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (!packages.some((x) => x.name === MetricsGrpcPackage.name)) {
|
|
84
|
-
packages.push(MetricsGrpcPackage);
|
|
85
|
-
}
|
|
104
|
+
const packages = this.getUniquePackages();
|
|
86
105
|
|
|
87
106
|
for (const pkg of packages) {
|
|
88
107
|
for (const service of Object.keys(pkg.definitions['grpc-js'])) {
|
|
@@ -90,11 +109,15 @@ export class GrpcTransport implements IMicroserviceTransport {
|
|
|
90
109
|
}
|
|
91
110
|
}
|
|
92
111
|
|
|
112
|
+
this.checkIsInitialized();
|
|
113
|
+
this.assertIsDefined(this.address);
|
|
114
|
+
this.assertIsDefined(this.maxRecv);
|
|
115
|
+
this.assertIsDefined(this.maxSend);
|
|
116
|
+
|
|
93
117
|
return createGrpcOptions(packages, {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
maxSend: this.maxSend!,
|
|
118
|
+
address: this.address,
|
|
119
|
+
maxRecv: this.maxRecv,
|
|
120
|
+
maxSend: this.maxSend,
|
|
98
121
|
});
|
|
99
122
|
}
|
|
100
123
|
|
|
@@ -109,4 +132,30 @@ export class GrpcTransport implements IMicroserviceTransport {
|
|
|
109
132
|
throw new SequenceException('You should call init() method first!');
|
|
110
133
|
}
|
|
111
134
|
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Checks the value must be defined and not null
|
|
138
|
+
* @param value any value
|
|
139
|
+
*/
|
|
140
|
+
private assertIsDefined<T>(value: T): asserts value is NonNullable<T> {
|
|
141
|
+
if (value === undefined || value === null) {
|
|
142
|
+
throw new Error(`${value} is not defined`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Merge user packages with system packages
|
|
148
|
+
* @returns array of unique packages
|
|
149
|
+
*/
|
|
150
|
+
private getUniquePackages(): Package[] {
|
|
151
|
+
const packages = [...this.packages];
|
|
152
|
+
|
|
153
|
+
for (const pkg of this.systemPackages) {
|
|
154
|
+
if (isPackageNotIncluded(packages, pkg)) {
|
|
155
|
+
packages.push(pkg);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return packages;
|
|
160
|
+
}
|
|
112
161
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Match packages name
|
|
3
|
+
* @param pkg
|
|
4
|
+
* @returns true if names is equal
|
|
5
|
+
*/
|
|
6
|
+
const isEqualPackageName =
|
|
7
|
+
<T extends { name: string }>(pkg: T) =>
|
|
8
|
+
(x: T): boolean =>
|
|
9
|
+
x.name === pkg.name;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Checks whether a pkg is in the array
|
|
13
|
+
* @param packages array of packages
|
|
14
|
+
* @param pkg package you are looking for
|
|
15
|
+
* @returns true if pkg exists in packages
|
|
16
|
+
*/
|
|
17
|
+
export const isPackageNotIncluded = <T extends { name: string }>(
|
|
18
|
+
packages: T[],
|
|
19
|
+
pkg: T,
|
|
20
|
+
): boolean => !packages.some(isEqualPackageName(pkg));
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { DynamicModule } from '@nestjs/common';
|
|
2
|
+
import { Module } from '@nestjs/common';
|
|
3
|
+
import type { Package } from '@rsdk/grpc.loader';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
ReflectionV1alphaController,
|
|
7
|
+
ReflectionV1Controller,
|
|
8
|
+
} from './controllers';
|
|
9
|
+
import { ReflectionService } from './reflection.service';
|
|
10
|
+
|
|
11
|
+
@Module({})
|
|
12
|
+
export class ReflectionModule {
|
|
13
|
+
static forRoot(packages: Package[]): DynamicModule {
|
|
14
|
+
return {
|
|
15
|
+
module: ReflectionModule,
|
|
16
|
+
controllers: [ReflectionV1alphaController, ReflectionV1Controller],
|
|
17
|
+
providers: [
|
|
18
|
+
ReflectionService,
|
|
19
|
+
{
|
|
20
|
+
provide: 'GRPC_PACKAGES',
|
|
21
|
+
useValue: packages,
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { FileDescriptorProto } from '@bufbuild/protobuf';
|
|
2
|
+
import type { PlatformTestingModule } from '@rsdk/testing';
|
|
3
|
+
import { PlatformTest } from '@rsdk/testing';
|
|
4
|
+
|
|
5
|
+
import { ReflectionService } from './reflection.service';
|
|
6
|
+
|
|
7
|
+
describe('ReflectionService', () => {
|
|
8
|
+
let testingModule: PlatformTestingModule;
|
|
9
|
+
let reflectionService: ReflectionService;
|
|
10
|
+
|
|
11
|
+
beforeAll(async () => {
|
|
12
|
+
const platformTesting = await PlatformTest.create({});
|
|
13
|
+
|
|
14
|
+
testingModule = await platformTesting
|
|
15
|
+
.createTestingModule({
|
|
16
|
+
providers: [
|
|
17
|
+
ReflectionService,
|
|
18
|
+
{
|
|
19
|
+
provide: 'GRPC_PACKAGES',
|
|
20
|
+
useValue: [],
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
})
|
|
24
|
+
.compile();
|
|
25
|
+
|
|
26
|
+
reflectionService = testingModule.get(ReflectionService);
|
|
27
|
+
|
|
28
|
+
reflectionService.onModuleInit();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
afterAll(async () => {
|
|
32
|
+
await testingModule.close();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('listServices()', () => {
|
|
36
|
+
it('lists all services', () => {
|
|
37
|
+
const services = reflectionService.listServices();
|
|
38
|
+
|
|
39
|
+
expect(services).toHaveLength(4);
|
|
40
|
+
expect(services).toContain('grpc.health.v1.Health');
|
|
41
|
+
expect(services).toContain('grpc.metrics.v1.Metrics');
|
|
42
|
+
expect(services).toContain('grpc.reflection.v1.ServerReflection');
|
|
43
|
+
expect(services).toContain('grpc.reflection.v1alpha.ServerReflection');
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('fileByFilename()', () => {
|
|
48
|
+
it('finds files with no transitive dependencies', () => {
|
|
49
|
+
const descriptor = FileDescriptorProto.fromBinary(
|
|
50
|
+
reflectionService.fileByName('grpc/health/v1/health.proto'),
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
expect(descriptor.name).toEqual('grpc/health/v1/health.proto');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('errors with no file found', () => {
|
|
57
|
+
expect(() => reflectionService.fileByName('nonexistent.proto')).toThrow(
|
|
58
|
+
'file: nonexistent.proto not found',
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('fileContainingSymbol()', () => {
|
|
64
|
+
it('finds symbols and returns transitive file dependencies', () => {
|
|
65
|
+
const descriptor = FileDescriptorProto.fromBinary(
|
|
66
|
+
reflectionService.fileBySymbol('grpc.health.v1.HealthCheckRequest'),
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
expect(descriptor.name).toEqual('grpc/health/v1/health.proto');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('resolves references to method types', () => {
|
|
73
|
+
const descriptor = FileDescriptorProto.fromBinary(
|
|
74
|
+
reflectionService.fileBySymbol('grpc.health.v1.Health.Check'),
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
expect(descriptor.name).toEqual('grpc/health/v1/health.proto');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('errors with no symbol found', () => {
|
|
81
|
+
expect(() =>
|
|
82
|
+
reflectionService.fileBySymbol('non.existant.symbol'),
|
|
83
|
+
).toThrow('symbol: non.existant.symbol not found');
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
EnumDescriptorProto,
|
|
3
|
+
MethodDescriptorProto,
|
|
4
|
+
} from '@bufbuild/protobuf';
|
|
5
|
+
import {
|
|
6
|
+
DescriptorProto,
|
|
7
|
+
FileDescriptorProto,
|
|
8
|
+
FileDescriptorSet,
|
|
9
|
+
ServiceDescriptorProto,
|
|
10
|
+
} from '@bufbuild/protobuf';
|
|
11
|
+
import type { OnModuleInit } from '@nestjs/common';
|
|
12
|
+
import { Inject, Injectable } from '@nestjs/common';
|
|
13
|
+
import HealthGrpcPackage from '@rsdk/builtin-contract/dist/grpc.health.v1';
|
|
14
|
+
import MetricsGrpcPackage from '@rsdk/builtin-contract/dist/grpc.metrics.v1';
|
|
15
|
+
import ReflectionGrpcPackageV1 from '@rsdk/builtin-contract/dist/grpc.reflection.v1';
|
|
16
|
+
import ReflectionGrpcPackageV1alpha from '@rsdk/builtin-contract/dist/grpc.reflection.v1alpha';
|
|
17
|
+
import type { Package } from '@rsdk/grpc.loader';
|
|
18
|
+
import { LoggerFactory } from '@rsdk/logging';
|
|
19
|
+
|
|
20
|
+
@Injectable()
|
|
21
|
+
export class ReflectionService implements OnModuleInit {
|
|
22
|
+
private readonly services: Set<string> = new Set();
|
|
23
|
+
private readonly files: FileDescriptorProto[] = [];
|
|
24
|
+
private readonly logger = LoggerFactory.create(ReflectionService);
|
|
25
|
+
|
|
26
|
+
constructor(@Inject('GRPC_PACKAGES') private readonly packages: Package[]) {}
|
|
27
|
+
|
|
28
|
+
onModuleInit(): void {
|
|
29
|
+
const allPackages = [
|
|
30
|
+
...this.packages,
|
|
31
|
+
HealthGrpcPackage,
|
|
32
|
+
MetricsGrpcPackage,
|
|
33
|
+
ReflectionGrpcPackageV1,
|
|
34
|
+
ReflectionGrpcPackageV1alpha,
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
const allPackagesNames = new Set(allPackages.map((pkg) => pkg.name));
|
|
38
|
+
|
|
39
|
+
for (const pkg of allPackages) {
|
|
40
|
+
if (pkg.descriptor) {
|
|
41
|
+
for (const service of Object.keys(pkg.definitions['grpc-js'])) {
|
|
42
|
+
this.logger.info(
|
|
43
|
+
`GRPC descriptor loaded for service: ${pkg.name}.${service}`,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
for (const file of FileDescriptorSet.fromBinary(pkg.descriptor).file) {
|
|
48
|
+
this.files.push(file);
|
|
49
|
+
|
|
50
|
+
if (allPackagesNames.has(`${file.package}`)) {
|
|
51
|
+
for (const service of file.service) {
|
|
52
|
+
this.services.add(`${file.package}.${service.name}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* List the full names of registered gRPC services
|
|
62
|
+
*
|
|
63
|
+
* note: the spec is unclear as to what the 'listServices' param can be; most
|
|
64
|
+
* clients seem to only pass '*' but unsure if this should behave like a
|
|
65
|
+
* filter. Until we know how this should behave with different inputs this
|
|
66
|
+
* just always returns *all* services.
|
|
67
|
+
*
|
|
68
|
+
* @returns full-qualified service names (eg. 'sample.SampleService')
|
|
69
|
+
*/
|
|
70
|
+
listServices(): string[] {
|
|
71
|
+
return [...this.services];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Find the proto file(s) that declares the given fully-qualified symbol name
|
|
76
|
+
*
|
|
77
|
+
* @param symbol fully-qualified name of the symbol to lookup
|
|
78
|
+
* (e.g. package.service[.method] or package.type)
|
|
79
|
+
*
|
|
80
|
+
* @returns descriptors of the file which contains this symbol and its imports
|
|
81
|
+
*/
|
|
82
|
+
fileBySymbol(symbol: string): Buffer {
|
|
83
|
+
const file = this.files.find((file) => this.findSymbol(symbol, file));
|
|
84
|
+
|
|
85
|
+
if (file) {
|
|
86
|
+
return Buffer.from(file.toBinary());
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
throw new Error(`symbol: ${symbol} not found`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Find a proto file by the file name
|
|
94
|
+
* @returns descriptors of the file which contains this symbol and its imports
|
|
95
|
+
*/
|
|
96
|
+
fileByName(filename: string): Buffer {
|
|
97
|
+
const file = this.files.find((file) => file.name === filename);
|
|
98
|
+
|
|
99
|
+
if (file) {
|
|
100
|
+
return Buffer.from(file.toBinary());
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
throw new Error(`file: ${filename} not found`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Find a proto file containing an extension to a message type
|
|
108
|
+
* @returns descriptors of the file which contains this symbol and its imports
|
|
109
|
+
*/
|
|
110
|
+
fileContainingExtension(
|
|
111
|
+
containingType: string,
|
|
112
|
+
extensionNumber: number,
|
|
113
|
+
): Buffer {
|
|
114
|
+
const extension = this.files.find((file) => {
|
|
115
|
+
const descriptor = this.findSymbol(containingType, file);
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
descriptor instanceof DescriptorProto &&
|
|
119
|
+
descriptor.extension.some(
|
|
120
|
+
(extension) => extensionNumber === extension.number,
|
|
121
|
+
)
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
if (extension) {
|
|
126
|
+
return Buffer.from(extension.toBinary());
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
throw new Error(
|
|
130
|
+
`containingType: ${containingType}, extensionNumber: ${extensionNumber} not found`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
allExtensionNumbersOfType(symbol: string): number[] {
|
|
135
|
+
return [
|
|
136
|
+
...new Set(
|
|
137
|
+
this.files.flatMap((file) =>
|
|
138
|
+
file.extension.flatMap((_fqn, _file, ext) =>
|
|
139
|
+
ext
|
|
140
|
+
.filter((e) => e.extendee === symbol)
|
|
141
|
+
.flatMap((e) => Number(e.number)),
|
|
142
|
+
),
|
|
143
|
+
),
|
|
144
|
+
),
|
|
145
|
+
];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private findSymbol(
|
|
149
|
+
symbol: string,
|
|
150
|
+
descriptor:
|
|
151
|
+
| FileDescriptorProto
|
|
152
|
+
| DescriptorProto
|
|
153
|
+
| EnumDescriptorProto
|
|
154
|
+
| ServiceDescriptorProto
|
|
155
|
+
| MethodDescriptorProto,
|
|
156
|
+
prefix = '',
|
|
157
|
+
):
|
|
158
|
+
| DescriptorProto
|
|
159
|
+
| EnumDescriptorProto
|
|
160
|
+
| ServiceDescriptorProto
|
|
161
|
+
| MethodDescriptorProto
|
|
162
|
+
| undefined {
|
|
163
|
+
if (descriptor instanceof FileDescriptorProto) {
|
|
164
|
+
const packageName = descriptor.package;
|
|
165
|
+
|
|
166
|
+
const packagePrefix = packageName ? `${packageName}.` : '';
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
descriptor.messageType.find((type) =>
|
|
170
|
+
this.findSymbol(symbol, type, packagePrefix),
|
|
171
|
+
) ??
|
|
172
|
+
descriptor.enumType.find((type) =>
|
|
173
|
+
this.findSymbol(symbol, type, packagePrefix),
|
|
174
|
+
) ??
|
|
175
|
+
descriptor.service.find((type) =>
|
|
176
|
+
this.findSymbol(symbol, type, packagePrefix),
|
|
177
|
+
)
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const fullName = prefix + descriptor.name;
|
|
182
|
+
|
|
183
|
+
if (symbol === fullName) {
|
|
184
|
+
return descriptor;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (descriptor instanceof DescriptorProto) {
|
|
188
|
+
const messagePrefix = `${fullName}.`;
|
|
189
|
+
|
|
190
|
+
return (
|
|
191
|
+
descriptor.nestedType.find((type) =>
|
|
192
|
+
this.findSymbol(symbol, type, messagePrefix),
|
|
193
|
+
) ??
|
|
194
|
+
descriptor.enumType.find((type) =>
|
|
195
|
+
this.findSymbol(symbol, type, messagePrefix),
|
|
196
|
+
)
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (descriptor instanceof ServiceDescriptorProto) {
|
|
201
|
+
const servicePrefix = `${fullName}.`;
|
|
202
|
+
|
|
203
|
+
return descriptor.method.find((method) =>
|
|
204
|
+
this.findSymbol(symbol, method, servicePrefix),
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
}
|