@themainstack/communication 1.1.0 → 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/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/proto-generator/index.js +5 -5
- package/package.json +1 -1
- 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/proto-generator/index.ts +6 -6
- package/tests/{any-type.spec.ts → struct-support.spec.ts} +16 -16
|
@@ -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
|
+
}
|
|
@@ -45,7 +45,7 @@ const path = __importStar(require("path"));
|
|
|
45
45
|
* @example
|
|
46
46
|
* requestSample: () => ({ id: '', data: AnyType })
|
|
47
47
|
*/
|
|
48
|
-
exports.AnyType = Symbol('google.protobuf.
|
|
48
|
+
exports.AnyType = Symbol('google.protobuf.Struct');
|
|
49
49
|
/**
|
|
50
50
|
* Generate a complete .proto file from method definitions
|
|
51
51
|
*
|
|
@@ -83,8 +83,8 @@ function generateProtoFromMethods(methods, options = {}) {
|
|
|
83
83
|
let rule = undefined;
|
|
84
84
|
const valueType = typeof value;
|
|
85
85
|
// Check for AnyType marker symbol
|
|
86
|
-
if (enableAnyType && (value === exports.AnyType || (typeof value === 'symbol' && value.description === 'google.protobuf.Any'))) {
|
|
87
|
-
type = "google.protobuf.
|
|
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
88
|
usesAnyType = true;
|
|
89
89
|
}
|
|
90
90
|
else if (value === null || value === undefined) {
|
|
@@ -155,9 +155,9 @@ function generateProtoFromMethods(methods, options = {}) {
|
|
|
155
155
|
if (packageName) {
|
|
156
156
|
lines.push(`package ${packageName};`, '');
|
|
157
157
|
}
|
|
158
|
-
// Add google.protobuf.
|
|
158
|
+
// Add google.protobuf.Struct import if needed
|
|
159
159
|
if (usesAnyType) {
|
|
160
|
-
lines.push('import "google/protobuf/
|
|
160
|
+
lines.push('import "google/protobuf/struct.proto";', '');
|
|
161
161
|
}
|
|
162
162
|
// Service definition
|
|
163
163
|
lines.push(`// ${serviceName} - Auto-generated gRPC service`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@themainstack/communication",
|
|
3
|
-
"version": "1.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",
|
|
@@ -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
|
+
}
|
|
@@ -8,12 +8,12 @@ import * as path from 'path';
|
|
|
8
8
|
* @example
|
|
9
9
|
* requestSample: () => ({ id: '', data: AnyType })
|
|
10
10
|
*/
|
|
11
|
-
export const AnyType = Symbol('google.protobuf.
|
|
11
|
+
export const AnyType = Symbol('google.protobuf.Struct');
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Maps JavaScript/TypeScript runtime types to Protobuf types.
|
|
15
15
|
*/
|
|
16
|
-
type ProtoType = "double" | "float" | "int32" | "int64" | "bool" | "string" | "bytes" | "google.protobuf.
|
|
16
|
+
type ProtoType = "double" | "float" | "int32" | "int64" | "bool" | "string" | "bytes" | "google.protobuf.Struct" | string;
|
|
17
17
|
|
|
18
18
|
interface ProtoField {
|
|
19
19
|
name: string;
|
|
@@ -109,8 +109,8 @@ export function generateProtoFromMethods(
|
|
|
109
109
|
const valueType = typeof value;
|
|
110
110
|
|
|
111
111
|
// Check for AnyType marker symbol
|
|
112
|
-
if (enableAnyType && (value === AnyType || (typeof value === 'symbol' && value.description === 'google.protobuf.Any'))) {
|
|
113
|
-
type = "google.protobuf.
|
|
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
114
|
usesAnyType = true;
|
|
115
115
|
} else if (value === null || value === undefined) {
|
|
116
116
|
type = "string";
|
|
@@ -183,9 +183,9 @@ export function generateProtoFromMethods(
|
|
|
183
183
|
lines.push(`package ${packageName};`, '');
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
-
// Add google.protobuf.
|
|
186
|
+
// Add google.protobuf.Struct import if needed
|
|
187
187
|
if (usesAnyType) {
|
|
188
|
-
lines.push('import "google/protobuf/
|
|
188
|
+
lines.push('import "google/protobuf/struct.proto";', '');
|
|
189
189
|
}
|
|
190
190
|
|
|
191
191
|
// Service definition
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import { generateProtoFromMethods, AnyType } from '../src/index';
|
|
3
3
|
|
|
4
|
-
describe('google.protobuf.
|
|
5
|
-
it('should export AnyType symbol', () => {
|
|
4
|
+
describe('google.protobuf.Struct support', () => {
|
|
5
|
+
it('should export AnyType symbol aliased to Struct', () => {
|
|
6
6
|
expect(AnyType).toBeDefined();
|
|
7
7
|
expect(typeof AnyType).toBe('symbol');
|
|
8
|
-
expect(AnyType.description).toBe('google.protobuf.
|
|
8
|
+
expect(AnyType.description).toBe('google.protobuf.Struct');
|
|
9
9
|
});
|
|
10
10
|
|
|
11
|
-
it('should generate proto with google.protobuf.
|
|
11
|
+
it('should generate proto with google.protobuf.Struct for marked fields', () => {
|
|
12
12
|
const proto = generateProtoFromMethods([
|
|
13
13
|
{
|
|
14
14
|
name: 'ProcessGenericData',
|
|
@@ -27,14 +27,14 @@ describe('google.protobuf.Any support', () => {
|
|
|
27
27
|
});
|
|
28
28
|
|
|
29
29
|
// Should include the import statement
|
|
30
|
-
expect(proto).toContain('import "google/protobuf/
|
|
30
|
+
expect(proto).toContain('import "google/protobuf/struct.proto";');
|
|
31
31
|
|
|
32
|
-
// Should use google.protobuf.
|
|
33
|
-
expect(proto).toContain('google.protobuf.
|
|
34
|
-
expect(proto).toContain('google.protobuf.
|
|
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
35
|
});
|
|
36
36
|
|
|
37
|
-
it('should not include import if no
|
|
37
|
+
it('should not include import if no Struct types are used', () => {
|
|
38
38
|
const proto = generateProtoFromMethods([
|
|
39
39
|
{
|
|
40
40
|
name: 'SimpleMethod',
|
|
@@ -46,11 +46,11 @@ describe('google.protobuf.Any support', () => {
|
|
|
46
46
|
serviceName: 'SimpleService',
|
|
47
47
|
});
|
|
48
48
|
|
|
49
|
-
expect(proto).not.toContain('import "google/protobuf/
|
|
50
|
-
expect(proto).not.toContain('google.protobuf.
|
|
49
|
+
expect(proto).not.toContain('import "google/protobuf/struct.proto";');
|
|
50
|
+
expect(proto).not.toContain('google.protobuf.Struct');
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
-
it('should handle mixed fields with some
|
|
53
|
+
it('should handle mixed fields with some Struct types', () => {
|
|
54
54
|
const proto = generateProtoFromMethods([
|
|
55
55
|
{
|
|
56
56
|
name: 'MixedMethod',
|
|
@@ -70,10 +70,10 @@ describe('google.protobuf.Any support', () => {
|
|
|
70
70
|
serviceName: 'MixedService',
|
|
71
71
|
});
|
|
72
72
|
|
|
73
|
-
expect(proto).toContain('import "google/protobuf/
|
|
73
|
+
expect(proto).toContain('import "google/protobuf/struct.proto";');
|
|
74
74
|
expect(proto).toContain('string id = 1;');
|
|
75
75
|
expect(proto).toContain('string name = 2;');
|
|
76
|
-
expect(proto).toContain('google.protobuf.
|
|
76
|
+
expect(proto).toContain('google.protobuf.Struct metadata = 3;');
|
|
77
77
|
expect(proto).toContain('int32 count = 4;');
|
|
78
78
|
});
|
|
79
79
|
|
|
@@ -94,7 +94,7 @@ describe('google.protobuf.Any support', () => {
|
|
|
94
94
|
});
|
|
95
95
|
|
|
96
96
|
// When disabled, AnyType should be treated as a regular value (string fallback)
|
|
97
|
-
expect(proto).not.toContain('import "google/protobuf/
|
|
98
|
-
expect(proto).not.toContain('google.protobuf.
|
|
97
|
+
expect(proto).not.toContain('import "google/protobuf/struct.proto";');
|
|
98
|
+
expect(proto).not.toContain('google.protobuf.Struct');
|
|
99
99
|
});
|
|
100
100
|
});
|