@themainstack/communication 1.0.2 → 1.1.1
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 +1 -0
- package/dist/grpc/client-factory.js +64 -1
- package/dist/grpc/server-factory.js +22 -1
- package/dist/grpc/struct-utils.d.ts +19 -0
- package/dist/grpc/struct-utils.js +81 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -1
- package/dist/proto-generator/index.d.ts +10 -0
- package/dist/proto-generator/index.js +21 -2
- package/package.json +2 -2
- package/src/grpc/client-factory.ts +74 -1
- package/src/grpc/server-factory.ts +26 -1
- package/src/grpc/struct-utils.ts +73 -0
- package/src/index.ts +2 -1
- package/src/proto-generator/index.ts +24 -3
- package/tests/struct-support.spec.ts +100 -0
- package/publish-config.npmrc +0 -1
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
|
|
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.GrpcClientFactory = void 0;
|
|
37
37
|
const grpc = __importStar(require("@grpc/grpc-js"));
|
|
38
38
|
const protoLoader = __importStar(require("@grpc/proto-loader"));
|
|
39
|
+
const struct_utils_1 = require("./struct-utils");
|
|
39
40
|
class GrpcClientFactory {
|
|
40
41
|
static createClient(options) {
|
|
41
42
|
const packageDefinition = protoLoader.loadSync(options.protoPath, {
|
|
@@ -67,7 +68,69 @@ class GrpcClientFactory {
|
|
|
67
68
|
throw new Error(`Service '${options.serviceName}' not found in ${location}`);
|
|
68
69
|
}
|
|
69
70
|
const ServiceClient = serviceDef[options.serviceName];
|
|
70
|
-
|
|
71
|
+
const client = new ServiceClient(options.url, options.credentials || grpc.credentials.createInsecure(), options.channelOptions);
|
|
72
|
+
// Wrap the client to intercept methods and handle Struct conversion automatically
|
|
73
|
+
const wrapper = new Proxy(client, {
|
|
74
|
+
get(target, prop) {
|
|
75
|
+
const value = target[prop];
|
|
76
|
+
// If accessing a method function, wrap it
|
|
77
|
+
if (typeof prop === 'string' && typeof value === 'function') {
|
|
78
|
+
// Find the method definition in the proto package definition
|
|
79
|
+
// packageDefinition uses flattened keys: 'package.Service.Method' or 'Service.Method' if in root
|
|
80
|
+
const methodKey = packageName
|
|
81
|
+
? `${packageName}.${options.serviceName}`
|
|
82
|
+
: options.serviceName;
|
|
83
|
+
// We need to look up the method definition to get request type
|
|
84
|
+
// The loaded packageDefinition is a flat map of FQN -> type definition
|
|
85
|
+
// But we don't have easy link from method name to request FQN here without traversing.
|
|
86
|
+
// Fortunately, ServiceClient.service (available on constructor) has the method definitions!
|
|
87
|
+
const serviceDefinition = ServiceClient.service;
|
|
88
|
+
const methodDef = serviceDefinition[prop];
|
|
89
|
+
if (methodDef && !methodDef.requestStream && !methodDef.responseStream) {
|
|
90
|
+
return (...args) => {
|
|
91
|
+
const request = args[0];
|
|
92
|
+
if (request && typeof request === 'object') {
|
|
93
|
+
// Introspect and convert Struct fields
|
|
94
|
+
const requestType = methodDef.requestType; // This contains the type definition structure
|
|
95
|
+
if (requestType && requestType.format === 'Protocol Buffer 3 DescriptorProto') {
|
|
96
|
+
// With @grpc/grpc-js and proto-loader, type info is attached
|
|
97
|
+
// traverse fields to find google.protobuf.Struct
|
|
98
|
+
for (const field of requestType.type.field) {
|
|
99
|
+
if (field.typeName === 'google.protobuf.Struct' && request[field.name]) {
|
|
100
|
+
// Auto-convert JSON to Struct
|
|
101
|
+
request[field.name] = (0, struct_utils_1.jsonToStruct)(request[field.name]);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// Intercept callback to unwrap Response Structs
|
|
107
|
+
const originalCallback = typeof args[args.length - 1] === 'function'
|
|
108
|
+
? args[args.length - 1]
|
|
109
|
+
: undefined;
|
|
110
|
+
if (originalCallback) {
|
|
111
|
+
args[args.length - 1] = (err, response) => {
|
|
112
|
+
if (response && typeof response === 'object') {
|
|
113
|
+
const responseType = methodDef.responseType;
|
|
114
|
+
if (responseType && responseType.format === 'Protocol Buffer 3 DescriptorProto') {
|
|
115
|
+
for (const field of responseType.type.field) {
|
|
116
|
+
if (field.typeName === 'google.protobuf.Struct' && response[field.name]) {
|
|
117
|
+
// Auto-convert Struct -> JSON
|
|
118
|
+
response[field.name] = (0, struct_utils_1.structToJson)(response[field.name]);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
originalCallback(err, response);
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
return value.apply(target, args);
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return value;
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
return wrapper;
|
|
71
134
|
}
|
|
72
135
|
}
|
|
73
136
|
exports.GrpcClientFactory = GrpcClientFactory;
|
|
@@ -39,6 +39,7 @@ const grpc = __importStar(require("@grpc/grpc-js"));
|
|
|
39
39
|
const protoLoader = __importStar(require("@grpc/proto-loader"));
|
|
40
40
|
const path = __importStar(require("path"));
|
|
41
41
|
const proto_generator_1 = require("../proto-generator");
|
|
42
|
+
const struct_utils_1 = require("./struct-utils");
|
|
42
43
|
/**
|
|
43
44
|
* GrpcServerFactory - Create and run gRPC servers easily
|
|
44
45
|
*
|
|
@@ -123,7 +124,27 @@ class GrpcServerFactory {
|
|
|
123
124
|
const methodName = handler.name.charAt(0).toLowerCase() + handler.name.slice(1);
|
|
124
125
|
implementations[methodName] = async (call, callback) => {
|
|
125
126
|
try {
|
|
126
|
-
|
|
127
|
+
let request = call.request;
|
|
128
|
+
// Introspect Request for Struct fields and unwrap to JSON
|
|
129
|
+
const methodDef = ServiceConstructor.service[handler.name];
|
|
130
|
+
if (methodDef && methodDef.requestType && methodDef.requestType.format === 'Protocol Buffer 3 DescriptorProto') {
|
|
131
|
+
for (const field of methodDef.requestType.type.field) {
|
|
132
|
+
if (field.typeName === 'google.protobuf.Struct' && request[field.name]) {
|
|
133
|
+
// Convert Struct -> JSON for the handler
|
|
134
|
+
request[field.name] = (0, struct_utils_1.structToJson)(request[field.name]);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const result = await handler.handler(request);
|
|
139
|
+
// Introspect Response for Struct fields and wrap JSON to Struct
|
|
140
|
+
if (result && typeof result === 'object' && methodDef && methodDef.responseType && methodDef.responseType.format === 'Protocol Buffer 3 DescriptorProto') {
|
|
141
|
+
for (const field of methodDef.responseType.type.field) {
|
|
142
|
+
if (field.typeName === 'google.protobuf.Struct' && result[field.name]) {
|
|
143
|
+
// Convert JSON -> Struct for the callback
|
|
144
|
+
result[field.name] = (0, struct_utils_1.jsonToStruct)(result[field.name]);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
127
148
|
callback(null, result);
|
|
128
149
|
}
|
|
129
150
|
catch (error) {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities for converting between JSON/JS Objects and google.protobuf.Struct
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Convert a plain JS object/value to google.protobuf.Value
|
|
6
|
+
*/
|
|
7
|
+
export declare function jsonToValue(val: any): any;
|
|
8
|
+
/**
|
|
9
|
+
* Convert a plain JS object to google.protobuf.Struct
|
|
10
|
+
*/
|
|
11
|
+
export declare function jsonToStruct(json: any): any;
|
|
12
|
+
/**
|
|
13
|
+
* Convert google.protobuf.Value to JS value
|
|
14
|
+
*/
|
|
15
|
+
export declare function valueToJson(val: any): any;
|
|
16
|
+
/**
|
|
17
|
+
* Convert google.protobuf.Struct to plain JS object
|
|
18
|
+
*/
|
|
19
|
+
export declare function structToJson(struct: any): any;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Utilities for converting between JSON/JS Objects and google.protobuf.Struct
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.jsonToValue = jsonToValue;
|
|
7
|
+
exports.jsonToStruct = jsonToStruct;
|
|
8
|
+
exports.valueToJson = valueToJson;
|
|
9
|
+
exports.structToJson = structToJson;
|
|
10
|
+
/**
|
|
11
|
+
* Convert a plain JS object/value to google.protobuf.Value
|
|
12
|
+
*/
|
|
13
|
+
function jsonToValue(val) {
|
|
14
|
+
if (val === null || val === undefined) {
|
|
15
|
+
return { nullValue: 0 };
|
|
16
|
+
}
|
|
17
|
+
if (typeof val === 'number') {
|
|
18
|
+
return { numberValue: val };
|
|
19
|
+
}
|
|
20
|
+
if (typeof val === 'string') {
|
|
21
|
+
return { stringValue: val };
|
|
22
|
+
}
|
|
23
|
+
if (typeof val === 'boolean') {
|
|
24
|
+
return { boolValue: val };
|
|
25
|
+
}
|
|
26
|
+
if (Array.isArray(val)) {
|
|
27
|
+
return { listValue: { values: val.map(jsonToValue) } };
|
|
28
|
+
}
|
|
29
|
+
if (typeof val === 'object') {
|
|
30
|
+
return { structValue: jsonToStruct(val) };
|
|
31
|
+
}
|
|
32
|
+
throw new Error(`Unsupported type for Struct conversion: ${typeof val}`);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Convert a plain JS object to google.protobuf.Struct
|
|
36
|
+
*/
|
|
37
|
+
function jsonToStruct(json) {
|
|
38
|
+
const fields = {};
|
|
39
|
+
for (const k in json) {
|
|
40
|
+
if (Object.prototype.hasOwnProperty.call(json, k)) {
|
|
41
|
+
fields[k] = jsonToValue(json[k]);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return { fields };
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Convert google.protobuf.Value to JS value
|
|
48
|
+
*/
|
|
49
|
+
function valueToJson(val) {
|
|
50
|
+
if (!val)
|
|
51
|
+
return null;
|
|
52
|
+
if ('nullValue' in val)
|
|
53
|
+
return null;
|
|
54
|
+
if ('numberValue' in val)
|
|
55
|
+
return val.numberValue;
|
|
56
|
+
if ('stringValue' in val)
|
|
57
|
+
return val.stringValue;
|
|
58
|
+
if ('boolValue' in val)
|
|
59
|
+
return val.boolValue;
|
|
60
|
+
if ('listValue' in val) {
|
|
61
|
+
return (val.listValue.values || []).map(valueToJson);
|
|
62
|
+
}
|
|
63
|
+
if ('structValue' in val) {
|
|
64
|
+
return structToJson(val.structValue);
|
|
65
|
+
}
|
|
66
|
+
// Fallback if 'kind' oneof logic is different in some implementations,
|
|
67
|
+
// but usually keys are explicit.
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Convert google.protobuf.Struct to plain JS object
|
|
72
|
+
*/
|
|
73
|
+
function structToJson(struct) {
|
|
74
|
+
if (!struct || !struct.fields)
|
|
75
|
+
return {};
|
|
76
|
+
const json = {};
|
|
77
|
+
for (const k in struct.fields) {
|
|
78
|
+
json[k] = valueToJson(struct.fields[k]);
|
|
79
|
+
}
|
|
80
|
+
return json;
|
|
81
|
+
}
|
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.Struct');
|
|
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
|
-
|
|
85
|
+
// Check for AnyType marker symbol
|
|
86
|
+
if (enableAnyType && (value === exports.AnyType || (typeof value === 'symbol' && value.description === 'google.protobuf.Any') || (typeof value === 'symbol' && value.description === 'google.protobuf.Struct'))) {
|
|
87
|
+
type = "google.protobuf.Struct";
|
|
88
|
+
usesAnyType = true;
|
|
89
|
+
}
|
|
90
|
+
else if (value === null || value === undefined) {
|
|
76
91
|
type = "string";
|
|
77
92
|
}
|
|
78
93
|
else if (valueType === "string") {
|
|
@@ -140,6 +155,10 @@ function generateProtoFromMethods(methods, options = {}) {
|
|
|
140
155
|
if (packageName) {
|
|
141
156
|
lines.push(`package ${packageName};`, '');
|
|
142
157
|
}
|
|
158
|
+
// Add google.protobuf.Struct import if needed
|
|
159
|
+
if (usesAnyType) {
|
|
160
|
+
lines.push('import "google/protobuf/struct.proto";', '');
|
|
161
|
+
}
|
|
143
162
|
// Service definition
|
|
144
163
|
lines.push(`// ${serviceName} - Auto-generated gRPC service`);
|
|
145
164
|
lines.push(`service ${serviceName} {`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@themainstack/communication",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
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
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as grpc from '@grpc/grpc-js';
|
|
2
2
|
import * as protoLoader from '@grpc/proto-loader';
|
|
3
3
|
import { GrpcClientOptions } from './types';
|
|
4
|
+
import { jsonToStruct, structToJson } from './struct-utils';
|
|
4
5
|
|
|
5
6
|
export class GrpcClientFactory {
|
|
6
7
|
static createClient<T extends grpc.Client>(options: GrpcClientOptions): T {
|
|
@@ -39,10 +40,82 @@ export class GrpcClientFactory {
|
|
|
39
40
|
|
|
40
41
|
const ServiceClient = serviceDef[options.serviceName];
|
|
41
42
|
|
|
42
|
-
|
|
43
|
+
const client = new ServiceClient(
|
|
43
44
|
options.url,
|
|
44
45
|
options.credentials || grpc.credentials.createInsecure(),
|
|
45
46
|
options.channelOptions
|
|
46
47
|
) as T;
|
|
48
|
+
|
|
49
|
+
// Wrap the client to intercept methods and handle Struct conversion automatically
|
|
50
|
+
const wrapper = new Proxy(client, {
|
|
51
|
+
get(target: any, prop: string | symbol) {
|
|
52
|
+
const value = target[prop];
|
|
53
|
+
|
|
54
|
+
// If accessing a method function, wrap it
|
|
55
|
+
if (typeof prop === 'string' && typeof value === 'function') {
|
|
56
|
+
// Find the method definition in the proto package definition
|
|
57
|
+
// packageDefinition uses flattened keys: 'package.Service.Method' or 'Service.Method' if in root
|
|
58
|
+
const methodKey = packageName
|
|
59
|
+
? `${packageName}.${options.serviceName}`
|
|
60
|
+
: options.serviceName;
|
|
61
|
+
|
|
62
|
+
// We need to look up the method definition to get request type
|
|
63
|
+
// The loaded packageDefinition is a flat map of FQN -> type definition
|
|
64
|
+
// But we don't have easy link from method name to request FQN here without traversing.
|
|
65
|
+
// Fortunately, ServiceClient.service (available on constructor) has the method definitions!
|
|
66
|
+
|
|
67
|
+
const serviceDefinition = ServiceClient.service;
|
|
68
|
+
const methodDef = serviceDefinition[prop];
|
|
69
|
+
|
|
70
|
+
if (methodDef && !methodDef.requestStream && !methodDef.responseStream) {
|
|
71
|
+
return (...args: any[]) => {
|
|
72
|
+
const request = args[0];
|
|
73
|
+
if (request && typeof request === 'object') {
|
|
74
|
+
// Introspect and convert Struct fields
|
|
75
|
+
const requestType = methodDef.requestType; // This contains the type definition structure
|
|
76
|
+
if (requestType && requestType.format === 'Protocol Buffer 3 DescriptorProto') {
|
|
77
|
+
// With @grpc/grpc-js and proto-loader, type info is attached
|
|
78
|
+
// traverse fields to find google.protobuf.Struct
|
|
79
|
+
for (const field of requestType.type.field) {
|
|
80
|
+
if (field.typeName === 'google.protobuf.Struct' && request[field.name]) {
|
|
81
|
+
// Auto-convert JSON to Struct
|
|
82
|
+
request[field.name] = jsonToStruct(request[field.name]);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Intercept callback to unwrap Response Structs
|
|
89
|
+
const originalCallback = typeof args[args.length - 1] === 'function'
|
|
90
|
+
? args[args.length - 1]
|
|
91
|
+
: undefined;
|
|
92
|
+
|
|
93
|
+
if (originalCallback) {
|
|
94
|
+
args[args.length - 1] = (err: any, response: any) => {
|
|
95
|
+
if (response && typeof response === 'object') {
|
|
96
|
+
const responseType = methodDef.responseType;
|
|
97
|
+
if (responseType && responseType.format === 'Protocol Buffer 3 DescriptorProto') {
|
|
98
|
+
for (const field of responseType.type.field) {
|
|
99
|
+
if (field.typeName === 'google.protobuf.Struct' && response[field.name]) {
|
|
100
|
+
// Auto-convert Struct -> JSON
|
|
101
|
+
response[field.name] = structToJson(response[field.name]);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
originalCallback(err, response);
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return value.apply(target, args);
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return value;
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
return wrapper as T;
|
|
47
120
|
}
|
|
48
121
|
}
|
|
@@ -3,6 +3,7 @@ import * as protoLoader from '@grpc/proto-loader';
|
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
import * as fs from 'fs';
|
|
5
5
|
import { generateProtoFromMethods, MethodDefinition } from '../proto-generator';
|
|
6
|
+
import { jsonToStruct, structToJson } from './struct-utils';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Options for creating a gRPC server
|
|
@@ -148,7 +149,31 @@ export class GrpcServerFactory {
|
|
|
148
149
|
callback: grpc.sendUnaryData<any>
|
|
149
150
|
) => {
|
|
150
151
|
try {
|
|
151
|
-
|
|
152
|
+
let request = call.request;
|
|
153
|
+
|
|
154
|
+
// Introspect Request for Struct fields and unwrap to JSON
|
|
155
|
+
const methodDef = ServiceConstructor.service[handler.name];
|
|
156
|
+
if (methodDef && methodDef.requestType && methodDef.requestType.format === 'Protocol Buffer 3 DescriptorProto') {
|
|
157
|
+
for (const field of methodDef.requestType.type.field) {
|
|
158
|
+
if (field.typeName === 'google.protobuf.Struct' && request[field.name]) {
|
|
159
|
+
// Convert Struct -> JSON for the handler
|
|
160
|
+
request[field.name] = structToJson(request[field.name]);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const result = await handler.handler(request);
|
|
166
|
+
|
|
167
|
+
// Introspect Response for Struct fields and wrap JSON to Struct
|
|
168
|
+
if (result && typeof result === 'object' && methodDef && methodDef.responseType && methodDef.responseType.format === 'Protocol Buffer 3 DescriptorProto') {
|
|
169
|
+
for (const field of methodDef.responseType.type.field) {
|
|
170
|
+
if (field.typeName === 'google.protobuf.Struct' && result[field.name]) {
|
|
171
|
+
// Convert JSON -> Struct for the callback
|
|
172
|
+
result[field.name] = jsonToStruct(result[field.name]);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
152
177
|
callback(null, result);
|
|
153
178
|
} catch (error: any) {
|
|
154
179
|
console.error(`gRPC Error in ${handler.name}:`, error);
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities for converting between JSON/JS Objects and google.protobuf.Struct
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Convert a plain JS object/value to google.protobuf.Value
|
|
7
|
+
*/
|
|
8
|
+
export function jsonToValue(val: any): any {
|
|
9
|
+
if (val === null || val === undefined) {
|
|
10
|
+
return { nullValue: 0 };
|
|
11
|
+
}
|
|
12
|
+
if (typeof val === 'number') {
|
|
13
|
+
return { numberValue: val };
|
|
14
|
+
}
|
|
15
|
+
if (typeof val === 'string') {
|
|
16
|
+
return { stringValue: val };
|
|
17
|
+
}
|
|
18
|
+
if (typeof val === 'boolean') {
|
|
19
|
+
return { boolValue: val };
|
|
20
|
+
}
|
|
21
|
+
if (Array.isArray(val)) {
|
|
22
|
+
return { listValue: { values: val.map(jsonToValue) } };
|
|
23
|
+
}
|
|
24
|
+
if (typeof val === 'object') {
|
|
25
|
+
return { structValue: jsonToStruct(val) };
|
|
26
|
+
}
|
|
27
|
+
throw new Error(`Unsupported type for Struct conversion: ${typeof val}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Convert a plain JS object to google.protobuf.Struct
|
|
32
|
+
*/
|
|
33
|
+
export function jsonToStruct(json: any): any {
|
|
34
|
+
const fields: Record<string, any> = {};
|
|
35
|
+
for (const k in json) {
|
|
36
|
+
if (Object.prototype.hasOwnProperty.call(json, k)) {
|
|
37
|
+
fields[k] = jsonToValue(json[k]);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return { fields };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Convert google.protobuf.Value to JS value
|
|
45
|
+
*/
|
|
46
|
+
export function valueToJson(val: any): any {
|
|
47
|
+
if (!val) return null;
|
|
48
|
+
if ('nullValue' in val) return null;
|
|
49
|
+
if ('numberValue' in val) return val.numberValue;
|
|
50
|
+
if ('stringValue' in val) return val.stringValue;
|
|
51
|
+
if ('boolValue' in val) return val.boolValue;
|
|
52
|
+
if ('listValue' in val) {
|
|
53
|
+
return (val.listValue.values || []).map(valueToJson);
|
|
54
|
+
}
|
|
55
|
+
if ('structValue' in val) {
|
|
56
|
+
return structToJson(val.structValue);
|
|
57
|
+
}
|
|
58
|
+
// Fallback if 'kind' oneof logic is different in some implementations,
|
|
59
|
+
// but usually keys are explicit.
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Convert google.protobuf.Struct to plain JS object
|
|
65
|
+
*/
|
|
66
|
+
export function structToJson(struct: any): any {
|
|
67
|
+
if (!struct || !struct.fields) return {};
|
|
68
|
+
const json: Record<string, any> = {};
|
|
69
|
+
for (const k in struct.fields) {
|
|
70
|
+
json[k] = valueToJson(struct.fields[k]);
|
|
71
|
+
}
|
|
72
|
+
return json;
|
|
73
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -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.Struct');
|
|
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.Struct" | 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
|
-
|
|
111
|
+
// Check for AnyType marker symbol
|
|
112
|
+
if (enableAnyType && (value === AnyType || (typeof value === 'symbol' && value.description === 'google.protobuf.Any') || (typeof value === 'symbol' && value.description === 'google.protobuf.Struct'))) {
|
|
113
|
+
type = "google.protobuf.Struct";
|
|
114
|
+
usesAnyType = true;
|
|
115
|
+
} else if (value === null || value === undefined) {
|
|
100
116
|
type = "string";
|
|
101
117
|
} else if (valueType === "string") {
|
|
102
118
|
type = "string";
|
|
@@ -167,6 +183,11 @@ export function generateProtoFromMethods(
|
|
|
167
183
|
lines.push(`package ${packageName};`, '');
|
|
168
184
|
}
|
|
169
185
|
|
|
186
|
+
// Add google.protobuf.Struct import if needed
|
|
187
|
+
if (usesAnyType) {
|
|
188
|
+
lines.push('import "google/protobuf/struct.proto";', '');
|
|
189
|
+
}
|
|
190
|
+
|
|
170
191
|
// Service definition
|
|
171
192
|
lines.push(`// ${serviceName} - Auto-generated gRPC service`);
|
|
172
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.Struct support', () => {
|
|
5
|
+
it('should export AnyType symbol aliased to Struct', () => {
|
|
6
|
+
expect(AnyType).toBeDefined();
|
|
7
|
+
expect(typeof AnyType).toBe('symbol');
|
|
8
|
+
expect(AnyType.description).toBe('google.protobuf.Struct');
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('should generate proto with google.protobuf.Struct 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/struct.proto";');
|
|
31
|
+
|
|
32
|
+
// Should use google.protobuf.Struct type
|
|
33
|
+
expect(proto).toContain('google.protobuf.Struct data = 2;');
|
|
34
|
+
expect(proto).toContain('google.protobuf.Struct result = 2;');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should not include import if no Struct 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/struct.proto";');
|
|
50
|
+
expect(proto).not.toContain('google.protobuf.Struct');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should handle mixed fields with some Struct 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/struct.proto";');
|
|
74
|
+
expect(proto).toContain('string id = 1;');
|
|
75
|
+
expect(proto).toContain('string name = 2;');
|
|
76
|
+
expect(proto).toContain('google.protobuf.Struct 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/struct.proto";');
|
|
98
|
+
expect(proto).not.toContain('google.protobuf.Struct');
|
|
99
|
+
});
|
|
100
|
+
});
|
package/publish-config.npmrc
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
//registry.npmjs.org/:_authToken=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
|