@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 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
- return new ServiceClient(options.url, options.credentials || grpc.credentials.createInsecure(), options.channelOptions);
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
- const result = await handler.handler(call.request);
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
- if (value === null || value === undefined) {
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.0.2",
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
- return new ServiceClient(
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
- const result = await handler.handler(call.request);
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
@@ -14,7 +14,8 @@
14
14
  // Auto-generate .proto files from TypeScript types
15
15
  export {
16
16
  generateProtoFromMethods,
17
- generateProtoFromFunction
17
+ generateProtoFromFunction,
18
+ AnyType
18
19
  } from './proto-generator';
19
20
  export type {
20
21
  MethodDefinition,
@@ -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
- if (value === null || value === undefined) {
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
+ });
@@ -1 +0,0 @@
1
- //registry.npmjs.org/:_authToken=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