@themainstack/communication 1.0.0 → 1.0.2
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 +194 -0
- package/dist/proto-generator/index.js +4 -2
- package/docs/INTEGRATION_EXAMPLE.md +63 -0
- package/examples/full-grpc-demo.ts +8 -17
- package/examples/grpc/fee.proto +9 -47
- package/package.json +2 -2
- package/publish-config.npmrc +1 -0
- package/src/grpc/server-factory.ts +1 -1
- package/src/proto-generator/index.ts +4 -2
package/README.md
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# @themainstack/communication
|
|
2
|
+
|
|
3
|
+
A unified gRPC framework for inter-service communication at Mainstack.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
1. [Overview](#overview)
|
|
7
|
+
2. [Installation](#installation)
|
|
8
|
+
3. [Proto Generation](#proto-generation)
|
|
9
|
+
4. [Creating a gRPC Server](#creating-a-grpc-server)
|
|
10
|
+
5. [Creating a gRPC Client](#creating-a-grpc-client)
|
|
11
|
+
6. [Error Handling](#error-handling)
|
|
12
|
+
7. [Full Example](#full-example)
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Overview
|
|
17
|
+
|
|
18
|
+
The workflow is simple:
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
Developer writes TypeScript function
|
|
22
|
+
↓
|
|
23
|
+
Package auto-generates .proto file
|
|
24
|
+
↓
|
|
25
|
+
Server Factory exposes function as gRPC
|
|
26
|
+
↓
|
|
27
|
+
Client Factory in another service calls it
|
|
28
|
+
↓
|
|
29
|
+
Error Handler normalizes any errors
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm install @themainstack/communication
|
|
38
|
+
# or
|
|
39
|
+
yarn add @themainstack/communication
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Proto Generation
|
|
45
|
+
|
|
46
|
+
Auto-generate `.proto` files from your TypeScript types.
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
import { generateProtoFromMethods } from '@themainstack/communication';
|
|
50
|
+
|
|
51
|
+
generateProtoFromMethods([
|
|
52
|
+
{
|
|
53
|
+
name: 'CalculateFee',
|
|
54
|
+
requestSample: () => ({ merchantId: '', amount: 0, currency: '' }),
|
|
55
|
+
responseSample: () => ({ fee: 0, total: 0 }),
|
|
56
|
+
}
|
|
57
|
+
], {
|
|
58
|
+
packageName: 'fee.v1',
|
|
59
|
+
serviceName: 'FeeService',
|
|
60
|
+
outputDir: './src/grpc',
|
|
61
|
+
});
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**Output:** `./src/grpc/feeservice.proto` is auto-generated!
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Creating a gRPC Server
|
|
69
|
+
|
|
70
|
+
Expose existing functions as gRPC endpoints.
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
import { GrpcServerFactory } from '@themainstack/communication';
|
|
74
|
+
|
|
75
|
+
// Your existing business function
|
|
76
|
+
async function calculateFee(request) {
|
|
77
|
+
return { fee: request.amount * 0.02, total: request.amount * 1.02 };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Create and start the server
|
|
81
|
+
const server = await GrpcServerFactory.createServer({
|
|
82
|
+
packageName: 'fee.v1',
|
|
83
|
+
serviceName: 'FeeService',
|
|
84
|
+
port: 50053,
|
|
85
|
+
}, [
|
|
86
|
+
{
|
|
87
|
+
name: 'CalculateFee',
|
|
88
|
+
handler: calculateFee, // Your existing function!
|
|
89
|
+
requestSample: () => ({ merchantId: '', amount: 0, currency: '' }),
|
|
90
|
+
responseSample: () => ({ fee: 0, total: 0 }),
|
|
91
|
+
}
|
|
92
|
+
]);
|
|
93
|
+
|
|
94
|
+
await server.start();
|
|
95
|
+
// 🚀 gRPC Server running on 0.0.0.0:50053
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Quick Expose (One-liner)
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
import { exposeAsGrpc } from '@themainstack/communication';
|
|
102
|
+
|
|
103
|
+
const server = await exposeAsGrpc(
|
|
104
|
+
'CalculateFee',
|
|
105
|
+
calculateFee,
|
|
106
|
+
{ requestSample: () => ({...}), responseSample: () => ({...}) },
|
|
107
|
+
{ packageName: 'fee.v1', serviceName: 'FeeService', port: 50053 }
|
|
108
|
+
);
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Creating a gRPC Client
|
|
114
|
+
|
|
115
|
+
Call gRPC services from other services.
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
import { GrpcClientFactory } from '@themainstack/communication';
|
|
119
|
+
|
|
120
|
+
const client = GrpcClientFactory.createClient({
|
|
121
|
+
serviceName: 'FeeService',
|
|
122
|
+
packageName: 'fee.v1',
|
|
123
|
+
protoPath: './src/grpc/fee.proto',
|
|
124
|
+
url: process.env.FEE_SERVICE_URL || 'localhost:50053',
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Make a call
|
|
128
|
+
client.CalculateFee(
|
|
129
|
+
{ merchantId: 'merchant_123', amount: 1000, currency: 'USD' },
|
|
130
|
+
(err, response) => {
|
|
131
|
+
if (err) {
|
|
132
|
+
handleGrpcError(err);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
console.log('Fee:', response.fee);
|
|
136
|
+
}
|
|
137
|
+
);
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Error Handling
|
|
143
|
+
|
|
144
|
+
Standardized gRPC error translation.
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
import { handleGrpcError } from '@themainstack/communication';
|
|
148
|
+
|
|
149
|
+
client.SomeMethod(request, (err, response) => {
|
|
150
|
+
if (err) {
|
|
151
|
+
try {
|
|
152
|
+
handleGrpcError(err); // Throws normalized error
|
|
153
|
+
} catch (normalizedError) {
|
|
154
|
+
console.error(normalizedError.message);
|
|
155
|
+
// Handle based on error type
|
|
156
|
+
}
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
// Process response
|
|
160
|
+
});
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Full Example
|
|
166
|
+
|
|
167
|
+
See `examples/full-grpc-demo.ts` in the repository for a complete working example.
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## Environment Variables
|
|
172
|
+
|
|
173
|
+
| Variable | Description | Default |
|
|
174
|
+
|----------|-------------|---------|
|
|
175
|
+
| `GRPC_PORT` | Port for gRPC server | `50051` |
|
|
176
|
+
| `FEE_SERVICE_URL` | Fee service gRPC address | `localhost:50053` |
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## API Reference
|
|
181
|
+
|
|
182
|
+
### Proto Generation
|
|
183
|
+
- `generateProtoFromMethods(methods, options)` - Generate proto with service definition
|
|
184
|
+
- `generateProtoFromFunction(fn, name)` - Generate proto for a single message
|
|
185
|
+
|
|
186
|
+
### Server
|
|
187
|
+
- `GrpcServerFactory.createServer(options, handlers)` - Create a gRPC server
|
|
188
|
+
- `exposeAsGrpc(name, handler, samples, options)` - Quick one-liner
|
|
189
|
+
|
|
190
|
+
### Client
|
|
191
|
+
- `GrpcClientFactory.createClient(options)` - Create a gRPC client
|
|
192
|
+
|
|
193
|
+
### Error Handling
|
|
194
|
+
- `handleGrpcError(err)` - Translate gRPC errors to application errors
|
|
@@ -89,7 +89,8 @@ function generateProtoFromMethods(methods, options = {}) {
|
|
|
89
89
|
if (value.length > 0) {
|
|
90
90
|
const firstItem = value[0];
|
|
91
91
|
if (typeof firstItem === "object") {
|
|
92
|
-
|
|
92
|
+
// Scope nested naming: Parent_Child
|
|
93
|
+
const nestedName = `${msgName}_${capitalize(key).replace(/s$/, "")}`;
|
|
93
94
|
type = analyzeMessage(firstItem, nestedName);
|
|
94
95
|
}
|
|
95
96
|
else {
|
|
@@ -103,7 +104,8 @@ function generateProtoFromMethods(methods, options = {}) {
|
|
|
103
104
|
}
|
|
104
105
|
}
|
|
105
106
|
else if (valueType === "object") {
|
|
106
|
-
|
|
107
|
+
// Scope nested naming: Parent_Child
|
|
108
|
+
const nestedName = `${msgName}_${capitalize(key)}`;
|
|
107
109
|
type = analyzeMessage(value, nestedName);
|
|
108
110
|
}
|
|
109
111
|
// Use snake_case for proto fields
|
|
@@ -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: '
|
|
51
|
-
serviceName: '
|
|
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:
|
|
62
|
-
fee:
|
|
61
|
+
originalAmount: 100,
|
|
62
|
+
fee: 10,
|
|
63
63
|
totalAmount: 0,
|
|
64
|
-
currency: '',
|
|
65
|
-
feePercentage:
|
|
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: '
|
|
85
|
-
packageName: '
|
|
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);
|
package/examples/grpc/fee.proto
CHANGED
|
@@ -4,57 +4,19 @@ package fee.v1;
|
|
|
4
4
|
|
|
5
5
|
// FeeService - Auto-generated gRPC service
|
|
6
6
|
service FeeService {
|
|
7
|
-
rpc
|
|
7
|
+
rpc CalculateFee (CalculateFeeRequest) returns (CalculateFeeResponse) {}
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
message
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
int32
|
|
14
|
-
string
|
|
15
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "1.0.2",
|
|
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",
|
|
@@ -23,4 +23,4 @@
|
|
|
23
23
|
"@grpc/grpc-js": "^1.14.3",
|
|
24
24
|
"@grpc/proto-loader": "^0.8.0"
|
|
25
25
|
}
|
|
26
|
-
}
|
|
26
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//registry.npmjs.org/:_authToken=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
|
|
@@ -109,7 +109,8 @@ export function generateProtoFromMethods(
|
|
|
109
109
|
if (value.length > 0) {
|
|
110
110
|
const firstItem = value[0];
|
|
111
111
|
if (typeof firstItem === "object") {
|
|
112
|
-
|
|
112
|
+
// Scope nested naming: Parent_Child
|
|
113
|
+
const nestedName = `${msgName}_${capitalize(key).replace(/s$/, "")}`;
|
|
113
114
|
type = analyzeMessage(firstItem, nestedName);
|
|
114
115
|
} else {
|
|
115
116
|
type = typeof firstItem === "number"
|
|
@@ -120,7 +121,8 @@ export function generateProtoFromMethods(
|
|
|
120
121
|
type = "string";
|
|
121
122
|
}
|
|
122
123
|
} else if (valueType === "object") {
|
|
123
|
-
|
|
124
|
+
// Scope nested naming: Parent_Child
|
|
125
|
+
const nestedName = `${msgName}_${capitalize(key)}`;
|
|
124
126
|
type = analyzeMessage(value, nestedName);
|
|
125
127
|
}
|
|
126
128
|
|