@themainstack/communication 1.0.1 → 1.1.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.
package/README.md CHANGED
@@ -182,6 +182,7 @@ See `examples/full-grpc-demo.ts` in the repository for a complete working exampl
182
182
  ### Proto Generation
183
183
  - `generateProtoFromMethods(methods, options)` - Generate proto with service definition
184
184
  - `generateProtoFromFunction(fn, name)` - Generate proto for a single message
185
+ - `AnyType` - Marker symbol for `google.protobuf.Any` dynamic fields
185
186
 
186
187
  ### Server
187
188
  - `GrpcServerFactory.createServer(options, handlers)` - Create a gRPC server
package/dist/index.d.ts CHANGED
@@ -9,7 +9,7 @@
9
9
  * 3. Client Factory - Call gRPC services from other services
10
10
  * 4. Error Handling - Standardized error translation
11
11
  */
12
- export { generateProtoFromMethods, generateProtoFromFunction } from './proto-generator';
12
+ export { generateProtoFromMethods, generateProtoFromFunction, AnyType } from './proto-generator';
13
13
  export type { MethodDefinition, GenerateProtoOptions } from './proto-generator';
14
14
  export { GrpcServerFactory, exposeAsGrpc } from './grpc/server-factory';
15
15
  export type { GrpcServerOptions, ServerMethodHandler } from './grpc/server-factory';
package/dist/index.js CHANGED
@@ -11,12 +11,13 @@
11
11
  * 4. Error Handling - Standardized error translation
12
12
  */
13
13
  Object.defineProperty(exports, "__esModule", { value: true });
14
- exports.GrpcError = exports.handleGrpcError = exports.GrpcClientFactory = exports.exposeAsGrpc = exports.GrpcServerFactory = exports.generateProtoFromFunction = exports.generateProtoFromMethods = void 0;
14
+ exports.GrpcError = exports.handleGrpcError = exports.GrpcClientFactory = exports.exposeAsGrpc = exports.GrpcServerFactory = exports.AnyType = exports.generateProtoFromFunction = exports.generateProtoFromMethods = void 0;
15
15
  // Proto Generation
16
16
  // Auto-generate .proto files from TypeScript types
17
17
  var proto_generator_1 = require("./proto-generator");
18
18
  Object.defineProperty(exports, "generateProtoFromMethods", { enumerable: true, get: function () { return proto_generator_1.generateProtoFromMethods; } });
19
19
  Object.defineProperty(exports, "generateProtoFromFunction", { enumerable: true, get: function () { return proto_generator_1.generateProtoFromFunction; } });
20
+ Object.defineProperty(exports, "AnyType", { enumerable: true, get: function () { return proto_generator_1.AnyType; } });
20
21
  // Server Factory
21
22
  // Expose existing functions as gRPC endpoints
22
23
  var server_factory_1 = require("./grpc/server-factory");
@@ -1,3 +1,11 @@
1
+ /**
2
+ * Marker symbol to indicate a field should use google.protobuf.Any type.
3
+ * Use this in your sample objects to mark dynamic/generic fields.
4
+ *
5
+ * @example
6
+ * requestSample: () => ({ id: '', data: AnyType })
7
+ */
8
+ export declare const AnyType: unique symbol;
1
9
  /**
2
10
  * Options for generating a proto file
3
11
  */
@@ -10,6 +18,8 @@ export interface GenerateProtoOptions {
10
18
  outputDir?: string;
11
19
  /** Output filename (default: derived from serviceName or 'generated.proto') */
12
20
  outputFilename?: string;
21
+ /** Enable google.protobuf.Any for dynamic types marked with AnyType symbol (default: true) */
22
+ enableAnyType?: boolean;
13
23
  }
