@themainstack/communication 1.1.1 → 1.1.2

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.
@@ -11,10 +11,22 @@ exports.structToJson = structToJson;
11
11
  * Convert a plain JS object/value to google.protobuf.Value
12
12
  */
13
13
  function jsonToValue(val) {
14
- if (val === null || val === undefined) {
14
+ // 1. Handle toJSON (covers Date and custom objects)
15
+ // Note: JSON.stringify calls toJSON() first.
16
+ if (val && typeof val.toJSON === 'function') {
17
+ val = val.toJSON();
18
+ }
19
+ // 2. Handle undefined/null
20
+ // undefined in Array becomes null. undefined passed directly becomes nullValue.
21
+ if (val === undefined || val === null) {
15
22
  return { nullValue: 0 };
16
23
  }
24
+ // 3. Primitives
17
25
  if (typeof val === 'number') {
26
+ // Handle infinity/NaN? Protobuf uses strings/special doubles, but standard jsonToValue usually
27
+ // expects finite numbers. JSON.stringify converts Infinity/NaN to null.
28
+ if (!Number.isFinite(val))
29
+ return { nullValue: 0 };
18
30
  return { numberValue: val };
19
31
  }
20
32
  if (typeof val === 'string') {
@@ -23,13 +35,17 @@ function jsonToValue(val) {
23
35
  if (typeof val === 'boolean') {
24
36
  return { boolValue: val };
25
37
  }
38
+ // 4. Arrays
26
39
  if (Array.isArray(val)) {
27
40
  return { listValue: { values: val.map(jsonToValue) } };
28
41
  }
42
+ // 5. Objects (Struct)
29
43
  if (typeof val === 'object') {
30
44
  return { structValue: jsonToStruct(val) };
31
45
  }
32
- throw new Error(`Unsupported type for Struct conversion: ${typeof val}`);
46
+ // Functions/Symbols in array -> null (JSON behavior)
47
+ // But usually we throw or ignore. JSON.stringify([function(){}]) -> [null]
48
+ return { nullValue: 0 };
33
49
  }
34
50
  /**
35
51
  * Convert a plain JS object to google.protobuf.Struct
@@ -37,8 +53,19 @@ function jsonToValue(val) {
37
53
  function jsonToStruct(json) {
38
54
  const fields = {};
39
55
  for (const k in json) {
56
+ // Only process own enumerable properties (standard JSON behavior usually)
40
57
  if (Object.prototype.hasOwnProperty.call(json, k)) {
41
- fields[k] = jsonToValue(json[k]);
58
+ let v = json[k];
59
+ // 1. Resolve toJSON
60
+ if (v && typeof v.toJSON === 'function') {
61
+ v = v.toJSON();
62
+ }
63
+ // 2. Skip properties that are undefined, functions, or symbols (standard JSON behavior)
64
+ if (v === undefined || typeof v === 'function' || typeof v === 'symbol') {
65
+ continue;
66
+ }
67
+ // 3. Convert remainder
68
+ fields[k] = jsonToValue(v);
42
69
  }
43
70
  }
44
71
  return { fields };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@themainstack/communication",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
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",
@@ -6,10 +6,23 @@
6
6
  * Convert a plain JS object/value to google.protobuf.Value
7
7
  */
8
8
  export function jsonToValue(val: any): any {
9
- if (val === null || val === undefined) {
9
+ // 1. Handle toJSON (covers Date and custom objects)
10
+ // Note: JSON.stringify calls toJSON() first.
11
+ if (val && typeof val.toJSON === 'function') {
12
+ val = val.toJSON();
13
+ }
14
+
15
+ // 2. Handle undefined/null
16
+ // undefined in Array becomes null. undefined passed directly becomes nullValue.
17
+ if (val === undefined || val === null) {
10
18
  return { nullValue: 0 };
11
19
  }
20
+
21
+ // 3. Primitives
12
22
  if (typeof val === 'number') {
23
+ // Handle infinity/NaN? Protobuf uses strings/special doubles, but standard jsonToValue usually
24
+ // expects finite numbers. JSON.stringify converts Infinity/NaN to null.
25
+ if (!Number.isFinite(val)) return { nullValue: 0 };
13
26
  return { numberValue: val };
14
27
  }
15
28
  if (typeof val === 'string') {
@@ -18,13 +31,20 @@ export function jsonToValue(val: any): any {
18
31
  if (typeof val === 'boolean') {
19
32
  return { boolValue: val };
20
33
  }
34
+
35
+ // 4. Arrays
21
36
  if (Array.isArray(val)) {
22
37
  return { listValue: { values: val.map(jsonToValue) } };
23
38
  }
39
+
40
+ // 5. Objects (Struct)
24
41
  if (typeof val === 'object') {
25
42
  return { structValue: jsonToStruct(val) };
26
43
  }
27
- throw new Error(`Unsupported type for Struct conversion: ${typeof val}`);
44
+
45
+ // Functions/Symbols in array -> null (JSON behavior)
46
+ // But usually we throw or ignore. JSON.stringify([function(){}]) -> [null]
47
+ return { nullValue: 0 };
28
48
  }
29
49
 
30
50
  /**
@@ -33,8 +53,22 @@ export function jsonToValue(val: any): any {
33
53
  export function jsonToStruct(json: any): any {
34
54
  const fields: Record<string, any> = {};
35
55
  for (const k in json) {
56
+ // Only process own enumerable properties (standard JSON behavior usually)
36
57
  if (Object.prototype.hasOwnProperty.call(json, k)) {
37
- fields[k] = jsonToValue(json[k]);
58
+ let v = json[k];
59
+
60
+ // 1. Resolve toJSON
61
+ if (v && typeof v.toJSON === 'function') {
62
+ v = v.toJSON();
63
+ }
64
+
65
+ // 2. Skip properties that are undefined, functions, or symbols (standard JSON behavior)
66
+ if (v === undefined || typeof v === 'function' || typeof v === 'symbol') {
67
+ continue;
68
+ }
69
+
70
+ // 3. Convert remainder
71
+ fields[k] = jsonToValue(v);
38
72
  }
39
73
  }
40
74
  return { fields };
@@ -0,0 +1,18 @@
1
+ syntax = "proto3";
2
+
3
+ package regression.v1;
4
+
5
+ import "google/protobuf/struct.proto";
6
+
7
+ // RegressionService - Auto-generated gRPC service
8
+ service RegressionService {
9
+ rpc Echo (EchoRequest) returns (EchoResponse) {}
10
+ }
11
+
12
+ message EchoResponse {
13
+ google.protobuf.Struct result = 1;
14
+ }
15
+
16
+ message EchoRequest {
17
+ google.protobuf.Struct data = 1;
18
+ }
@@ -0,0 +1,47 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { jsonToStruct, structToJson } from '../src/grpc/struct-utils';
3
+
4
+ describe('struct-utils edge cases', () => {
5
+ it('should handle Dates by converting to ISO string (like JSON.stringify)', () => {
6
+ const date = new Date('2023-01-01T00:00:00.000Z');
7
+ const input = { createdAt: date };
8
+
9
+ // JSON.stringify behavior:
10
+ // {"createdAt":"2023-01-01T00:00:00.000Z"}
11
+
12
+ const struct = jsonToStruct(input);
13
+ const output = structToJson(struct);
14
+
15
+ expect(output.createdAt).toBe(date.toISOString());
16
+ });
17
+
18
+ it('should ignore undefined fields (like JSON.stringify)', () => {
19
+ const input = { defined: true, missing: undefined };
20
+
21
+ // JSON.stringify behavior:
22
+ // {"defined":true}
23
+
24
+ const struct = jsonToStruct(input);
25
+ const output = structToJson(struct);
26
+
27
+ expect(output).toHaveProperty('defined');
28
+ expect(output).not.toHaveProperty('missing');
29
+ });
30
+
31
+ it('should support toJSON() method', () => {
32
+ const input = {
33
+ custom: {
34
+ val: 123,
35
+ toJSON() { return { val: 456 }; }
36
+ }
37
+ };
38
+
39
+ // JSON.stringify behavior:
40
+ // {"custom":{"val":456}}
41
+
42
+ const struct = jsonToStruct(input);
43
+ const output = structToJson(struct);
44
+
45
+ expect(output.custom.val).toBe(456);
46
+ });
47
+ });
@@ -0,0 +1,72 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { GrpcServerFactory, GrpcClientFactory, AnyType } from '../src/index';
3
+ import * as path from 'path';
4
+ import * as fs from 'fs';
5
+
6
+ describe('Struct Regression Integration', () => {
7
+ it('should transparently handle plain JSON including Dates across gRPC', async () => {
8
+ const PORT = 50058;
9
+ const PACKAGE_NAME = 'regression.v1';
10
+ const SERVICE_NAME = 'RegressionService';
11
+
12
+ // Define service using AnyType (which maps to Struct)
13
+ const handlers = [{
14
+ name: 'Echo',
15
+ handler: async (req: any) => {
16
+ // Return exactly what we received to verify server-side unwrapping
17
+ return { result: req.data };
18
+ },
19
+ requestSample: () => ({ data: AnyType }),
20
+ responseSample: () => ({ result: AnyType }),
21
+ }];
22
+
23
+ const mkDir = path.join(__dirname, 'fixtures');
24
+ if (!fs.existsSync(mkDir)) fs.mkdirSync(mkDir, { recursive: true });
25
+
26
+ // Start Server
27
+ const server = await GrpcServerFactory.createServer({
28
+ packageName: PACKAGE_NAME,
29
+ serviceName: SERVICE_NAME,
30
+ port: PORT,
31
+ protoOutputDir: mkDir,
32
+ }, handlers);
33
+ await server.start();
34
+
35
+ // Create Client
36
+ const client = GrpcClientFactory.createClient<any>({
37
+ packageName: PACKAGE_NAME,
38
+ serviceName: SERVICE_NAME,
39
+ protoPath: path.join(mkDir, 'regression.proto'),
40
+ url: `0.0.0.0:${PORT}`,
41
+ });
42
+
43
+ // Test Payload
44
+ const date = new Date('2024-01-01T12:00:00.000Z');
45
+ const payload = {
46
+ message: 'hello',
47
+ count: 42,
48
+ createdAt: date,
49
+ missing: undefined
50
+ };
51
+
52
+ try {
53
+ const response = await new Promise((resolve, reject) => {
54
+ client.Echo({ data: payload }, (err: any, res: any) => {
55
+ if (err) reject(err);
56
+ else resolve(res);
57
+ });
58
+ });
59
+
60
+ // Verify Client -> Server (Server unwrapped correctly) -> Client (Client unwrapped correctly)
61
+ const result = (response as any).result;
62
+
63
+ expect(result.message).toBe('hello');
64
+ expect(result.count).toBe(42);
65
+ expect(result.createdAt).toBe(date.toISOString()); // Date became string
66
+ expect(result).not.toHaveProperty('missing'); // undefined removed
67
+
68
+ } finally {
69
+ server.stop();
70
+ }
71
+ });
72
+ });