@themainstack/communication 1.0.0

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.
@@ -0,0 +1,263 @@
1
+ import * as grpc from '@grpc/grpc-js';
2
+ import * as protoLoader from '@grpc/proto-loader';
3
+ import * as path from 'path';
4
+ import * as fs from 'fs';
5
+ import { generateProtoFromMethods, MethodDefinition } from '../proto-generator';
6
+
7
+ /**
8
+ * Options for creating a gRPC server
9
+ */
10
+ export interface GrpcServerOptions {
11
+ /** The package name (e.g., 'fee.v1') */
12
+ packageName: string;
13
+ /** The service name (e.g., 'FeeService') */
14
+ serviceName: string;
15
+ /** Port to listen on (default: 50051) */
16
+ port?: number;
17
+ /** Host to bind to (default: '0.0.0.0') */
18
+ host?: string;
19
+ /** Path to existing proto file (if not provided, will auto-generate) */
20
+ protoPath?: string;
21
+ /** Directory to save auto-generated proto (default: './grpc') */
22
+ protoOutputDir?: string;
23
+ }
24
+
25
+ /**
26
+ * Method handler definition for the server
27
+ */
28
+ export interface ServerMethodHandler<TReq = any, TRes = any> {
29
+ /** Method name (e.g., 'CalculateWithdrawalFee') */
30
+ name: string;
31
+ /** The actual handler function */
32
+ handler: (request: TReq) => Promise<TRes> | TRes;
33
+ /** Sample request for proto generation */
34
+ requestSample: () => TReq;
35
+ /** Sample response for proto generation */
36
+ responseSample: () => TRes;
37
+ }
38
+
39
+ /**
40
+ * GrpcServerFactory - Create and run gRPC servers easily
41
+ *
42
+ * Usage:
43
+ * ```typescript
44
+ * const server = await GrpcServerFactory.createServer({
45
+ * packageName: 'fee.v1',
46
+ * serviceName: 'FeeService',
47
+ * port: 50053,
48
+ * }, [
49
+ * {
50
+ * name: 'CalculateWithdrawalFee',
51
+ * handler: async (req) => calculateFee(req),
52
+ * requestSample: () => ({ merchantId: '', amount: 0 }),
53
+ * responseSample: () => ({ dollarAmount: 0, fees: [] }),
54
+ * }
55
+ * ]);
56
+ *
57
+ * await server.start();
58
+ * ```
59
+ */
60
+ export class GrpcServerFactory {
61
+ private server: grpc.Server;
62
+ private options: Required<GrpcServerOptions>;
63
+ private isRunning: boolean = false;
64
+
65
+ private constructor(
66
+ server: grpc.Server,
67
+ options: Required<GrpcServerOptions>
68
+ ) {
69
+ this.server = server;
70
+ this.options = options;
71
+ }
72
+
73
+ /**
74
+ * Create a new gRPC server with the given handlers
75
+ */
76
+ static async createServer(
77
+ options: GrpcServerOptions,
78
+ handlers: ServerMethodHandler[]
79
+ ): Promise<GrpcServerFactory> {
80
+ const fullOptions: Required<GrpcServerOptions> = {
81
+ port: 50051,
82
+ host: '0.0.0.0',
83
+ protoOutputDir: './grpc',
84
+ protoPath: '',
85
+ ...options,
86
+ };
87
+
88
+ // Step 1: Generate or load proto file
89
+ let protoPath = fullOptions.protoPath;
90
+
91
+ if (!protoPath) {
92
+ // Auto-generate proto from handlers
93
+ const methodDefinitions: MethodDefinition<any, any>[] = handlers.map(h => ({
94
+ name: h.name,
95
+ requestSample: h.requestSample,
96
+ responseSample: h.responseSample,
97
+ }));
98
+
99
+ const protoContent = generateProtoFromMethods(methodDefinitions, {
100
+ packageName: fullOptions.packageName,
101
+ serviceName: fullOptions.serviceName,
102
+ outputDir: fullOptions.protoOutputDir,
103
+ });
104
+
105
+ protoPath = path.join(
106
+ fullOptions.protoOutputDir,
107
+ `${fullOptions.serviceName.toLowerCase().replace(/service$/, '')}.proto`
108
+ );
109
+
110
+ console.log(`Proto file auto-generated: ${protoPath}`);
111
+ }
112
+
113
+ // Step 2: Load the proto definition
114
+ const packageDefinition = protoLoader.loadSync(protoPath, {
115
+ keepCase: false, // Convert snake_case to camelCase
116
+ longs: String,
117
+ enums: String,
118
+ defaults: true,
119
+ oneofs: true,
120
+ });
121
+
122
+ const protoDescriptor = grpc.loadPackageDefinition(packageDefinition);
123
+
124
+ // Step 3: Navigate to the service definition
125
+ let serviceDefinition: any = protoDescriptor;
126
+ const packageParts = fullOptions.packageName.split('.');
127
+ for (const part of packageParts) {
128
+ serviceDefinition = serviceDefinition[part];
129
+ if (!serviceDefinition) {
130
+ throw new Error(`Package '${fullOptions.packageName}' not found in proto`);
131
+ }
132
+ }
133
+
134
+ const ServiceConstructor = serviceDefinition[fullOptions.serviceName];
135
+ if (!ServiceConstructor || !ServiceConstructor.service) {
136
+ throw new Error(`Service '${fullOptions.serviceName}' not found in package '${fullOptions.packageName}'`);
137
+ }
138
+
139
+ // Step 4: Create handler implementations
140
+ const implementations: Record<string, grpc.handleUnaryCall<any, any>> = {};
141
+
142
+ for (const handler of handlers) {
143
+ // gRPC uses lowercase first letter for method names in implementation
144
+ const methodName = handler.name.charAt(0).toLowerCase() + handler.name.slice(1);
145
+
146
+ implementations[methodName] = async (
147
+ call: grpc.ServerUnaryCall<any, any>,
148
+ callback: grpc.sendUnaryData<any>
149
+ ) => {
150
+ try {
151
+ const result = await handler.handler(call.request);
152
+ callback(null, result);
153
+ } catch (error: any) {
154
+ console.error(`gRPC Error in ${handler.name}:`, error);
155
+ callback({
156
+ code: grpc.status.INTERNAL,
157
+ message: error.message || 'Internal server error',
158
+ });
159
+ }
160
+ };
161
+ }
162
+
163
+ // Step 5: Create and configure the server
164
+ const server = new grpc.Server();
165
+ server.addService(ServiceConstructor.service, implementations);
166
+
167
+ return new GrpcServerFactory(server, { ...fullOptions, protoPath });
168
+ }
169
+
170
+ /**
171
+ * Start the gRPC server
172
+ */
173
+ async start(): Promise<void> {
174
+ return new Promise((resolve, reject) => {
175
+ const address = `${this.options.host}:${this.options.port}`;
176
+
177
+ this.server.bindAsync(
178
+ address,
179
+ grpc.ServerCredentials.createInsecure(),
180
+ (error, port) => {
181
+ if (error) {
182
+ reject(error);
183
+ return;
184
+ }
185
+
186
+ this.isRunning = true;
187
+ console.log(` gRPC Server running on ${address}`);
188
+ console.log(` Service: ${this.options.serviceName}`);
189
+ console.log(` Package: ${this.options.packageName}`);
190
+ resolve();
191
+ }
192
+ );
193
+ });
194
+ }
195
+
196
+ /**
197
+ * Stop the gRPC server
198
+ */
199
+ async stop(): Promise<void> {
200
+ return new Promise((resolve) => {
201
+ this.server.tryShutdown(() => {
202
+ this.isRunning = false;
203
+ console.log('gRPC Server stopped');
204
+ resolve();
205
+ });
206
+ });
207
+ }
208
+
209
+ /**
210
+ * Force stop the server immediately
211
+ */
212
+ forceStop(): void {
213
+ this.server.forceShutdown();
214
+ this.isRunning = false;
215
+ console.log('gRPC Server force stopped');
216
+ }
217
+
218
+ /**
219
+ * Check if server is running
220
+ */
221
+ get running(): boolean {
222
+ return this.isRunning;
223
+ }
224
+
225
+ /**
226
+ * Get the server address
227
+ */
228
+ get address(): string {
229
+ return `${this.options.host}:${this.options.port}`;
230
+ }
231
+ }
232
+
233
+ /**
234
+ * Helper function to quickly expose a function as a gRPC service
235
+ *
236
+ * Usage:
237
+ * ```typescript
238
+ * const server = await exposeAsGrpc(
239
+ * 'CalculateFee',
240
+ * async (req) => ({ fee: req.amount * 0.02 }),
241
+ * { requestSample: () => ({ amount: 100 }), responseSample: () => ({ fee: 2 }) },
242
+ * { packageName: 'fee.v1', serviceName: 'FeeService', port: 50053 }
243
+ * );
244
+ * ```
245
+ */
246
+ export async function exposeAsGrpc<TReq extends object, TRes extends object>(
247
+ methodName: string,
248
+ handler: (request: TReq) => Promise<TRes> | TRes,
249
+ samples: { requestSample: () => TReq; responseSample: () => TRes },
250
+ options: GrpcServerOptions
251
+ ): Promise<GrpcServerFactory> {
252
+ const server = await GrpcServerFactory.createServer(options, [
253
+ {
254
+ name: methodName,
255
+ handler,
256
+ requestSample: samples.requestSample,
257
+ responseSample: samples.responseSample,
258
+ },
259
+ ]);
260
+
261
+ await server.start();
262
+ return server;
263
+ }
@@ -0,0 +1,11 @@
1
+ import { ChannelOptions, ChannelCredentials } from '@grpc/grpc-js';
2
+
3
+ export interface GrpcClientOptions {
4
+ serviceName: string;
5
+ packageName?: string;
6
+ protoPath: string;
7
+ url: string;
8
+ credentials?: ChannelCredentials;
9
+ channelOptions?: ChannelOptions;
10
+ loaderOptions?: object;
11
+ }
package/src/index.ts ADDED
@@ -0,0 +1,42 @@
1
+ /**
2
+ * @themainstack/communication
3
+ *
4
+ * A unified gRPC framework for inter-service communication.
5
+ *
6
+ * Workflow:
7
+ * 1. Proto Generation - Auto-generate .proto files from TypeScript
8
+ * 2. Server Factory - Expose functions as gRPC endpoints
9
+ * 3. Client Factory - Call gRPC services from other services
10
+ * 4. Error Handling - Standardized error translation
11
+ */
12
+
13
+ // Proto Generation
14
+ // Auto-generate .proto files from TypeScript types
15
+ export {
16
+ generateProtoFromMethods,
17
+ generateProtoFromFunction
18
+ } from './proto-generator';
19
+ export type {
20
+ MethodDefinition,
21
+ GenerateProtoOptions
22
+ } from './proto-generator';
23
+
24
+ // Server Factory
25
+ // Expose existing functions as gRPC endpoints
26
+ export {
27
+ GrpcServerFactory,
28
+ exposeAsGrpc
29
+ } from './grpc/server-factory';
30
+ export type {
31
+ GrpcServerOptions,
32
+ ServerMethodHandler
33
+ } from './grpc/server-factory';
34
+
35
+ // Client Factory
36
+ // Create clients to call gRPC services
37
+ export { GrpcClientFactory } from './grpc/client-factory';
38
+ export type { GrpcClientOptions } from './grpc/types';
39
+
40
+ // Error Handling
41
+ // Standardized gRPC error translation
42
+ export { handleGrpcError, GrpcError } from './grpc/errors';
@@ -0,0 +1,293 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+
4
+ /**
5
+ * Maps JavaScript/TypeScript runtime types to Protobuf types.
6
+ */
7
+ type ProtoType = "double" | "float" | "int32" | "int64" | "bool" | "string" | "bytes" | string;
8
+
9
+ interface ProtoField {
10
+ name: string;
11
+ type: ProtoType;
12
+ rule?: "repeated";
13
+ id: number;
14
+ }
15
+
16
+ interface ProtoMessage {
17
+ name: string;
18
+ fields: ProtoField[];
19
+ }
20
+
21
+ interface ProtoMethod {
22
+ name: string;
23
+ requestType: string;
24
+ responseType: string;
25
+ }
26
+
27
+ /**
28
+ * Options for generating a proto file
29
+ */
30
+ export interface GenerateProtoOptions {
31
+ /** The package name (e.g., 'fee.v1') */
32
+ packageName?: string;
33
+ /** The service name (e.g., 'FeeService') */
34
+ serviceName?: string;
35
+ /** Directory to save the proto file (if not provided, won't save to file) */
36
+ outputDir?: string;
37
+ /** Output filename (default: derived from serviceName or 'generated.proto') */
38
+ outputFilename?: string;
39
+ }
40
+
41
+ /**
42
+ * Definition of a gRPC method with request and response generators
43
+ */
44
+ export interface MethodDefinition<TReq extends object, TRes extends object> {
45
+ /** Method name (e.g., 'CalculateWithdrawalFee') */
46
+ name: string;
47
+ /** Function that returns a sample request object */
48
+ requestSample: () => TReq;
49
+ /** Function that returns a sample response object */
50
+ responseSample: () => TRes;
51
+ }
52
+
53
+ /**
54
+ * Generate a complete .proto file from method definitions
55
+ *
56
+ * @param methods Array of method definitions
57
+ * @param options Generation options
58
+ * @returns The generated proto string
59
+ */
60
+ export function generateProtoFromMethods(
61
+ methods: MethodDefinition<any, any>[],
62
+ options: GenerateProtoOptions = {}
63
+ ): string {
64
+ const { packageName, serviceName = 'GeneratedService', outputDir, outputFilename } = options;
65
+
66
+ const messages: ProtoMessage[] = [];
67
+ const protoMethods: ProtoMethod[] = [];
68
+ const processedObjects = new Set<object>();
69
+
70
+ // Helper: Capitalize string
71
+ function capitalize(s: string): string {
72
+ return s.charAt(0).toUpperCase() + s.slice(1);
73
+ }
74
+
75
+ // Helper: Convert camelCase to snake_case
76
+ function toSnakeCase(s: string): string {
77
+ return s.replace(/([A-Z])/g, '_$1').toLowerCase().replace(/^_/, '');
78
+ }
79
+
80
+ // Recursive function to analyze object and generate messages
81
+ function analyzeMessage(obj: any, msgName: string): string {
82
+ if (obj instanceof Date) return "string";
83
+
84
+ if (processedObjects.has(obj)) {
85
+ console.warn(`Circular reference detected for ${msgName}. Breaking recursion.`);
86
+ return "string";
87
+ }
88
+ processedObjects.add(obj);
89
+
90
+ const fields: ProtoField[] = [];
91
+ let fieldIdCounter = 1;
92
+
93
+ for (const [key, value] of Object.entries(obj)) {
94
+ let type: ProtoType = "string";
95
+ let rule: "repeated" | undefined = undefined;
96
+
97
+ const valueType = typeof value;
98
+
99
+ if (value === null || value === undefined) {
100
+ type = "string";
101
+ } else if (valueType === "string") {
102
+ type = "string";
103
+ } else if (valueType === "boolean") {
104
+ type = "bool";
105
+ } else if (valueType === "number") {
106
+ type = Number.isInteger(value) ? "int32" : "double";
107
+ } else if (Array.isArray(value)) {
108
+ rule = "repeated";
109
+ if (value.length > 0) {
110
+ const firstItem = value[0];
111
+ if (typeof firstItem === "object") {
112
+ const nestedName = capitalize(key).replace(/s$/, "");
113
+ type = analyzeMessage(firstItem, nestedName);
114
+ } else {
115
+ type = typeof firstItem === "number"
116
+ ? (Number.isInteger(firstItem) ? "int32" : "double")
117
+ : typeof firstItem === "boolean" ? "bool" : "string";
118
+ }
119
+ } else {
120
+ type = "string";
121
+ }
122
+ } else if (valueType === "object") {
123
+ const nestedName = capitalize(key);
124
+ type = analyzeMessage(value, nestedName);
125
+ }
126
+
127
+ // Use snake_case for proto fields
128
+ fields.push({ name: toSnakeCase(key), type, rule, id: fieldIdCounter++ });
129
+ }
130
+
131
+ // Check if message already exists (same name)
132
+ const existingIndex = messages.findIndex(m => m.name === msgName);
133
+ if (existingIndex === -1) {
134
+ messages.push({ name: msgName, fields });
135
+ }
136
+
137
+ return msgName;
138
+ }
139
+
140
+ // Process each method
141
+ for (const method of methods) {
142
+ const requestTypeName = `${method.name}Request`;
143
+ const responseTypeName = `${method.name}Response`;
144
+
145
+ // Analyze request and response
146
+ const requestSample = method.requestSample();
147
+ const responseSample = method.responseSample();
148
+
149
+ analyzeMessage(requestSample, requestTypeName);
150
+ processedObjects.clear(); // Reset for next analysis
151
+ analyzeMessage(responseSample, responseTypeName);
152
+ processedObjects.clear();
153
+
154
+ protoMethods.push({
155
+ name: method.name,
156
+ requestType: requestTypeName,
157
+ responseType: responseTypeName,
158
+ });
159
+ }
160
+
161
+ // Format the proto file
162
+ const lines: string[] = ['syntax = "proto3";', ''];
163
+
164
+ if (packageName) {
165
+ lines.push(`package ${packageName};`, '');
166
+ }
167
+
168
+ // Service definition
169
+ lines.push(`// ${serviceName} - Auto-generated gRPC service`);
170
+ lines.push(`service ${serviceName} {`);
171
+ for (const method of protoMethods) {
172
+ lines.push(` rpc ${method.name} (${method.requestType}) returns (${method.responseType}) {}`);
173
+ }
174
+ lines.push('}', '');
175
+
176
+ // Message definitions (reverse for proper dependency order)
177
+ for (const msg of messages.reverse()) {
178
+ lines.push(`message ${msg.name} {`);
179
+ for (const field of msg.fields) {
180
+ const rulePrefix = field.rule ? `${field.rule} ` : "";
181
+ lines.push(` ${rulePrefix}${field.type} ${field.name} = ${field.id};`);
182
+ }
183
+ lines.push('}', '');
184
+ }
185
+
186
+ const protoContent = lines.join('\n');
187
+
188
+ // Save to file if outputDir is specified
189
+ if (outputDir) {
190
+ const filename = outputFilename || `${toSnakeCase(serviceName).replace(/_service$/, '')}.proto`;
191
+ const fullPath = path.join(outputDir, filename);
192
+
193
+ // Create directory if it doesn't exist
194
+ if (!fs.existsSync(outputDir)) {
195
+ fs.mkdirSync(outputDir, { recursive: true });
196
+ }
197
+
198
+ fs.writeFileSync(fullPath, protoContent);
199
+ console.log(`✅ Proto file generated: ${fullPath}`);
200
+ }
201
+
202
+ return protoContent;
203
+ }
204
+
205
+ /**
206
+ * Convenience function for single-function proto generation (original API)
207
+ */
208
+ export function generateProtoFromFunction<T extends object>(
209
+ generatorFn: () => T,
210
+ rootMessageName: string = "RootMessage"
211
+ ): string {
212
+ const messages: ProtoMessage[] = [];
213
+ const processedObjects = new Set<object>();
214
+
215
+ function capitalize(s: string): string {
216
+ return s.charAt(0).toUpperCase() + s.slice(1);
217
+ }
218
+
219
+ function toSnakeCase(s: string): string {
220
+ return s.replace(/([A-Z])/g, '_$1').toLowerCase().replace(/^_/, '');
221
+ }
222
+
223
+ function analyzeMessage(obj: any, msgName: string): string {
224
+ if (obj instanceof Date) return "string";
225
+
226
+ if (processedObjects.has(obj)) {
227
+ return "string";
228
+ }
229
+ processedObjects.add(obj);
230
+
231
+ const fields: ProtoField[] = [];
232
+ let fieldIdCounter = 1;
233
+
234
+ for (const [key, value] of Object.entries(obj)) {
235
+ let type: ProtoType = "string";
236
+ let rule: "repeated" | undefined = undefined;
237
+
238
+ const valueType = typeof value;
239
+
240
+ if (value === null || value === undefined) {
241
+ type = "string";
242
+ } else if (valueType === "string") {
243
+ type = "string";
244
+ } else if (valueType === "boolean") {
245
+ type = "bool";
246
+ } else if (valueType === "number") {
247
+ type = Number.isInteger(value) ? "int32" : "double";
248
+ } else if (Array.isArray(value)) {
249
+ rule = "repeated";
250
+ if (value.length > 0) {
251
+ const firstItem = value[0];
252
+ if (typeof firstItem === "object") {
253
+ const nestedName = capitalize(key).replace(/s$/, "");
254
+ type = analyzeMessage(firstItem, nestedName);
255
+ } else {
256
+ type = typeof firstItem === "number"
257
+ ? (Number.isInteger(firstItem) ? "int32" : "double")
258
+ : typeof firstItem === "boolean" ? "bool" : "string";
259
+ }
260
+ } else {
261
+ type = "string";
262
+ }
263
+ } else if (valueType === "object") {
264
+ const nestedName = capitalize(key);
265
+ type = analyzeMessage(value, nestedName);
266
+ }
267
+
268
+ fields.push({ name: toSnakeCase(key), type, rule, id: fieldIdCounter++ });
269
+ }
270
+
271
+ messages.push({ name: msgName, fields });
272
+ return msgName;
273
+ }
274
+
275
+ const sampleData = generatorFn();
276
+ if (!sampleData || typeof sampleData !== "object") {
277
+ throw new Error("Generator function must return a non-null object.");
278
+ }
279
+
280
+ analyzeMessage(sampleData, rootMessageName);
281
+
282
+ const lines: string[] = ['syntax = "proto3";', ''];
283
+ for (const msg of messages.reverse()) {
284
+ lines.push(`message ${msg.name} {`);
285
+ for (const field of msg.fields) {
286
+ const rulePrefix = field.rule ? `${field.rule} ` : "";
287
+ lines.push(` ${rulePrefix}${field.type} ${field.name} = ${field.id};`);
288
+ }
289
+ lines.push('}', '');
290
+ }
291
+
292
+ return lines.join('\n');
293
+ }
@@ -0,0 +1,15 @@
1
+ syntax = "proto3";
2
+
3
+ package test.v1;
4
+
5
+ service GreeterService {
6
+ rpc SayHello (HelloRequest) returns (HelloReply) {}
7
+ }
8
+
9
+ message HelloRequest {
10
+ string name = 1;
11
+ }
12
+
13
+ message HelloReply {
14
+ string message = 1;
15
+ }
@@ -0,0 +1,15 @@
1
+ syntax = "proto3";
2
+
3
+ // No package definition here
4
+
5
+ service RootService {
6
+ rpc SayHello (RootRequest) returns (RootReply) {}
7
+ }
8
+
9
+ message RootRequest {
10
+ string name = 1;
11
+ }
12
+
13
+ message RootReply {
14
+ string message = 1;
15
+ }
@@ -0,0 +1,44 @@
1
+ import { join } from 'path';
2
+ import { describe, it, expect } from 'vitest';
3
+ import * as grpc from '@grpc/grpc-js';
4
+ import { GrpcClientFactory } from '../../src/grpc/client-factory';
5
+
6
+ describe('GrpcClientFactory', () => {
7
+ const protoPath = join(__dirname, '../fixtures/dummy.proto');
8
+ const noPackageProtoPath = join(__dirname, '../fixtures/no-package.proto');
9
+
10
+ it('should create a client successfully (Strict Package)', () => {
11
+ const client = GrpcClientFactory.createClient<grpc.Client>({
12
+ packageName: 'test.v1',
13
+ serviceName: 'GreeterService',
14
+ protoPath,
15
+ url: 'localhost:50051',
16
+ });
17
+ expect(client).toBeDefined();
18
+ client.close();
19
+ });
20
+
21
+ it('should create a client successfully for ROOT service (Implicit Default)', () => {
22
+ const client = GrpcClientFactory.createClient<grpc.Client>({
23
+ // packageName omitted -> defaults to '' (Root)
24
+ serviceName: 'RootService',
25
+ protoPath: noPackageProtoPath,
26
+ url: 'localhost:50051',
27
+ });
28
+ expect(client).toBeDefined();
29
+ client.close();
30
+ });
31
+
32
+ it('should throw error if implicit default fails (expecting root, but service is missing)', () => {
33
+ // Relying on default '' means looking at root.
34
+ // But GreeterService is inside test.v1, so it should NOT be found at root.
35
+ expect(() => {
36
+ GrpcClientFactory.createClient<grpc.Client>({
37
+ // packageName omitted -> defaults to root
38
+ serviceName: 'GreeterService',
39
+ protoPath, // This proto puts it in 'test.v1'
40
+ url: 'localhost:50051',
41
+ });
42
+ }).toThrow(/not found in root definition/);
43
+ });
44
+ });