@webda/grpc 4.0.0-beta.6 → 4.0.0-beta.7

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.
@@ -10,10 +10,10 @@ import { EventEmitter } from "node:events";
10
10
  const mockOperations: Record<string, any> = {};
11
11
  const mockSchemas: Record<string, any> = {};
12
12
  const coreEventCallbacks: Record<string, Function> = {};
13
- let mockCallOperationResult: any = {};
14
- let mockCallOperationError: any = null;
13
+ const mockCallOperationResult: any = {};
14
+ const mockCallOperationError: any = null;
15
15
  /** If set, callOperation will use this as the raw _output string (instead of JSON.stringify(mockCallOperationResult)) */
16
- let mockCallOperationRawOutput: string | undefined = undefined;
16
+ const mockCallOperationRawOutput: string | undefined = undefined;
17
17
  /** Services returned by the mocked useCore().getServices() — set per test to inject HttpServer stubs */
18
18
  let mockCoreServices: Record<string, any> = {};
19
19
  /** Models returned by the mocked useApplication().getModels() — used by build() */
@@ -83,6 +83,10 @@ vi.mock("@webda/core", () => {
83
83
  }
84
84
  },
85
85
  OperationContext: class {},
86
+ AsyncQueue: class {},
87
+ HttpContext: class {},
88
+ WebdaError: { OperationCancelledError: class extends Error {} },
89
+ getOperationStreaming: (op: any) => op?.streaming ?? op?.grpc?.streaming ?? "none",
86
90
  SimpleOperationContext: class {
87
91
  _input: Buffer;
88
92
  _output: string;
@@ -100,6 +104,8 @@ vi.mock("@webda/core", () => {
100
104
  }
101
105
  },
102
106
  WebContext: class {},
107
+ StreamingOperationContext: class {},
108
+ toPublicChunk: (value: unknown) => (value === undefined ? undefined : JSON.parse(JSON.stringify(value))),
103
109
  useCoreEvents: (eventName: string, callback: Function) => {
104
110
  coreEventCallbacks[eventName] = callback;
105
111
  return () => {
@@ -113,6 +119,9 @@ vi.mock("@webda/core", () => {
113
119
  useCore: () => ({ getServices: () => mockCoreServices }),
114
120
  useInstanceStorage: () => ({}),
115
121
  runWithInstanceStorage: (_storage: any, fn: () => any) => fn(),
122
+ runWithContext: (_ctx: any, fn: () => any) => fn(),
123
+ emitCoreEvent: () => {},
124
+ useRouter: () => ({ checkRequest: async () => true }),
116
125
  Command: () => () => {},
117
126
  BuildCommand: () => () => {}
118
127
  };
@@ -357,7 +366,10 @@ class GrpcServiceGenerateProtoTest {
357
366
  mockOperations["Test.Ping"] = {};
358
367
 
359
368
  const outPath = join(this.tmpDir, "test.proto");
360
- const service = new GrpcService("testGrpc", new GrpcServiceParameters().load({ protoFile: outPath, packageName: "myapp" }));
369
+ const service = new GrpcService(
370
+ "testGrpc",
371
+ new GrpcServiceParameters().load({ protoFile: outPath, packageName: "myapp" })
372
+ );
361
373
 
362
374
  await service.build();
363
375
 
@@ -410,128 +422,8 @@ class GrpcServiceGenerateProtoTest {
410
422
  }
411
423
  }
412
424
 
413
- /**
414
- * Create a gRPC frame from a JSON message
415
- */
416
- function createGrpcFrame(data: any): Buffer {
417
- const payload = Buffer.from(JSON.stringify(data));
418
- const frame = Buffer.alloc(5 + payload.length);
419
- frame.writeUInt8(0, 0);
420
- frame.writeUInt32BE(payload.length, 1);
421
- payload.copy(frame, 5);
422
- return frame;
423
- }
424
-
425
- /**
426
- * Create a mock request EventEmitter that simulates an HTTP request with gRPC data
427
- */
428
- function createMockReq(url: string, frameData?: any): EventEmitter & { url: string; headers: Record<string, string> } {
429
- const req = new EventEmitter() as any;
430
- req.url = url;
431
- req.headers = { "content-type": "application/grpc" };
432
-
433
- if (frameData !== undefined) {
434
- // Schedule data emission on next tick so handlers can be registered
435
- process.nextTick(() => {
436
- const frame = createGrpcFrame(frameData);
437
- req.emit("data", frame);
438
- // For client/bidi streaming, also emit end
439
- req.emit("end");
440
- });
441
- }
442
-
443
- return req;
444
- }
445
-
446
- /**
447
- * Create a mock response that captures what was written
448
- */
449
- function createMockRes(): {
450
- res: any;
451
- written: Buffer[];
452
- trailers: Record<string, string> | null;
453
- ended: boolean;
454
- headStatus: number;
455
- headHeaders: Record<string, string>;
456
- headers: Record<string, string>;
457
- } {
458
- const state = {
459
- written: [] as Buffer[],
460
- trailers: null as Record<string, string> | null,
461
- ended: false,
462
- headStatus: 0,
463
- headHeaders: {} as Record<string, string>,
464
- headers: {} as Record<string, string>,
465
- res: null as any
466
- };
467
-
468
- state.res = {
469
- setHeader(key: string, value: string) {
470
- state.headers[key] = value;
471
- },
472
- writeHead(status: number, headers: any) {
473
- state.headStatus = status;
474
- state.headHeaders = headers || {};
475
- },
476
- write(data: Buffer) {
477
- state.written.push(Buffer.from(data));
478
- },
479
- addTrailers(t: Record<string, string>) {
480
- state.trailers = t;
481
- },
482
- end() {
483
- state.ended = true;
484
- }
485
- };
486
-
487
- return state;
488
- }
489
-
490
- /**
491
- * Create a JSON-based method definition mock
492
- */
493
- function createJsonMethodDef() {
494
- return {
495
- path: "/webda.TestService/TestMethod",
496
- requestSerialize: (msg: any) => Buffer.from(JSON.stringify(msg)),
497
- requestDeserialize: (buf: Buffer) => JSON.parse(buf.toString()),
498
- responseSerialize: (msg: any) => Buffer.from(JSON.stringify(msg)),
499
- responseDeserialize: (buf: Buffer) => JSON.parse(buf.toString()),
500
- requestStream: false,
501
- responseStream: false
502
- };
503
- }
504
-
505
- /**
506
- * Set up a service with a mapped rpc method
507
- */
508
- function setupServiceWithRpc(opId: string, opDef: any = {}): InstanceType<typeof GrpcService> {
509
- const service = new GrpcService("testGrpc", new GrpcServiceParameters().load({}));
510
- const methodDef = createJsonMethodDef();
511
- const grpcPath = `/webda.TestService/${opId.split(".").pop()}`;
512
-
513
- // Manually set up the rpcToOperation and rpcMethods maps
514
- (service as any).rpcToOperation.set(grpcPath, opId);
515
- (service as any).rpcMethods.set(grpcPath, methodDef);
516
-
517
- // Set up the mock operation
518
- mockOperations[opId] = opDef;
519
-
520
- return service;
521
- }
522
-
523
425
  @suite
524
- class GrpcServiceHandleGrpcRequestTest {
525
- beforeEach() {
526
- // Clear operations
527
- for (const key of Object.keys(mockOperations)) {
528
- delete mockOperations[key];
529
- }
530
- mockCallOperationResult = {};
531
- mockCallOperationError = null;
532
- mockCallOperationRawOutput = undefined;
533
- }
534
-
426
+ class GrpcServiceUnknownMethodTest {
535
427
  @test
536
428
  async unimplementedMethodReturns12() {
537
429
  const service = new GrpcService("testGrpc", new GrpcServiceParameters().load({}));
@@ -555,320 +447,6 @@ class GrpcServiceHandleGrpcRequestTest {
555
447
  assert.ok(decodedMessage.includes("Method not found"), `Expected 'Method not found' in: ${decodedMessage}`);
556
448
  assert.ok(ended);
557
449
  }
558
-
559
- @test
560
- async unaryRequestReturnsResponse() {
561
- const service = setupServiceWithRpc("Test.DoSomething", {});
562
- mockCallOperationResult = { success: true, id: "123" };
563
-
564
- const grpcPath = "/webda.TestService/DoSomething";
565
- const req = createMockReq(grpcPath, { input: "hello" });
566
- const mock = createMockRes();
567
-
568
- await service.handleGrpcRequest(req as any, mock.res);
569
-
570
- // Should have written a response frame
571
- assert.strictEqual(mock.written.length, 1);
572
- const payload = mock.written[0].subarray(5);
573
- const response = JSON.parse(payload.toString());
574
- assert.deepStrictEqual(response, { success: true, id: "123" });
575
-
576
- // Should have ended with status 0
577
- assert.ok(mock.trailers !== null);
578
- assert.strictEqual(mock.trailers!["grpc-status"], "0");
579
- assert.ok(mock.ended);
580
- }
581
-
582
- @test
583
- async unaryWrapsScalarOutputInValueField() {
584
- // When the operation returns a JSON-parsed scalar (not an object), wrap in {value}
585
- const service = setupServiceWithRpc("Test.GetVersion", {});
586
- mockCallOperationRawOutput = '"1.2.3"';
587
-
588
- const grpcPath = "/webda.TestService/GetVersion";
589
- const req = createMockReq(grpcPath, {});
590
- const mock = createMockRes();
591
- await service.handleGrpcRequest(req as any, mock.res);
592
- assert.strictEqual(mock.written.length, 1);
593
- const response = JSON.parse(mock.written[0].subarray(5).toString());
594
- assert.deepStrictEqual(response, { value: "1.2.3" });
595
- }
596
-
597
- @test
598
- async unaryWrapsUnparseableOutputInValueField() {
599
- // When the operation output isn't valid JSON, fall through to {value: rawString}
600
- const service = setupServiceWithRpc("Test.Raw", {});
601
- mockCallOperationRawOutput = "not-json-at-all";
602
-
603
- const grpcPath = "/webda.TestService/Raw";
604
- const req = createMockReq(grpcPath, {});
605
- const mock = createMockRes();
606
- await service.handleGrpcRequest(req as any, mock.res);
607
- assert.strictEqual(mock.written.length, 1);
608
- const response = JSON.parse(mock.written[0].subarray(5).toString());
609
- assert.deepStrictEqual(response, { value: "not-json-at-all" });
610
- }
611
-
612
- @test
613
- async unaryRequestHandlesOperationError() {
614
- const service = setupServiceWithRpc("Test.FailOp", {});
615
- mockCallOperationError = {
616
- getResponseCode: () => 404,
617
- message: "Resource not found"
618
- };
619
-
620
- const grpcPath = "/webda.TestService/FailOp";
621
- const req = createMockReq(grpcPath, { id: "missing" });
622
- const mock = createMockRes();
623
-
624
- await service.handleGrpcRequest(req as any, mock.res);
625
-
626
- // Should have ended with error status
627
- assert.ok(mock.trailers !== null);
628
- assert.strictEqual(mock.trailers!["grpc-status"], String(GrpcStatus.NOT_FOUND));
629
- assert.ok(mock.ended);
630
- }
631
-
632
- @test
633
- async serverStreamingReturnsMultipleMessages() {
634
- const service = setupServiceWithRpc("Test.StreamItems", {
635
- grpc: { streaming: "server" }
636
- });
637
- mockCallOperationRawOutput = '{"id":1}\n{"id":2}\n{"id":3}';
638
-
639
- const grpcPath = "/webda.TestService/StreamItems";
640
- const req = createMockReq(grpcPath, { query: "all" });
641
- const mock = createMockRes();
642
-
643
- await service.handleGrpcRequest(req as any, mock.res);
644
-
645
- // Should have written 3 response frames (one per NDJSON line)
646
- assert.strictEqual(mock.written.length, 3, `Expected 3 frames, got ${mock.written.length}`);
647
-
648
- const msg1 = JSON.parse(mock.written[0].subarray(5).toString());
649
- const msg2 = JSON.parse(mock.written[1].subarray(5).toString());
650
- const msg3 = JSON.parse(mock.written[2].subarray(5).toString());
651
- assert.deepStrictEqual(msg1, { id: 1 });
652
- assert.deepStrictEqual(msg2, { id: 2 });
653
- assert.deepStrictEqual(msg3, { id: 3 });
654
-
655
- // Should end with OK
656
- assert.strictEqual(mock.trailers!["grpc-status"], "0");
657
- }
658
-
659
- @test
660
- async serverStreamingHandlesNonJsonLines() {
661
- const service = setupServiceWithRpc("Test.StreamRaw", {
662
- grpc: { streaming: "server" }
663
- });
664
- mockCallOperationRawOutput = 'not-json-line\n{"valid":true}';
665
-
666
- const grpcPath = "/webda.TestService/StreamRaw";
667
- const req = createMockReq(grpcPath, {});
668
- const mock = createMockRes();
669
-
670
- await service.handleGrpcRequest(req as any, mock.res);
671
-
672
- // First line is not valid JSON, should be wrapped in { data: ... }
673
- assert.strictEqual(mock.written.length, 2);
674
- const msg1 = JSON.parse(mock.written[0].subarray(5).toString());
675
- assert.deepStrictEqual(msg1, { data: "not-json-line" });
676
- const msg2 = JSON.parse(mock.written[1].subarray(5).toString());
677
- assert.deepStrictEqual(msg2, { valid: true });
678
- }
679
-
680
- @test
681
- async serverStreamingHandlesNoOutput() {
682
- const service = setupServiceWithRpc("Test.StreamEmpty", {
683
- grpc: { streaming: "server" }
684
- });
685
- // Set rawOutput to empty string to simulate no output
686
- mockCallOperationRawOutput = "";
687
-
688
- const grpcPath = "/webda.TestService/StreamEmpty";
689
- const req = createMockReq(grpcPath, {});
690
- const mock = createMockRes();
691
-
692
- await service.handleGrpcRequest(req as any, mock.res);
693
-
694
- // No data frames should be written (empty string filtered by split+filter)
695
- assert.strictEqual(mock.written.length, 0);
696
- assert.strictEqual(mock.trailers!["grpc-status"], "0");
697
- }
698
-
699
- @test
700
- async serverStreamingHandlesOperationError() {
701
- const service = setupServiceWithRpc("Test.StreamFail", {
702
- grpc: { streaming: "server" }
703
- });
704
- const err: any = new Error("Stream failed");
705
- err.getResponseCode = () => 400;
706
- mockCallOperationError = err;
707
-
708
- const grpcPath = "/webda.TestService/StreamFail";
709
- const req = createMockReq(grpcPath, {});
710
- const mock = createMockRes();
711
-
712
- await service.handleGrpcRequest(req as any, mock.res);
713
-
714
- assert.strictEqual(mock.trailers!["grpc-status"], String(GrpcStatus.INVALID_ARGUMENT));
715
- }
716
-
717
- @test
718
- async clientStreamingCollectsMessagesAndResponds() {
719
- const service = setupServiceWithRpc("Test.Upload", {
720
- grpc: { streaming: "client" }
721
- });
722
-
723
- const grpcPath = "/webda.TestService/Upload";
724
-
725
- // Create mock req that will emit multiple messages then end
726
- const req = new EventEmitter() as any;
727
- req.url = grpcPath;
728
- req.headers = { "content-type": "application/grpc" };
729
-
730
- const mock = createMockRes();
731
-
732
- mockCallOperationResult = { count: 2, status: "received" };
733
-
734
- const promise = service.handleGrpcRequest(req as any, mock.res);
735
-
736
- // Emit two messages
737
- process.nextTick(() => {
738
- req.emit("data", createGrpcFrame({ item: "a" }));
739
- req.emit("data", createGrpcFrame({ item: "b" }));
740
- // End the stream
741
- req.emit("end");
742
- });
743
-
744
- await promise;
745
-
746
- // Should respond with a unary response (sendUnary)
747
- assert.strictEqual(mock.written.length, 1);
748
- const response = JSON.parse(mock.written[0].subarray(5).toString());
749
- assert.deepStrictEqual(response, { count: 2, status: "received" });
750
- assert.strictEqual(mock.trailers!["grpc-status"], "0");
751
- }
752
-
753
- @test
754
- async bidiStreamingCollectsAndSendsResponse() {
755
- const service = setupServiceWithRpc("Test.Chat", {
756
- grpc: { streaming: "bidi" }
757
- });
758
-
759
- const grpcPath = "/webda.TestService/Chat";
760
- const req = new EventEmitter() as any;
761
- req.url = grpcPath;
762
- req.headers = { "content-type": "application/grpc" };
763
-
764
- const mock = createMockRes();
765
-
766
- mockCallOperationResult = { reply: "ack" };
767
-
768
- const promise = service.handleGrpcRequest(req as any, mock.res);
769
-
770
- process.nextTick(() => {
771
- req.emit("data", createGrpcFrame({ msg: "hello" }));
772
- req.emit("end");
773
- });
774
-
775
- await promise;
776
-
777
- // Bidi uses stream.send (not sendUnary), so no trailers with end
778
- // The current implementation sends once then resolves
779
- assert.strictEqual(mock.written.length, 1);
780
- const response = JSON.parse(mock.written[0].subarray(5).toString());
781
- assert.deepStrictEqual(response, { reply: "ack" });
782
- }
783
-
784
- @test
785
- async clientStreamingHandlesOperationError() {
786
- const service = setupServiceWithRpc("Test.UploadFail", {
787
- grpc: { streaming: "client" }
788
- });
789
- const err: any = new Error("Upload failed");
790
- err.getResponseCode = () => 401;
791
- mockCallOperationError = err;
792
-
793
- const grpcPath = "/webda.TestService/UploadFail";
794
- const req = new EventEmitter() as any;
795
- req.url = grpcPath;
796
- req.headers = { "content-type": "application/grpc" };
797
-
798
- const mock = createMockRes();
799
-
800
- const promise = service.handleGrpcRequest(req as any, mock.res);
801
-
802
- process.nextTick(() => {
803
- req.emit("data", createGrpcFrame({ item: "x" }));
804
- req.emit("end");
805
- });
806
-
807
- await promise;
808
-
809
- assert.strictEqual(mock.trailers!["grpc-status"], String(GrpcStatus.UNAUTHENTICATED));
810
- }
811
-
812
- @test
813
- async clientStreamingHandlesCancellation() {
814
- const service = setupServiceWithRpc("Test.CancelUpload", {
815
- grpc: { streaming: "client" }
816
- });
817
-
818
- const grpcPath = "/webda.TestService/CancelUpload";
819
- const req = new EventEmitter() as any;
820
- req.url = grpcPath;
821
- req.headers = { "content-type": "application/grpc" };
822
-
823
- const mock = createMockRes();
824
-
825
- const promise = service.handleGrpcRequest(req as any, mock.res);
826
-
827
- // Emit close (cancellation) instead of end
828
- process.nextTick(() => {
829
- req.emit("close");
830
- });
831
-
832
- await promise;
833
-
834
- // Should resolve without writing data
835
- assert.strictEqual(mock.written.length, 0);
836
- }
837
-
838
- @test
839
- async unaryWithNoGrpcFieldUsesNoneStreaming() {
840
- // Operation without grpc field should default to unary
841
- const service = setupServiceWithRpc("Test.Simple", {
842
- // no grpc field at all
843
- });
844
-
845
- mockCallOperationResult = { ok: true };
846
-
847
- const grpcPath = "/webda.TestService/Simple";
848
- const req = createMockReq(grpcPath, { query: "test" });
849
- const mock = createMockRes();
850
-
851
- await service.handleGrpcRequest(req as any, mock.res);
852
-
853
- assert.strictEqual(mock.written.length, 1);
854
- assert.strictEqual(mock.trailers!["grpc-status"], "0");
855
- }
856
-
857
- @test
858
- async unaryWithNullOutputReturnsEmptyObject() {
859
- const service = setupServiceWithRpc("Test.NoOutput", {});
860
- mockCallOperationRawOutput = "";
861
-
862
- const grpcPath = "/webda.TestService/NoOutput";
863
- const req = createMockReq(grpcPath, {});
864
- const mock = createMockRes();
865
-
866
- await service.handleGrpcRequest(req as any, mock.res);
867
-
868
- assert.strictEqual(mock.written.length, 1);
869
- const response = JSON.parse(mock.written[0].subarray(5).toString());
870
- assert.deepStrictEqual(response, {});
871
- }
872
450
  }
873
451
 
874
452
  @suite
@@ -997,7 +575,10 @@ class GrpcServiceBuildRpcMapTest {
997
575
  class GrpcServiceInitTest {
998
576
  @test
999
577
  async initWithoutProtoFileLogsWarning() {
1000
- const service = new GrpcService("testGrpc", new GrpcServiceParameters().load({ protoFile: "/nonexistent/path/app.proto" }));
578
+ const service = new GrpcService(
579
+ "testGrpc",
580
+ new GrpcServiceParameters().load({ protoFile: "/nonexistent/path/app.proto" })
581
+ );
1001
582
 
1002
583
  // Should not throw
1003
584
  await service.init();
@@ -1030,7 +611,10 @@ class GrpcServiceInitTest {
1030
611
  Plain: {} // no registerRequestInterceptor — must be skipped
1031
612
  };
1032
613
 
1033
- const service = new GrpcService("testGrpc", new GrpcServiceParameters().load({ protoFile: "/nonexistent/file.proto" }));
614
+ const service = new GrpcService(
615
+ "testGrpc",
616
+ new GrpcServiceParameters().load({ protoFile: "/nonexistent/file.proto" })
617
+ );
1034
618
  await service.init();
1035
619
 
1036
620
  assert.strictEqual(interceptorsA.length, 1, "HttpServer should get the interceptor");
@@ -1063,7 +647,10 @@ class GrpcServiceInitTest {
1063
647
  async initLogsWarningWhenNoHttpServer() {
1064
648
  // No services registered → init should log a warning but not throw.
1065
649
  mockCoreServices = {};
1066
- const service = new GrpcService("testGrpc", new GrpcServiceParameters().load({ protoFile: "/nonexistent/file.proto" }));
650
+ const service = new GrpcService(
651
+ "testGrpc",
652
+ new GrpcServiceParameters().load({ protoFile: "/nonexistent/file.proto" })
653
+ );
1067
654
  await service.init();
1068
655
  mockCoreServices = {};
1069
656
  }
@@ -693,4 +693,21 @@ class ProtoGeneratorTest {
693
693
  // leading-letter guard.
694
694
  assert.ok(result.includes("_1Metadata value = 1;"), "leading-digit sanitized name is prefixed with `_`");
695
695
  }
696
+
697
+ @test
698
+ async streamsFromTheOperationStreamingMode() {
699
+ const schemas = {
700
+ "Relay.Frame": { type: "object", properties: { frame: { type: "string" } }, "x-webda-stream": true }
701
+ } as any;
702
+ const proto = generateProto(
703
+ {
704
+ "Relay.Connect": { id: "Relay.Connect", input: "Relay.Frame", output: "Relay.Frame", streaming: "bidi" },
705
+ "Relay.Watch": { id: "Relay.Watch", input: "void", output: "Relay.Frame", streaming: "server" }
706
+ } as any,
707
+ schemas,
708
+ "webda"
709
+ );
710
+ assert.match(proto, /rpc Connect\(stream \w+\) returns \(stream \w+\);/);
711
+ assert.match(proto, /rpc Watch\(google\.protobuf\.Empty\) returns \(stream \w+\);/);
712
+ }
696
713
  }
@@ -111,7 +111,7 @@ export function generateProto(
111
111
  for (const { opId, op, rpcName } of ops) {
112
112
  const inputType = getInputType(op, schemas, messages);
113
113
  const outputType = getOutputType(op, schemas, messages);
114
- const streaming = op.grpc?.streaming || "none";
114
+ const streaming = op.streaming || (typeof op.grpc === "object" && op.grpc?.streaming) || "none";
115
115
 
116
116
  const inputMod = streaming === "client" || streaming === "bidi" ? "stream " : "";
117
117
  const outputMod = streaming === "server" || streaming === "bidi" ? "stream " : "";
@@ -11,7 +11,8 @@ import {
11
11
  useApplication
12
12
  } from "@webda/core";
13
13
  import { WebdaApplicationTest } from "@webda/core/lib/test/application.js";
14
- import { GrpcService } from "./grpcservice.service.js";
14
+ import { EventEmitter } from "node:events";
15
+ import { GrpcService, GrpcServiceParameters } from "./grpcservice.service.js";
15
16
  import { GrpcStatus } from "./grpc-stream.js";
16
17
 
17
18
  /**
@@ -38,23 +39,48 @@ class GrpcQueryStatementsTest extends WebdaApplicationTest {
38
39
  }
39
40
 
40
41
  /**
41
- * Send one message through the gRPC unary handler
42
+ * Send one message through the gRPC handler
42
43
  * @param query - the query field of the request
43
44
  * @returns the response or the gRPC error status
44
45
  */
45
46
  async unary(query: string): Promise<{ response?: any; status?: number; message?: string }> {
46
47
  const service: any = Object.create(GrpcService.prototype);
47
- const result: { response?: any; status?: number; message?: string } = {};
48
- const stream = {
49
- onMessage: (callback: (message: any) => void) => callback({ query }),
50
- sendUnary: (response: any) => (result.response = response),
51
- sendError: (status: number, message: string) => {
52
- result.status = status;
53
- result.message = message;
54
- }
55
- };
56
- await service.handleUnary(stream, "GrpcNotes.Query", {});
57
- return result;
48
+ service.parameters = new GrpcServiceParameters().load({});
49
+ service.rpcToOperation = new Map([["/webda.GrpcNotes/Query", "GrpcNotes.Query"]]);
50
+ service.rpcMethods = new Map([
51
+ [
52
+ "/webda.GrpcNotes/Query",
53
+ {
54
+ path: "/webda.GrpcNotes/Query",
55
+ requestDeserialize: (buf: Buffer) => JSON.parse(buf.toString()),
56
+ responseSerialize: (msg: any) => Buffer.from(JSON.stringify(msg))
57
+ }
58
+ ]
59
+ ]);
60
+ const req: any = new EventEmitter();
61
+ req.url = "/webda.GrpcNotes/Query";
62
+ req.headers = { "content-type": "application/grpc" };
63
+ const written: Buffer[] = [];
64
+ const headers: Record<string, string> = {};
65
+ let trailers: Record<string, string> = {};
66
+ const res: any = new EventEmitter();
67
+ res.headersSent = false;
68
+ res.setHeader = (key: string, value: string) => (headers[key.toLowerCase()] = value);
69
+ res.write = (data: Buffer) => (written.push(Buffer.from(data)), true);
70
+ res.addTrailers = (t: Record<string, string>) => (trailers = t);
71
+ res.end = () => (res.writableEnded = true);
72
+ const done = service.handleGrpcRequest(req, res);
73
+ const payload = Buffer.from(JSON.stringify({ query }));
74
+ const frame = Buffer.alloc(5 + payload.length);
75
+ frame.writeUInt32BE(payload.length, 1);
76
+ payload.copy(frame, 5);
77
+ req.emit("data", frame);
78
+ req.emit("end");
79
+ await done;
80
+ const status = Number({ ...headers, ...trailers }["grpc-status"]);
81
+ return status === GrpcStatus.OK
82
+ ? { response: JSON.parse(written[0].subarray(5).toString()) }
83
+ : { status, message: decodeURIComponent({ ...headers, ...trailers }["grpc-message"]) };
58
84
  }
59
85
 
60
86
  @test