14
24
  /**
15
25
  * Definition of a gRPC method with request and response generators
@@ -33,10 +33,19 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.AnyType = void 0;
36
37
  exports.generateProtoFromMethods = generateProtoFromMethods;
37
38
  exports.generateProtoFromFunction = generateProtoFromFunction;
38
39
  const fs = __importStar(require("fs"));
39
40
  const path = __importStar(require("path"));
41
+ /**
42
+ * Marker symbol to indicate a field should use google.protobuf.Any type.
43
+ * Use this in your sample objects to mark dynamic/generic fields.
44
+ *
45
+ * @example
46
+ * requestSample: () => ({ id: '', data: AnyType })
47
+ */
48
+ exports.AnyType = Symbol('google.protobuf.Any');
40
49
  /**
41
50
  * Generate a complete .proto file from method definitions
42
51
  *
@@ -45,10 +54,11 @@ const path = __importStar(require("path"));
45
54
  * @returns The generated proto string
46
55
  */
47
56
  function generateProtoFromMethods(methods, options = {}) {
48
- const { packageName, serviceName = 'GeneratedService', outputDir, outputFilename } = options;
57
+ const { packageName, serviceName = 'GeneratedService', outputDir, outputFilename, enableAnyType = true } = options;
49
58
  const messages = [];
50
59
  const protoMethods = [];
51
60
  const processedObjects = new Set();
61
+ let usesAnyType = false;
52
62
  // Helper: Capitalize string
53
63
  function capitalize(s) {
54
64
  return s.charAt(0).toUpperCase() + s.slice(1);
@@ -72,7 +82,12 @@ function generateProtoFromMethods(methods, options = {}) {
72
82
  let type = "string";
73
83
  let rule = undefined;
74
84
  const valueType = typeof value;
75
- if (value === null || value === undefined) {
85
+ // Check for AnyType marker symbol
86
+ if (enableAnyType && (value === exports.AnyType || (typeof value === 'symbol' && value.description === 'google.protobuf.Any'))) {
87
+ type = "google.protobuf.Any";
88
+ usesAnyType = true;
89
+ }
90
+ else if (value === null || value === undefined) {
76
91
  type = "string";
77
92
  }
78
93
  else if (valueType === "string") {
@@ -89,7 +104,8 @@ function generateProtoFromMethods(methods, options = {}) {
89
104
  if (value.length > 0) {
90
105
  const firstItem = value[0];
91
106
  if (typeof firstItem === "object") {
92
- const nestedName = capitalize(key).replace(/s$/, "");
107
+ // Scope nested naming: Parent_Child
108
+ const nestedName = `${msgName}_${capitalize(key).replace(/s$/, "")}`;
93
109
  type = analyzeMessage(firstItem, nestedName);
94
110
  }
95
111
  else {
@@ -103,7 +119,8 @@ function generateProtoFromMethods(methods, options = {}) {
103
119
  }
104
120
  }
105
121
  else if (valueType === "object") {
106
- const nestedName = capitalize(key);
122
+ // Scope nested naming: Parent_Child
123
+ const nestedName = `${msgName}_${capitalize(key)}`;
107
124
  type = analyzeMessage(value, nestedName);
108
125
  }
109
126
  // Use snake_case for proto fields
@@ -138,6 +155,10 @@ function generateProtoFromMethods(methods, options = {}) {
138
155
  if (packageName) {
139
156
  lines.push(`package ${packageName};`, '');
140
157
  }
158
+ // Add google.protobuf.Any import if needed
159
+ if (usesAnyType) {
160
+ lines.push('import "google/protobuf/any.proto";', '');
161
+ }
141
162
  // Service definition
142
163
  lines.push(`// ${serviceName} - Auto-generated gRPC service`);
143
164
  lines.push(`service ${serviceName} {`);
@@ -127,6 +127,69 @@ export async function startGrpcServer() {
127
127
  }
128
128
  ```
129
129
 
130
+ ### 1.3.1 Organizing Handlers in Separate Files (Recommended)
131
+
132
+ For services with many gRPC methods, you can keep your code clean by defining handlers in separate files and importing them:
133
+
134
+ **Step 1: Define handlers in separate files**
135
+
136
+ ```typescript
137
+ // fee-service/src/grpc/handlers/withdrawal-fee.handler.ts
138
+ import { FeesService } from '../../fee/fees.service';
139
+
140
+ const feesService = new FeesService();
141
+
142
+ export const withdrawalFeeHandler = {
143
+ name: 'CalculateWithdrawalFee',
144
+ handler: async (request: any) => {
145
+ const result = await feesService.calculateFeesForPlan(
146
+ 'starter',
147
+ request.currency,
148
+ request.amount,
149
+ 'withdrawal',
150
+ );
151
+ return { /* mapped response */ };
152
+ },
153
+ requestSample: () => ({ merchantId: '', amount: 0, currency: '' }),
154
+ responseSample: () => ({ dollarAmount: 0, fee: 0 }),
155
+ };
156
+ ```
157
+
158
+ ```typescript
159
+ // fee-service/src/grpc/handlers/deposit-fee.handler.ts
160
+ export const depositFeeHandler = {
161
+ name: 'CalculateDepositFee',
162
+ handler: async (request: any) => { /* logic */ },
163
+ requestSample: () => ({ /* ... */ }),
164
+ responseSample: () => ({ /* ... */ }),
165
+ };
166
+ ```
167
+
168
+ **Step 2: Import and use them in your bootstrap file**
169
+
170
+ ```typescript
171
+ // fee-service/src/grpc/grpc-bootstrap.ts
172
+ import { GrpcServerFactory } from '@themainstack/communication';
173
+ import { withdrawalFeeHandler } from './handlers/withdrawal-fee.handler';
174
+ import { depositFeeHandler } from './handlers/deposit-fee.handler';
175
+
176
+ export async function startGrpcServer() {
177
+ const server = await GrpcServerFactory.createServer({
178
+ packageName: 'fee.v1',
179
+ serviceName: 'FeeService',
180
+ port: parseInt(process.env.GRPC_PORT || '50053'),
181
+ }, [
182
+ withdrawalFeeHandler, // Clean imports!
183
+ depositFeeHandler,
184
+ ]);
185
+
186
+ await server.start();
187
+ return server;
188
+ }
189
+ ```
190
+
191
+ This keeps your bootstrap file as a simple **configuration list** while the actual business logic lives in dedicated handler files.
192
+
130
193
  ### 1.4 Integrate with main.ts
131
194
 
132
195
  ```typescript
@@ -47,8 +47,8 @@ async function startServer() {
47
47
 
48
48
  const server = await GrpcServerFactory.createServer(
49
49
  {
50
- packageName: 'calculator.v1',
51
- serviceName: 'CalculatorService',
50
+ packageName: 'fee.v1',
51
+ serviceName: 'FeeService',
52
52
  port: 50055,
53
53
  protoOutputDir: path.join(__dirname, 'grpc'),
54
54
  },
@@ -58,11 +58,11 @@ async function startServer() {
58
58
  handler: calculateFee, // Your existing function!
59
59
  requestSample: () => ({ merchantId: '', amount: 0, currency: '' }),
60
60
  responseSample: () => ({
61
- originalAmount: 0,
62
- fee: 0,
61
+ originalAmount: 100,
62
+ fee: 10,
63
63
  totalAmount: 0,
64
- currency: '',
65
- feePercentage: 0
64
+ currency: 'USD',
65
+ feePercentage: 10
66
66
  }),
67
67
  },
68
68
  ]
@@ -81,8 +81,8 @@ async function callServer() {
81
81
  await new Promise(resolve => setTimeout(resolve, 500));
82
82
 
83
83
  const client = GrpcClientFactory.createClient<any>({
84
- serviceName: 'CalculatorService',
85
- packageName: 'calculator.v1',
84
+ serviceName: 'FeeService',
85
+ packageName: 'fee.v1',
86
86
  protoPath: path.join(__dirname, 'grpc', 'calculator.proto'),
87
87
  url: 'localhost:50055',
88
88
  });
@@ -108,11 +108,6 @@ async function callServer() {
108
108
 
109
109
  // RUN THE DEMO
110
110
  async function main() {
111
- console.log('------------------------------------------------------------');
112
- console.log(' @themainstack/communication - Full gRPC Demo');
113
- console.log('------------------------------------------------------------');
114
- console.log('');
115
-
116
111
  // Start server
117
112
  const server = await startServer();
118
113
 
@@ -130,10 +125,6 @@ async function main() {
130
125
  // Stop server
131
126
  await server.stop();
132
127
  }
133
-
134
- console.log('------------------------------------------------------------');
135
- console.log(' Demo Complete!');
136
- console.log('------------------------------------------------------------');
137
128
  }
138
129
 
139
130
  main().catch(console.error);
@@ -4,57 +4,19 @@ package fee.v1;
4
4
 
5
5
  // FeeService - Auto-generated gRPC service
6
6
  service FeeService {
7
- rpc CalculateWithdrawalFee (CalculateWithdrawalFeeRequest) returns (CalculateWithdrawalFeeResponse) {}
7
+ rpc CalculateFee (CalculateFeeRequest) returns (CalculateFeeResponse) {}
8
8
  }
9
9
 
10
- message CalculateWithdrawalFeeResponse {
11
- double dollar_amount = 1;
12
- double dollar_transaction_fee = 2;
13
- int32 local_transaction_fee = 3;
14
- string local_currency = 4;
15
- string local_amount = 5;
16
- double exchange_rate = 6;
17
- repeated AppliedFee applied_fees = 7;
18
- repeated string applied_fee_ids = 8;
19
- Tax tax = 9;
10
+ message CalculateFeeResponse {
11
+ int32 original_amount = 1;
12
+ int32 fee = 2;
13
+ int32 total_amount = 3;
14
+ string currency = 4;
15
+ int32 fee_percentage = 5;
20
16
  }
21
17
 
22
- message Tax {
23
- string id = 1;
24
- double dollar_tax = 2;
25
- int32 local_tax = 3;
26
- int32 gateway_recorded_tax = 4;
27
- int32 surplus_on_gateway_recorded_tax = 5;
28
- repeated Breakdown breakdown = 6;
29
- }
30
-
31
- message Breakdown {
32
- string country = 1;
33
- int32 flat_amount = 2;
34
- string percentage_decimal = 3;
35
- string rate_type = 4;
36
- string state = 5;
37
- string tax_type = 6;
38
- string taxability_reason = 7;
39
- }
40
-
41
- message AppliedFee {
42
- string id = 1;
43
- string type = 2;
44
- string display_name = 3;
45
- double amount = 4;
46
- string amount_type = 5;
47
- int32 extra = 6;
48
- double value = 7;
49
- double value_usd = 8;
50
- bool is_mainstack = 9;
51
- bool is_processor = 10;
52
- bool is_deposit = 11;
53
- }
54
-
55
- message CalculateWithdrawalFeeRequest {
18
+ message CalculateFeeRequest {
56
19
  string merchant_id = 1;
57
- double amount = 2;
20
+ int32 amount = 2;
58
21
  string currency = 3;
59
- string payout_method = 4;
60
22
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@themainstack/communication",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "private": false,
5
5
  "description": "Unified gRPC framework for inter-service communication - auto-generates protos, creates servers, and provides type-safe clients",
6
6
  "main": "dist/index.js",
@@ -177,7 +177,7 @@ export class GrpcServerFactory {
177
177
  this.server.bindAsync(
178
178
  address,
179
179
  grpc.ServerCredentials.createInsecure(),
180
- (error, port) => {
180
+ (error: Error | null, port: number) => {
181
181
  if (error) {
182
182
  reject(error);
183
183
  return;
package/src/index.ts CHANGED
@@ -14,7 +14,8 @@
14
14
  // Auto-generate .proto files from TypeScript types
15
15
  export {
16
16
  generateProtoFromMethods,
17
- generateProtoFromFunction
17
+ generateProtoFromFunction,
18
+ AnyType
18
19
  } from './proto-generator';
19
20
  export type {
20
21
  MethodDefinition,
@@ -1,10 +1,19 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
3
 
4
+ /**
5
+ * Marker symbol to indicate a field should use google.protobuf.Any type.
6
+ * Use this in your sample objects to mark dynamic/generic fields.
7
+ *
8
+ * @example
9
+ * requestSample: () => ({ id: '', data: AnyType })
10
+ */
11
+ export const AnyType = Symbol('google.protobuf.Any');
12
+
4
13
  /**
5
14
  * Maps JavaScript/TypeScript runtime types to Protobuf types.
6
15
  */
7
- type ProtoType = "double" | "float" | "int32" | "int64" | "bool" | "string" | "bytes" | string;
16
+ type ProtoType = "double" | "float" | "int32" | "int64" | "bool" | "string" | "bytes" | "google.protobuf.Any" | string;
8
17
 
9
18
  interface ProtoField {
10
19
  name: string;
@@ -36,6 +45,8 @@ export interface GenerateProtoOptions {
36
45
  outputDir?: string;
37
46
  /** Output filename (default: derived from serviceName or 'generated.proto') */
38
47
  outputFilename?: string;
48
+ /** Enable google.protobuf.Any for dynamic types marked with AnyType symbol (default: true) */
49
+ enableAnyType?: boolean;
39
50
  }
40
51
 
41
52
  /**
@@ -61,11 +72,12 @@ export function generateProtoFromMethods(
61
72
  methods: MethodDefinition<any, any>[],
62
73
  options: GenerateProtoOptions = {}
63
74
  ): string {
64
- const { packageName, serviceName = 'GeneratedService', outputDir, outputFilename } = options;
75
+ const { packageName, serviceName = 'GeneratedService', outputDir, outputFilename, enableAnyType = true } = options;
65
76
 
66
77
  const messages: ProtoMessage[] = [];
67
78
  const protoMethods: ProtoMethod[] = [];
68
79
  const processedObjects = new Set<object>();
80
+ let usesAnyType = false;
69
81
 
70
82
  // Helper: Capitalize string
71
83
  function capitalize(s: string): string {
@@ -96,7 +108,11 @@ export function generateProtoFromMethods(
96
108
 
97
109
  const valueType = typeof value;
98
110
 
99
- if (value === null || value === undefined) {
111
+ // Check for AnyType marker symbol
112
+ if (enableAnyType && (value === AnyType || (typeof value === 'symbol' && value.description === 'google.protobuf.Any'))) {
113
+ type = "google.protobuf.Any";
114
+ usesAnyType = true;
115
+ } else if (value === null || value === undefined) {
100
116
  type = "string";
101
117
  } else if (valueType === "string") {
102
118
  type = "string";
@@ -109,7 +125,8 @@ export function generateProtoFromMethods(
109
125
  if (value.length > 0) {
110
126
  const firstItem = value[0];
111
127
  if (typeof firstItem === "object") {
112
- const nestedName = capitalize(key).replace(/s$/, "");
128
+ // Scope nested naming: Parent_Child
129
+ const nestedName = `${msgName}_${capitalize(key).replace(/s$/, "")}`;
113
130
  type = analyzeMessage(firstItem, nestedName);
114
131
  } else {
115
132
  type = typeof firstItem === "number"
@@ -120,7 +137,8 @@ export function generateProtoFromMethods(
120
137
  type = "string";
121
138
  }
122
139
  } else if (valueType === "object") {
123
- const nestedName = capitalize(key);
140
+ // Scope nested naming: Parent_Child
141
+ const nestedName = `${msgName}_${capitalize(key)}`;
124
142
  type = analyzeMessage(value, nestedName);
125
143
  }
126
144
 
@@ -165,6 +183,11 @@ export function generateProtoFromMethods(
165
183
  lines.push(`package ${packageName};`, '');
166
184
  }
167
185
 
186
+ // Add google.protobuf.Any import if needed
187
+ if (usesAnyType) {
188
+ lines.push('import "google/protobuf/any.proto";', '');
189
+ }
190
+
168
191
  // Service definition
169
192
  lines.push(`// ${serviceName} - Auto-generated gRPC service`);
170
193
  lines.push(`service ${serviceName} {`);
@@ -0,0 +1,100 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { generateProtoFromMethods, AnyType } from '../src/index';
3
+
4
+ describe('google.protobuf.Any support', () => {
5
+ it('should export AnyType symbol', () => {
6
+ expect(AnyType).toBeDefined();
7
+ expect(typeof AnyType).toBe('symbol');
8
+ expect(AnyType.description).toBe('google.protobuf.Any');
9
+ });
10
+
11
+ it('should generate proto with google.protobuf.Any for marked fields', () => {
12
+ const proto = generateProtoFromMethods([
13
+ {
14
+ name: 'ProcessGenericData',
15
+ requestSample: () => ({
16
+ id: '',
17
+ data: AnyType,
18
+ }),
19
+ responseSample: () => ({
20
+ success: true,
21
+ result: AnyType,
22
+ }),
23
+ }
24
+ ], {
25
+ packageName: 'generic.v1',
26
+ serviceName: 'GenericService',
27
+ });
28
+
29
+ // Should include the import statement
30
+ expect(proto).toContain('import "google/protobuf/any.proto";');
31
+
32
+ // Should use google.protobuf.Any type
33
+ expect(proto).toContain('google.protobuf.Any data = 2;');
34
+ expect(proto).toContain('google.protobuf.Any result = 2;');
35
+ });
36
+
37
+ it('should not include import if no Any types are used', () => {
38
+ const proto = generateProtoFromMethods([
39
+ {
40
+ name: 'SimpleMethod',
41
+ requestSample: () => ({ id: '', amount: 0 }),
42
+ responseSample: () => ({ success: true }),
43
+ }
44
+ ], {
45
+ packageName: 'simple.v1',
46
+ serviceName: 'SimpleService',
47
+ });
48
+
49
+ expect(proto).not.toContain('import "google/protobuf/any.proto";');
50
+ expect(proto).not.toContain('google.protobuf.Any');
51
+ });
52
+
53
+ it('should handle mixed fields with some Any types', () => {
54
+ const proto = generateProtoFromMethods([
55
+ {
56
+ name: 'MixedMethod',
57
+ requestSample: () => ({
58
+ id: '',
59
+ name: 'test',
60
+ metadata: AnyType,
61
+ count: 0,
62
+ }),
63
+ responseSample: () => ({
64
+ status: 'ok',
65
+ payload: AnyType,
66
+ }),
67
+ }
68
+ ], {
69
+ packageName: 'mixed.v1',
70
+ serviceName: 'MixedService',
71
+ });
72
+
73
+ expect(proto).toContain('import "google/protobuf/any.proto";');
74
+ expect(proto).toContain('string id = 1;');
75
+ expect(proto).toContain('string name = 2;');
76
+ expect(proto).toContain('google.protobuf.Any metadata = 3;');
77
+ expect(proto).toContain('int32 count = 4;');
78
+ });
79
+
80
+ it('should respect enableAnyType: false option', () => {
81
+ const proto = generateProtoFromMethods([
82
+ {
83
+ name: 'DisabledAny',
84
+ requestSample: () => ({
85
+ id: '',
86
+ data: AnyType,
87
+ }),
88
+ responseSample: () => ({ success: true }),
89
+ }
90
+ ], {
91
+ packageName: 'test.v1',
92
+ serviceName: 'TestService',
93
+ enableAnyType: false,
94
+ });
95
+
96
+ // When disabled, AnyType should be treated as a regular value (string fallback)
97
+ expect(proto).not.toContain('import "google/protobuf/any.proto";');
98
+ expect(proto).not.toContain('google.protobuf.Any');
99
+ });
100
+ });