@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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|
package/src/grpc/struct-utils.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
});
|