@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.
- package/dist/grpc/client-factory.d.ts +5 -0
- package/dist/grpc/client-factory.js +73 -0
- package/dist/grpc/errors.d.ts +8 -0
- package/dist/grpc/errors.js +24 -0
- package/dist/grpc/server-factory.d.ts +98 -0
- package/dist/grpc/server-factory.js +220 -0
- package/dist/grpc/types.d.ts +10 -0
- package/dist/grpc/types.js +2 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +33 -0
- package/dist/proto-generator/index.d.ts +36 -0
- package/dist/proto-generator/index.js +250 -0
- package/docs/GRPC_USAGE.md +203 -0
- package/docs/INTEGRATION_EXAMPLE.md +364 -0
- package/examples/full-grpc-demo.ts +139 -0
- package/examples/grpc/calculator.proto +22 -0
- package/examples/grpc/fee.proto +60 -0
- package/examples/protos/hero.proto +17 -0
- package/package.json +26 -0
- package/src/grpc/client-factory.ts +48 -0
- package/src/grpc/errors.ts +31 -0
- package/src/grpc/server-factory.ts +263 -0
- package/src/grpc/types.ts +11 -0
- package/src/index.ts +42 -0
- package/src/proto-generator/index.ts +293 -0
- package/tests/fixtures/dummy.proto +15 -0
- package/tests/fixtures/no-package.proto +15 -0
- package/tests/grpc/client-factory.spec.ts +44 -0
- package/tests/grpc/errors.spec.ts +31 -0
- package/tests/index.spec.ts +29 -0
- package/tsconfig.json +13 -0
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
# Detailed Integration Example: fee-service ↔ payment-service
|
|
2
|
+
|
|
3
|
+
This document shows exactly how to integrate two services using `@themainstack/communication`.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Part 1: Fee Service (The Server)
|
|
8
|
+
|
|
9
|
+
### 1.1 Install the Package
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
cd fee-service
|
|
13
|
+
yarn add @themainstack/communication
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
### 1.2 Your Existing Business Logic
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
// fee-service/src/fee/fees.service.ts
|
|
20
|
+
import { Injectable } from '@nestjs/common';
|
|
21
|
+
|
|
22
|
+
@Injectable()
|
|
23
|
+
export class FeesService {
|
|
24
|
+
async calculateFeesForPlan(
|
|
25
|
+
planSlug: string,
|
|
26
|
+
currency: string,
|
|
27
|
+
amount: number,
|
|
28
|
+
type: string,
|
|
29
|
+
) {
|
|
30
|
+
// Your existing business logic...
|
|
31
|
+
const feePercentage = planSlug === 'starter' ? 2.5 : 1.5;
|
|
32
|
+
const fee = amount * (feePercentage / 100);
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
dollarAmount: amount,
|
|
36
|
+
dollarTransactionFee: fee,
|
|
37
|
+
localTransactionFee: fee * 400, // Example: NGN rate
|
|
38
|
+
localCurrency: currency,
|
|
39
|
+
localAmount: (amount - fee).toString(),
|
|
40
|
+
exchangeRate: 400,
|
|
41
|
+
appliedFees: [{
|
|
42
|
+
id: 'fee_001',
|
|
43
|
+
type: 'withdrawal',
|
|
44
|
+
displayName: 'Withdrawal Fee',
|
|
45
|
+
amount: fee,
|
|
46
|
+
amountType: 'percent',
|
|
47
|
+
}],
|
|
48
|
+
appliedFeeIds: ['fee_001'],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### 1.3 Create the gRPC Bootstrap File
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
// fee-service/src/grpc/grpc-bootstrap.ts
|
|
58
|
+
import { GrpcServerFactory } from '@themainstack/communication';
|
|
59
|
+
import { FeesService } from '../fee/fees.service';
|
|
60
|
+
|
|
61
|
+
const feesService = new FeesService();
|
|
62
|
+
|
|
63
|
+
export async function startGrpcServer() {
|
|
64
|
+
const server = await GrpcServerFactory.createServer({
|
|
65
|
+
packageName: 'fee.v1',
|
|
66
|
+
serviceName: 'FeeService',
|
|
67
|
+
port: parseInt(process.env.GRPC_PORT || '50053'),
|
|
68
|
+
protoOutputDir: './src/grpc',
|
|
69
|
+
}, [
|
|
70
|
+
{
|
|
71
|
+
name: 'CalculateWithdrawalFee',
|
|
72
|
+
handler: async (request: any) => {
|
|
73
|
+
// Call your existing service
|
|
74
|
+
const result = await feesService.calculateFeesForPlan(
|
|
75
|
+
'starter', // Default to starter plan
|
|
76
|
+
request.currency,
|
|
77
|
+
request.amount,
|
|
78
|
+
'withdrawal',
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
// Map to proto response format
|
|
82
|
+
return {
|
|
83
|
+
dollar_amount: result.dollarAmount,
|
|
84
|
+
dollar_transaction_fee: result.dollarTransactionFee,
|
|
85
|
+
local_transaction_fee: result.localTransactionFee,
|
|
86
|
+
local_currency: result.localCurrency,
|
|
87
|
+
local_amount: result.localAmount,
|
|
88
|
+
exchange_rate: result.exchangeRate,
|
|
89
|
+
applied_fees: result.appliedFees.map(f => ({
|
|
90
|
+
id: f.id,
|
|
91
|
+
type: f.type,
|
|
92
|
+
display_name: f.displayName,
|
|
93
|
+
amount: f.amount,
|
|
94
|
+
amount_type: f.amountType,
|
|
95
|
+
})),
|
|
96
|
+
applied_fee_ids: result.appliedFeeIds,
|
|
97
|
+
};
|
|
98
|
+
},
|
|
99
|
+
// Sample data for proto generation
|
|
100
|
+
requestSample: () => ({
|
|
101
|
+
merchantId: '',
|
|
102
|
+
amount: 0,
|
|
103
|
+
currency: '',
|
|
104
|
+
payoutMethod: '',
|
|
105
|
+
}),
|
|
106
|
+
responseSample: () => ({
|
|
107
|
+
dollarAmount: 0,
|
|
108
|
+
dollarTransactionFee: 0,
|
|
109
|
+
localTransactionFee: 0,
|
|
110
|
+
localCurrency: '',
|
|
111
|
+
localAmount: '',
|
|
112
|
+
exchangeRate: 0,
|
|
113
|
+
appliedFees: [{
|
|
114
|
+
id: '',
|
|
115
|
+
type: '',
|
|
116
|
+
displayName: '',
|
|
117
|
+
amount: 0,
|
|
118
|
+
amountType: '',
|
|
119
|
+
}],
|
|
120
|
+
appliedFeeIds: [''],
|
|
121
|
+
}),
|
|
122
|
+
},
|
|
123
|
+
]);
|
|
124
|
+
|
|
125
|
+
await server.start();
|
|
126
|
+
return server;
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### 1.4 Integrate with main.ts
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
// fee-service/src/main.ts
|
|
134
|
+
import { NestFactory } from '@nestjs/core';
|
|
135
|
+
import { AppModule } from './app.module';
|
|
136
|
+
import { startGrpcServer } from './grpc/grpc-bootstrap';
|
|
137
|
+
|
|
138
|
+
async function bootstrap() {
|
|
139
|
+
// Start HTTP server (existing)
|
|
140
|
+
const app = await NestFactory.create(AppModule);
|
|
141
|
+
const httpPort = process.env.PORT || 40400;
|
|
142
|
+
await app.listen(httpPort);
|
|
143
|
+
console.log(`HTTP server on port ${httpPort}`);
|
|
144
|
+
|
|
145
|
+
// Start gRPC server (new)
|
|
146
|
+
await startGrpcServer();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
bootstrap();
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### 1.5 Result
|
|
153
|
+
|
|
154
|
+
When you start fee-service, it will:
|
|
155
|
+
1. Auto-generate `./src/grpc/fee.proto`
|
|
156
|
+
2. Start HTTP on port 40400
|
|
157
|
+
3. Start gRPC on port 50053
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Part 2: Payment Service (The Client)
|
|
162
|
+
|
|
163
|
+
### 2.1 Install the Package
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
cd payment-service
|
|
167
|
+
yarn add @themainstack/communication
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### 2.2 Copy the Proto File
|
|
171
|
+
|
|
172
|
+
Copy the generated proto from fee-service:
|
|
173
|
+
```bash
|
|
174
|
+
mkdir -p src/payouts/protos
|
|
175
|
+
cp ../fee-service/src/grpc/fee.proto src/payouts/protos/
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### 2.3 Create the Fee Client
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
// payment-service/src/payouts/clients/fee.client.ts
|
|
182
|
+
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
|
183
|
+
import { GrpcClientFactory, handleGrpcError } from '@themainstack/communication';
|
|
184
|
+
import { join } from 'path';
|
|
185
|
+
|
|
186
|
+
export interface FeeCalculation {
|
|
187
|
+
dollarAmount: number;
|
|
188
|
+
dollarTransactionFee: number;
|
|
189
|
+
localTransactionFee: number;
|
|
190
|
+
localCurrency: string;
|
|
191
|
+
localAmount: string;
|
|
192
|
+
exchangeRate: number;
|
|
193
|
+
appliedFees: any[];
|
|
194
|
+
appliedFeeIds: string[];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
@Injectable()
|
|
198
|
+
export class FeeClient implements OnModuleInit {
|
|
199
|
+
private readonly logger = new Logger(FeeClient.name);
|
|
200
|
+
private client: any;
|
|
201
|
+
|
|
202
|
+
onModuleInit() {
|
|
203
|
+
const FEE_SERVICE_URL = process.env.FEE_SERVICE_GRPC_URL || 'localhost:50053';
|
|
204
|
+
|
|
205
|
+
this.client = GrpcClientFactory.createClient({
|
|
206
|
+
serviceName: 'FeeService',
|
|
207
|
+
packageName: 'fee.v1',
|
|
208
|
+
protoPath: join(__dirname, '../protos/fee.proto'),
|
|
209
|
+
url: FEE_SERVICE_URL,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
this.logger.log(`Fee gRPC Client connected to ${FEE_SERVICE_URL}`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async calculateWithdrawalFee(
|
|
216
|
+
merchantId: string,
|
|
217
|
+
amount: number,
|
|
218
|
+
currency: string,
|
|
219
|
+
payoutMethod: string,
|
|
220
|
+
): Promise<FeeCalculation> {
|
|
221
|
+
return new Promise((resolve, reject) => {
|
|
222
|
+
this.client.CalculateWithdrawalFee(
|
|
223
|
+
{
|
|
224
|
+
merchant_id: merchantId,
|
|
225
|
+
amount,
|
|
226
|
+
currency,
|
|
227
|
+
payout_method: payoutMethod,
|
|
228
|
+
},
|
|
229
|
+
(err: any, response: any) => {
|
|
230
|
+
if (err) {
|
|
231
|
+
try {
|
|
232
|
+
handleGrpcError(err);
|
|
233
|
+
} catch (normalizedError: any) {
|
|
234
|
+
this.logger.error(`Fee calculation failed: ${normalizedError.message}`);
|
|
235
|
+
reject(normalizedError);
|
|
236
|
+
}
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Map snake_case response to camelCase
|
|
241
|
+
resolve({
|
|
242
|
+
dollarAmount: response.dollar_amount,
|
|
243
|
+
dollarTransactionFee: response.dollar_transaction_fee,
|
|
244
|
+
localTransactionFee: response.local_transaction_fee,
|
|
245
|
+
localCurrency: response.local_currency,
|
|
246
|
+
localAmount: response.local_amount,
|
|
247
|
+
exchangeRate: response.exchange_rate,
|
|
248
|
+
appliedFees: response.applied_fees || [],
|
|
249
|
+
appliedFeeIds: response.applied_fee_ids || [],
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
);
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### 2.4 Register in Module
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
// payment-service/src/payouts/payouts.module.ts
|
|
262
|
+
import { Module } from '@nestjs/common';
|
|
263
|
+
import { PayoutsService } from './payouts.service';
|
|
264
|
+
import { FeeClient } from './clients/fee.client';
|
|
265
|
+
|
|
266
|
+
@Module({
|
|
267
|
+
providers: [PayoutsService, FeeClient],
|
|
268
|
+
exports: [PayoutsService],
|
|
269
|
+
})
|
|
270
|
+
export class PayoutsModule {}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### 2.5 Use in Your Service
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
// payment-service/src/payouts/payouts.service.ts
|
|
277
|
+
import { Injectable } from '@nestjs/common';
|
|
278
|
+
import { FeeClient } from './clients/fee.client';
|
|
279
|
+
|
|
280
|
+
@Injectable()
|
|
281
|
+
export class PayoutsService {
|
|
282
|
+
constructor(private readonly feeClient: FeeClient) {}
|
|
283
|
+
|
|
284
|
+
async processPayout(merchantId: string, amount: number, currency: string) {
|
|
285
|
+
// Call fee-service via gRPC
|
|
286
|
+
const feeCalculation = await this.feeClient.calculateWithdrawalFee(
|
|
287
|
+
merchantId,
|
|
288
|
+
amount,
|
|
289
|
+
currency,
|
|
290
|
+
'bank_transfer',
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
console.log('Fee:', feeCalculation.dollarTransactionFee);
|
|
294
|
+
console.log('Net Amount:', amount - feeCalculation.dollarTransactionFee);
|
|
295
|
+
|
|
296
|
+
// Continue with payout logic...
|
|
297
|
+
return {
|
|
298
|
+
grossAmount: amount,
|
|
299
|
+
fee: feeCalculation.dollarTransactionFee,
|
|
300
|
+
netAmount: amount - feeCalculation.dollarTransactionFee,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
308
|
+
## Part 3: Environment Configuration
|
|
309
|
+
|
|
310
|
+
### fee-service/.env
|
|
311
|
+
```env
|
|
312
|
+
PORT=40400
|
|
313
|
+
GRPC_PORT=50053
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### payment-service/.env
|
|
317
|
+
```env
|
|
318
|
+
PORT=40401
|
|
319
|
+
FEE_SERVICE_GRPC_URL=localhost:50053
|
|
320
|
+
# In Docker/K8s: FEE_SERVICE_GRPC_URL=fee-service:50053
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
325
|
+
## Part 4: Testing the Integration
|
|
326
|
+
|
|
327
|
+
### Start fee-service
|
|
328
|
+
```bash
|
|
329
|
+
cd fee-service
|
|
330
|
+
yarn start:dev
|
|
331
|
+
# Output:
|
|
332
|
+
# HTTP server on port 40400
|
|
333
|
+
# 🚀 gRPC Server running on 0.0.0.0:50053
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### Start payment-service
|
|
337
|
+
```bash
|
|
338
|
+
cd payment-service
|
|
339
|
+
yarn start:dev
|
|
340
|
+
# Output:
|
|
341
|
+
# Fee gRPC Client connected to localhost:50053
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
### Make a Request
|
|
345
|
+
```bash
|
|
346
|
+
curl -X POST http://localhost:40401/payouts/process \
|
|
347
|
+
-H "Content-Type: application/json" \
|
|
348
|
+
-d '{"merchantId": "m123", "amount": 1000, "currency": "USD"}'
|
|
349
|
+
|
|
350
|
+
# Response:
|
|
351
|
+
# { "grossAmount": 1000, "fee": 25, "netAmount": 975 }
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
---
|
|
355
|
+
|
|
356
|
+
## Summary
|
|
357
|
+
|
|
358
|
+
| Step | fee-service | payment-service |
|
|
359
|
+
|------|-------------|-----------------|
|
|
360
|
+
| 1 | Install package | Install package |
|
|
361
|
+
| 2 | Write business function | Copy proto file |
|
|
362
|
+
| 3 | Use `GrpcServerFactory` | Use `GrpcClientFactory` |
|
|
363
|
+
| 4 | Add to main.ts | Create client class |
|
|
364
|
+
| 5 | Proto auto-generated! | Call client methods |
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Full Demo: gRPC Server + Client using @themainstack/communication
|
|
3
|
+
*
|
|
4
|
+
* This demonstrates the complete workflow:
|
|
5
|
+
* 1. Define a function you want to expose
|
|
6
|
+
* 2. Create a gRPC server (proto auto-generated)
|
|
7
|
+
* 3. Create a client to call the server
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { GrpcServerFactory, GrpcClientFactory, handleGrpcError } from '../src';
|
|
11
|
+
import * as path from 'path';
|
|
12
|
+
|
|
13
|
+
// 1: Define your business logic function
|
|
14
|
+
|
|
15
|
+
interface FeeRequest {
|
|
16
|
+
merchantId: string;
|
|
17
|
+
amount: number;
|
|
18
|
+
currency: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface FeeResponse {
|
|
22
|
+
originalAmount: number;
|
|
23
|
+
fee: number;
|
|
24
|
+
totalAmount: number;
|
|
25
|
+
currency: string;
|
|
26
|
+
feePercentage: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Your existing business function
|
|
30
|
+
async function calculateFee(request: FeeRequest): Promise<FeeResponse> {
|
|
31
|
+
const feePercentage = 2.5; // 2.5% fee
|
|
32
|
+
const fee = request.amount * (feePercentage / 100);
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
originalAmount: request.amount,
|
|
36
|
+
fee: fee,
|
|
37
|
+
totalAmount: request.amount + fee,
|
|
38
|
+
currency: request.currency,
|
|
39
|
+
feePercentage: feePercentage,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 2: Create and start the gRPC server
|
|
44
|
+
|
|
45
|
+
async function startServer() {
|
|
46
|
+
console.log('Starting gRPC Server...\n');
|
|
47
|
+
|
|
48
|
+
const server = await GrpcServerFactory.createServer(
|
|
49
|
+
{
|
|
50
|
+
packageName: 'calculator.v1',
|
|
51
|
+
serviceName: 'CalculatorService',
|
|
52
|
+
port: 50055,
|
|
53
|
+
protoOutputDir: path.join(__dirname, 'grpc'),
|
|
54
|
+
},
|
|
55
|
+
[
|
|
56
|
+
{
|
|
57
|
+
name: 'CalculateFee',
|
|
58
|
+
handler: calculateFee, // Your existing function!
|
|
59
|
+
requestSample: () => ({ merchantId: '', amount: 0, currency: '' }),
|
|
60
|
+
responseSample: () => ({
|
|
61
|
+
originalAmount: 0,
|
|
62
|
+
fee: 0,
|
|
63
|
+
totalAmount: 0,
|
|
64
|
+
currency: '',
|
|
65
|
+
feePercentage: 0
|
|
66
|
+
}),
|
|
67
|
+
},
|
|
68
|
+
]
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
await server.start();
|
|
72
|
+
return server;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 3: Create a client and call the server
|
|
76
|
+
|
|
77
|
+
async function callServer() {
|
|
78
|
+
console.log('\nCreating gRPC Client...\n');
|
|
79
|
+
|
|
80
|
+
// Wait a moment for server to be ready
|
|
81
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
82
|
+
|
|
83
|
+
const client = GrpcClientFactory.createClient<any>({
|
|
84
|
+
serviceName: 'CalculatorService',
|
|
85
|
+
packageName: 'calculator.v1',
|
|
86
|
+
protoPath: path.join(__dirname, 'grpc', 'calculator.proto'),
|
|
87
|
+
url: 'localhost:50055',
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Make a gRPC call
|
|
91
|
+
return new Promise<FeeResponse>((resolve, reject) => {
|
|
92
|
+
client.CalculateFee(
|
|
93
|
+
{ merchantId: 'merchant_123', amount: 1000, currency: 'USD' },
|
|
94
|
+
(err: any, response: any) => {
|
|
95
|
+
if (err) {
|
|
96
|
+
try {
|
|
97
|
+
handleGrpcError(err);
|
|
98
|
+
} catch (e) {
|
|
99
|
+
reject(e);
|
|
100
|
+
}
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
resolve(response);
|
|
104
|
+
}
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// RUN THE DEMO
|
|
110
|
+
async function main() {
|
|
111
|
+
console.log('------------------------------------------------------------');
|
|
112
|
+
console.log(' @themainstack/communication - Full gRPC Demo');
|
|
113
|
+
console.log('------------------------------------------------------------');
|
|
114
|
+
console.log('');
|
|
115
|
+
|
|
116
|
+
// Start server
|
|
117
|
+
const server = await startServer();
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
// Call the server
|
|
121
|
+
const result = await callServer();
|
|
122
|
+
|
|
123
|
+
console.log('gRPC Call Successful!\n');
|
|
124
|
+
console.log('Request: { merchantId: "merchant_123", amount: 1000, currency: "USD" }');
|
|
125
|
+
console.log('Response:', JSON.stringify(result, null, 2));
|
|
126
|
+
console.log('');
|
|
127
|
+
} catch (error) {
|
|
128
|
+
console.error('Error:', error);
|
|
129
|
+
} finally {
|
|
130
|
+
// Stop server
|
|
131
|
+
await server.stop();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
console.log('------------------------------------------------------------');
|
|
135
|
+
console.log(' Demo Complete!');
|
|
136
|
+
console.log('------------------------------------------------------------');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
syntax = "proto3";
|
|
2
|
+
|
|
3
|
+
package calculator.v1;
|
|
4
|
+
|
|
5
|
+
// CalculatorService - Auto-generated gRPC service
|
|
6
|
+
service CalculatorService {
|
|
7
|
+
rpc CalculateFee (CalculateFeeRequest) returns (CalculateFeeResponse) {}
|
|
8
|
+
}
|
|
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;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
message CalculateFeeRequest {
|
|
19
|
+
string merchant_id = 1;
|
|
20
|
+
int32 amount = 2;
|
|
21
|
+
string currency = 3;
|
|
22
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
syntax = "proto3";
|
|
2
|
+
|
|
3
|
+
package fee.v1;
|
|
4
|
+
|
|
5
|
+
// FeeService - Auto-generated gRPC service
|
|
6
|
+
service FeeService {
|
|
7
|
+
rpc CalculateWithdrawalFee (CalculateWithdrawalFeeRequest) returns (CalculateWithdrawalFeeResponse) {}
|
|
8
|
+
}
|
|
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;
|
|
20
|
+
}
|
|
21
|
+
|
|
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 {
|
|
56
|
+
string merchant_id = 1;
|
|
57
|
+
double amount = 2;
|
|
58
|
+
string currency = 3;
|
|
59
|
+
string payout_method = 4;
|
|
60
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
syntax = "proto3";
|
|
2
|
+
|
|
3
|
+
package hero.v1;
|
|
4
|
+
|
|
5
|
+
service HeroService {
|
|
6
|
+
rpc GetHero (HeroRequest) returns (HeroResponse) {}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
message HeroRequest {
|
|
10
|
+
int32 id = 1;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
message HeroResponse {
|
|
14
|
+
int32 id = 1;
|
|
15
|
+
string name = 2;
|
|
16
|
+
string power = 3;
|
|
17
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@themainstack/communication",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Unified gRPC framework for inter-service communication - auto-generates protos, creates servers, and provides type-safe clients",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc -p tsconfig.json",
|
|
10
|
+
"prepublishOnly": "npm run build",
|
|
11
|
+
"test": "npx vitest --run"
|
|
12
|
+
},
|
|
13
|
+
"author": "Mainstack Engineering",
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"typescript": "^5.3.3",
|
|
17
|
+
"vitest": "^4.0.7"
|
|
18
|
+
},
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@grpc/grpc-js": "^1.14.3",
|
|
24
|
+
"@grpc/proto-loader": "^0.8.0"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import * as grpc from '@grpc/grpc-js';
|
|
2
|
+
import * as protoLoader from '@grpc/proto-loader';
|
|
3
|
+
import { GrpcClientOptions } from './types';
|
|
4
|
+
|
|
5
|
+
export class GrpcClientFactory {
|
|
6
|
+
static createClient<T extends grpc.Client>(options: GrpcClientOptions): T {
|
|
7
|
+
const packageDefinition = protoLoader.loadSync(options.protoPath, {
|
|
8
|
+
keepCase: true,
|
|
9
|
+
longs: String,
|
|
10
|
+
enums: String,
|
|
11
|
+
defaults: true,
|
|
12
|
+
oneofs: true,
|
|
13
|
+
...options.loaderOptions,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const protoDescriptor = grpc.loadPackageDefinition(packageDefinition);
|
|
17
|
+
|
|
18
|
+
let serviceDef: any = protoDescriptor;
|
|
19
|
+
|
|
20
|
+
// Default to empty string (root) if not provided
|
|
21
|
+
const packageName = options.packageName || '';
|
|
22
|
+
|
|
23
|
+
// Only traverse if packageName is non-empty
|
|
24
|
+
if (packageName) {
|
|
25
|
+
const pkgNameParts = packageName.split('.');
|
|
26
|
+
for (const part of pkgNameParts) {
|
|
27
|
+
if (serviceDef[part]) {
|
|
28
|
+
serviceDef = serviceDef[part];
|
|
29
|
+
} else {
|
|
30
|
+
throw new Error(`Package part '${part}' not found in proto definition`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!serviceDef[options.serviceName]) {
|
|
36
|
+
const location = packageName ? `package '${packageName}'` : 'root definition';
|
|
37
|
+
throw new Error(`Service '${options.serviceName}' not found in ${location}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const ServiceClient = serviceDef[options.serviceName];
|
|
41
|
+
|
|
42
|
+
return new ServiceClient(
|
|
43
|
+
options.url,
|
|
44
|
+
options.credentials || grpc.credentials.createInsecure(),
|
|
45
|
+
options.channelOptions
|
|
46
|
+
) as T;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { ServiceError, status } from '@grpc/grpc-js';
|
|
2
|
+
|
|
3
|
+
export class GrpcError extends Error {
|
|
4
|
+
public code: status;
|
|
5
|
+
public details: string;
|
|
6
|
+
public metadata: any;
|
|
7
|
+
|
|
8
|
+
constructor(code: status, message: string, details?: string, metadata?: any) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = 'GrpcError';
|
|
11
|
+
this.code = code;
|
|
12
|
+
this.details = details || message;
|
|
13
|
+
this.metadata = metadata;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const handleGrpcError = (err: any): never => {
|
|
18
|
+
if (err && (err.code !== undefined || err.message)) {
|
|
19
|
+
const error = err as ServiceError;
|
|
20
|
+
// Map gRPC status codes to more friendly errors if needed
|
|
21
|
+
// For now, we wrap it in our GrpcError
|
|
22
|
+
throw new GrpcError(
|
|
23
|
+
error.code ?? status.UNKNOWN,
|
|
24
|
+
error.message || 'Unknown gRPC error',
|
|
25
|
+
error.details,
|
|
26
|
+
error.metadata
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
throw new GrpcError(status.UNKNOWN, 'An unexpected error occurred', JSON.stringify(err));
|
|
31
|
+
};
|