@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,250 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.generateProtoFromMethods = generateProtoFromMethods;
37
+ exports.generateProtoFromFunction = generateProtoFromFunction;
38
+ const fs = __importStar(require("fs"));
39
+ const path = __importStar(require("path"));
40
+ /**
41
+ * Generate a complete .proto file from method definitions
42
+ *
43
+ * @param methods Array of method definitions
44
+ * @param options Generation options
45
+ * @returns The generated proto string
46
+ */
47
+ function generateProtoFromMethods(methods, options = {}) {
48
+ const { packageName, serviceName = 'GeneratedService', outputDir, outputFilename } = options;
49
+ const messages = [];
50
+ const protoMethods = [];
51
+ const processedObjects = new Set();
52
+ // Helper: Capitalize string
53
+ function capitalize(s) {
54
+ return s.charAt(0).toUpperCase() + s.slice(1);
55
+ }
56
+ // Helper: Convert camelCase to snake_case
57
+ function toSnakeCase(s) {
58
+ return s.replace(/([A-Z])/g, '_$1').toLowerCase().replace(/^_/, '');
59
+ }
60
+ // Recursive function to analyze object and generate messages
61
+ function analyzeMessage(obj, msgName) {
62
+ if (obj instanceof Date)
63
+ return "string";
64
+ if (processedObjects.has(obj)) {
65
+ console.warn(`Circular reference detected for ${msgName}. Breaking recursion.`);
66
+ return "string";
67
+ }
68
+ processedObjects.add(obj);
69
+ const fields = [];
70
+ let fieldIdCounter = 1;
71
+ for (const [key, value] of Object.entries(obj)) {
72
+ let type = "string";
73
+ let rule = undefined;
74
+ const valueType = typeof value;
75
+ if (value === null || value === undefined) {
76
+ type = "string";
77
+ }
78
+ else if (valueType === "string") {
79
+ type = "string";
80
+ }
81
+ else if (valueType === "boolean") {
82
+ type = "bool";
83
+ }
84
+ else if (valueType === "number") {
85
+ type = Number.isInteger(value) ? "int32" : "double";
86
+ }
87
+ else if (Array.isArray(value)) {
88
+ rule = "repeated";
89
+ if (value.length > 0) {
90
+ const firstItem = value[0];
91
+ if (typeof firstItem === "object") {
92
+ const nestedName = capitalize(key).replace(/s$/, "");
93
+ type = analyzeMessage(firstItem, nestedName);
94
+ }
95
+ else {
96
+ type = typeof firstItem === "number"
97
+ ? (Number.isInteger(firstItem) ? "int32" : "double")
98
+ : typeof firstItem === "boolean" ? "bool" : "string";
99
+ }
100
+ }
101
+ else {
102
+ type = "string";
103
+ }
104
+ }
105
+ else if (valueType === "object") {
106
+ const nestedName = capitalize(key);
107
+ type = analyzeMessage(value, nestedName);
108
+ }
109
+ // Use snake_case for proto fields
110
+ fields.push({ name: toSnakeCase(key), type, rule, id: fieldIdCounter++ });
111
+ }
112
+ // Check if message already exists (same name)
113
+ const existingIndex = messages.findIndex(m => m.name === msgName);
114
+ if (existingIndex === -1) {
115
+ messages.push({ name: msgName, fields });
116
+ }
117
+ return msgName;
118
+ }
119
+ // Process each method
120
+ for (const method of methods) {
121
+ const requestTypeName = `${method.name}Request`;
122
+ const responseTypeName = `${method.name}Response`;
123
+ // Analyze request and response
124
+ const requestSample = method.requestSample();
125
+ const responseSample = method.responseSample();
126
+ analyzeMessage(requestSample, requestTypeName);
127
+ processedObjects.clear(); // Reset for next analysis
128
+ analyzeMessage(responseSample, responseTypeName);
129
+ processedObjects.clear();
130
+ protoMethods.push({
131
+ name: method.name,
132
+ requestType: requestTypeName,
133
+ responseType: responseTypeName,
134
+ });
135
+ }
136
+ // Format the proto file
137
+ const lines = ['syntax = "proto3";', ''];
138
+ if (packageName) {
139
+ lines.push(`package ${packageName};`, '');
140
+ }
141
+ // Service definition
142
+ lines.push(`// ${serviceName} - Auto-generated gRPC service`);
143
+ lines.push(`service ${serviceName} {`);
144
+ for (const method of protoMethods) {
145
+ lines.push(` rpc ${method.name} (${method.requestType}) returns (${method.responseType}) {}`);
146
+ }
147
+ lines.push('}', '');
148
+ // Message definitions (reverse for proper dependency order)
149
+ for (const msg of messages.reverse()) {
150
+ lines.push(`message ${msg.name} {`);
151
+ for (const field of msg.fields) {
152
+ const rulePrefix = field.rule ? `${field.rule} ` : "";
153
+ lines.push(` ${rulePrefix}${field.type} ${field.name} = ${field.id};`);
154
+ }
155
+ lines.push('}', '');
156
+ }
157
+ const protoContent = lines.join('\n');
158
+ // Save to file if outputDir is specified
159
+ if (outputDir) {
160
+ const filename = outputFilename || `${toSnakeCase(serviceName).replace(/_service$/, '')}.proto`;
161
+ const fullPath = path.join(outputDir, filename);
162
+ // Create directory if it doesn't exist
163
+ if (!fs.existsSync(outputDir)) {
164
+ fs.mkdirSync(outputDir, { recursive: true });
165
+ }
166
+ fs.writeFileSync(fullPath, protoContent);
167
+ console.log(`✅ Proto file generated: ${fullPath}`);
168
+ }
169
+ return protoContent;
170
+ }
171
+ /**
172
+ * Convenience function for single-function proto generation (original API)
173
+ */
174
+ function generateProtoFromFunction(generatorFn, rootMessageName = "RootMessage") {
175
+ const messages = [];
176
+ const processedObjects = new Set();
177
+ function capitalize(s) {
178
+ return s.charAt(0).toUpperCase() + s.slice(1);
179
+ }
180
+ function toSnakeCase(s) {
181
+ return s.replace(/([A-Z])/g, '_$1').toLowerCase().replace(/^_/, '');
182
+ }
183
+ function analyzeMessage(obj, msgName) {
184
+ if (obj instanceof Date)
185
+ return "string";
186
+ if (processedObjects.has(obj)) {
187
+ return "string";
188
+ }
189
+ processedObjects.add(obj);
190
+ const fields = [];
191
+ let fieldIdCounter = 1;
192
+ for (const [key, value] of Object.entries(obj)) {
193
+ let type = "string";
194
+ let rule = undefined;
195
+ const valueType = typeof value;
196
+ if (value === null || value === undefined) {
197
+ type = "string";
198
+ }
199
+ else if (valueType === "string") {
200
+ type = "string";
201
+ }
202
+ else if (valueType === "boolean") {
203
+ type = "bool";
204
+ }
205
+ else if (valueType === "number") {
206
+ type = Number.isInteger(value) ? "int32" : "double";
207
+ }
208
+ else if (Array.isArray(value)) {
209
+ rule = "repeated";
210
+ if (value.length > 0) {
211
+ const firstItem = value[0];
212
+ if (typeof firstItem === "object") {
213
+ const nestedName = capitalize(key).replace(/s$/, "");
214
+ type = analyzeMessage(firstItem, nestedName);
215
+ }
216
+ else {
217
+ type = typeof firstItem === "number"
218
+ ? (Number.isInteger(firstItem) ? "int32" : "double")
219
+ : typeof firstItem === "boolean" ? "bool" : "string";
220
+ }
221
+ }
222
+ else {
223
+ type = "string";
224
+ }
225
+ }
226
+ else if (valueType === "object") {
227
+ const nestedName = capitalize(key);
228
+ type = analyzeMessage(value, nestedName);
229
+ }
230
+ fields.push({ name: toSnakeCase(key), type, rule, id: fieldIdCounter++ });
231
+ }
232
+ messages.push({ name: msgName, fields });
233
+ return msgName;
234
+ }
235
+ const sampleData = generatorFn();
236
+ if (!sampleData || typeof sampleData !== "object") {
237
+ throw new Error("Generator function must return a non-null object.");
238
+ }
239
+ analyzeMessage(sampleData, rootMessageName);
240
+ const lines = ['syntax = "proto3";', ''];
241
+ for (const msg of messages.reverse()) {
242
+ lines.push(`message ${msg.name} {`);
243
+ for (const field of msg.fields) {
244
+ const rulePrefix = field.rule ? `${field.rule} ` : "";
245
+ lines.push(` ${rulePrefix}${field.type} ${field.name} = ${field.id};`);
246
+ }
247
+ lines.push('}', '');
248
+ }
249
+ return lines.join('\n');
250
+ }
@@ -0,0 +1,203 @@
1
+ # @themainstack/communication - gRPC Usage Guide
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. [Step 1: Proto Generation](#step-1-proto-generation)
9
+ 4. [Step 2: Creating a gRPC Server](#step-2-creating-a-grpc-server)
10
+ 5. [Step 3: Creating a gRPC Client](#step-3-creating-a-grpc-client)
11
+ 6. [Step 4: Error Handling](#step-4-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
+ ## Step 1: 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
+ ## Step 2: 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
+ ## Step 3: 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
+ ## Step 4: 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` for a complete working example that:
168
+ 1. Defines a business function
169
+ 2. Creates a gRPC server (proto auto-generated)
170
+ 3. Creates a client and calls the server
171
+ 4. Handles the response
172
+
173
+ Run it:
174
+ ```bash
175
+ npx ts-node examples/full-grpc-demo.ts
176
+ ```
177
+
178
+ ---
179
+
180
+ ## Environment Variables
181
+
182
+ | Variable | Description | Default |
183
+ |----------|-------------|---------|
184
+ | `GRPC_PORT` | Port for gRPC server | `50051` |
185
+ | `FEE_SERVICE_URL` | Fee service gRPC address | `localhost:50053` |
186
+
187
+ ---
188
+
189
+ ## API Reference
190
+
191
+ ### Proto Generation
192
+ - `generateProtoFromMethods(methods, options)` - Generate proto with service definition
193
+ - `generateProtoFromFunction(fn, name)` - Generate proto for a single message
194
+
195
+ ### Server
196
+ - `GrpcServerFactory.createServer(options, handlers)` - Create a gRPC server
197
+ - `exposeAsGrpc(name, handler, samples, options)` - Quick one-liner
198
+
199
+ ### Client
200
+ - `GrpcClientFactory.createClient(options)` - Create a gRPC client
201
+
202
+ ### Error Handling
203
+ - `handleGrpcError(err)` - Translate gRPC errors to application errors