mirror of
https://github.com/Kong/insomnia.git
synced 2026-04-23 23:59:32 -04:00
Add ability to use Buf Schema Registry as a schema source for gRPC requests (#6975)
* Add support for Buf Reflection Api * Add test; Change tooltips to links * style * Remove label class * request tests * Update copy * Rename prop; Fix alignment, input * Add user agent header * use onBlur and simplify * fix lint --------- Co-authored-by: jackkav <jackkav@gmail.com>
This commit is contained in:
50
package-lock.json
generated
50
package-lock.json
generated
@@ -741,6 +741,11 @@
|
||||
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@bufbuild/protobuf": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.4.1.tgz",
|
||||
"integrity": "sha512-4dthhwBGD9nlpY35ic8dMQC5R0dsND2b2xyeVO3qf+hBk8m7Y9dUs+SmMh6rqO2pGLUTKHefGXLDW+z19hBPdQ=="
|
||||
},
|
||||
"node_modules/@codemirror/language": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.0.0.tgz",
|
||||
@@ -775,6 +780,29 @@
|
||||
"w3c-keyname": "^2.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@connectrpc/connect": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@connectrpc/connect/-/connect-1.1.3.tgz",
|
||||
"integrity": "sha512-AXkbsLQe2Nm7VuoN5nqp05GEb9mPa/f5oFzDqTbHME4i8TghTrlY03uefbhuAq4wjsnfDnmuxHZvn6ndlgXmbg==",
|
||||
"peerDependencies": {
|
||||
"@bufbuild/protobuf": "^1.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@connectrpc/connect-node": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@connectrpc/connect-node/-/connect-node-1.1.3.tgz",
|
||||
"integrity": "sha512-oq7Uk8XlLzC2+eHaxZTX189dhujD0/tK9plizxofsFHUnLquMSmzQQ2GzvTv4u6U05eZYc/crySmf86Sqpi1bA==",
|
||||
"dependencies": {
|
||||
"undici": "^5.26.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@bufbuild/protobuf": "^1.3.3",
|
||||
"@connectrpc/connect": "1.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@develar/schema-utils": {
|
||||
"version": "2.6.5",
|
||||
"resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz",
|
||||
@@ -1774,6 +1802,14 @@
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@fastify/busboy": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.0.0.tgz",
|
||||
"integrity": "sha512-JUFJad5lv7jxj926GPgymrWQxxjPYuJNiNjNMzqT+HiuP6Vl3dk5xzG+8sTX96np0ZAluvaMzPsjhHZ5rNuNQQ==",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@formatjs/ecma402-abstract": {
|
||||
"version": "1.17.0",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.17.0.tgz",
|
||||
@@ -23435,6 +23471,17 @@
|
||||
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz",
|
||||
"integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A=="
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "5.27.2",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-5.27.2.tgz",
|
||||
"integrity": "sha512-iS857PdOEy/y3wlM3yRp+6SNQQ6xU0mmZcwRSriqk+et/cwWAtwmIGf6WkoDN2EK/AMdCO/dfXzIwi+rFMrjjQ==",
|
||||
"dependencies": {
|
||||
"@fastify/busboy": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "5.26.5",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||
@@ -24829,6 +24876,9 @@
|
||||
"dependencies": {
|
||||
"@apideck/better-ajv-errors": "^0.3.6",
|
||||
"@apidevtools/swagger-parser": "10.1.0",
|
||||
"@bufbuild/protobuf": "^1.4.1",
|
||||
"@connectrpc/connect": "^1.1.3",
|
||||
"@connectrpc/connect-node": "^1.1.3",
|
||||
"@getinsomnia/node-libcurl": "^2.4.1-9",
|
||||
"@grpc/grpc-js": "^1.8.17",
|
||||
"@grpc/proto-loader": "^0.7.7",
|
||||
|
||||
@@ -36,6 +36,9 @@
|
||||
"dependencies": {
|
||||
"@apideck/better-ajv-errors": "^0.3.6",
|
||||
"@apidevtools/swagger-parser": "10.1.0",
|
||||
"@bufbuild/protobuf": "^1.4.1",
|
||||
"@connectrpc/connect": "^1.1.3",
|
||||
"@connectrpc/connect-node": "^1.1.3",
|
||||
"@getinsomnia/node-libcurl": "^2.4.1-9",
|
||||
"@grpc/grpc-js": "^1.8.17",
|
||||
"@grpc/proto-loader": "^0.7.7",
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import { TextDecoder, TextEncoder } from 'util';
|
||||
|
||||
Object.assign(globalThis, { TextDecoder, TextEncoder });
|
||||
|
||||
globalThis.__DEV__ = false;
|
||||
|
||||
globalThis.requestAnimationFrame = (callback: FrameRequestCallback) => {
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
import {
|
||||
AnyMessage,
|
||||
MethodInfo,
|
||||
PartialMessage,
|
||||
ServiceType,
|
||||
} from '@bufbuild/protobuf';
|
||||
import { UnaryResponse } from '@connectrpc/connect';
|
||||
import { createConnectTransport } from '@connectrpc/connect-node';
|
||||
import { beforeEach, describe, expect, it, jest } from '@jest/globals';
|
||||
import * as grpcReflection from 'grpc-reflection-js';
|
||||
import protobuf from 'protobufjs';
|
||||
@@ -6,6 +14,7 @@ import { globalBeforeEach } from '../../../__jest__/before-each';
|
||||
import { loadMethodsFromReflection } from '../grpc';
|
||||
|
||||
jest.mock('grpc-reflection-js');
|
||||
jest.mock('@connectrpc/connect-node');
|
||||
|
||||
describe('loadMethodsFromReflection', () => {
|
||||
beforeEach(globalBeforeEach);
|
||||
@@ -37,7 +46,11 @@ describe('loadMethodsFromReflection', () => {
|
||||
});
|
||||
|
||||
it('parses methods', async () => {
|
||||
const methods = await loadMethodsFromReflection({ url: 'foo.com', metadata: [] });
|
||||
const methods = await loadMethodsFromReflection({
|
||||
url: 'foo.com',
|
||||
metadata: [],
|
||||
reflectionApi: { enabled: false, apiKey: '', url: '', module: '' },
|
||||
});
|
||||
expect(methods).toStrictEqual([{
|
||||
type: 'unary',
|
||||
fullPath: '/FooService/Foo',
|
||||
@@ -75,7 +88,11 @@ describe('loadMethodsFromReflection', () => {
|
||||
});
|
||||
|
||||
it('parses methods', async () => {
|
||||
const methods = await loadMethodsFromReflection({ url: 'foo.com', metadata: [] });
|
||||
const methods = await loadMethodsFromReflection({
|
||||
url: 'foo.com',
|
||||
metadata: [],
|
||||
reflectionApi: { enabled: false, apiKey: '', url: '', module: '' },
|
||||
});
|
||||
expect(methods).toStrictEqual([{
|
||||
type: 'unary',
|
||||
fullPath: '/FooService/format',
|
||||
@@ -125,7 +142,11 @@ describe('loadMethodsFromReflection', () => {
|
||||
});
|
||||
|
||||
it('parses methods', async () => {
|
||||
const methods = await loadMethodsFromReflection({ url: 'foo-bar.com', metadata: [] });
|
||||
const methods = await loadMethodsFromReflection({
|
||||
url: 'foo-bar.com',
|
||||
metadata: [],
|
||||
reflectionApi: { enabled: false, apiKey: '', url: '', module: '' },
|
||||
});
|
||||
expect(methods).toStrictEqual([{
|
||||
type: 'unary',
|
||||
fullPath: '/FooService/Foo',
|
||||
@@ -142,4 +163,66 @@ describe('loadMethodsFromReflection', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('buf reflection api', () => {
|
||||
it('loads module', async () => {
|
||||
(createConnectTransport as unknown as jest.Mock).mockImplementation(
|
||||
options => {
|
||||
expect(options.baseUrl).toStrictEqual('https://buf.build');
|
||||
return {
|
||||
async unary(
|
||||
service: ServiceType,
|
||||
method: MethodInfo,
|
||||
_: AbortSignal | undefined,
|
||||
__: number | undefined,
|
||||
header: HeadersInit | undefined,
|
||||
input: PartialMessage<AnyMessage>
|
||||
): Promise<UnaryResponse> {
|
||||
expect(new Headers(header).get('Authorization')).toStrictEqual('Bearer TEST_KEY');
|
||||
expect(input).toStrictEqual({ module: 'buf.build/connectrpc/eliza' });
|
||||
return {
|
||||
service: service,
|
||||
method: method,
|
||||
header: new Headers(),
|
||||
trailer: new Headers(),
|
||||
stream: false,
|
||||
// Output of running `buf curl https://buf.build/buf.reflect.v1beta1.FileDescriptorSetService/GetFileDescriptorSet --data '{"module": "buf.build/connectrpc/eliza"}' --schema buf.build/bufbuild/reflect -H 'Authorization: Bearer buf-token'`
|
||||
message: method.O.fromJsonString(
|
||||
'{"fileDescriptorSet":{"file":[{"name":"connectrpc/eliza/v1/eliza.proto","package":"connectrpc.eliza.v1","messageType":[{"name":"SayRequest","field":[{"name":"sentence","number":1,"label":"LABEL_OPTIONAL","type":"TYPE_STRING","jsonName":"sentence"}]},{"name":"SayResponse","field":[{"name":"sentence","number":1,"label":"LABEL_OPTIONAL","type":"TYPE_STRING","jsonName":"sentence"}]},{"name":"ConverseRequest","field":[{"name":"sentence","number":1,"label":"LABEL_OPTIONAL","type":"TYPE_STRING","jsonName":"sentence"}]},{"name":"ConverseResponse","field":[{"name":"sentence","number":1,"label":"LABEL_OPTIONAL","type":"TYPE_STRING","jsonName":"sentence"}]},{"name":"IntroduceRequest","field":[{"name":"name","number":1,"label":"LABEL_OPTIONAL","type":"TYPE_STRING","jsonName":"name"}]},{"name":"IntroduceResponse","field":[{"name":"sentence","number":1,"label":"LABEL_OPTIONAL","type":"TYPE_STRING","jsonName":"sentence"}]}],"service":[{"name":"ElizaService","method":[{"name":"Say","inputType":".connectrpc.eliza.v1.SayRequest","outputType":".connectrpc.eliza.v1.SayResponse","options":{"idempotencyLevel":"NO_SIDE_EFFECTS"}},{"name":"Converse","inputType":".connectrpc.eliza.v1.ConverseRequest","outputType":".connectrpc.eliza.v1.ConverseResponse","options":{},"clientStreaming":true,"serverStreaming":true},{"name":"Introduce","inputType":".connectrpc.eliza.v1.IntroduceRequest","outputType":".connectrpc.eliza.v1.IntroduceResponse","options":{},"serverStreaming":true}]}],"syntax":"proto3"}]},"version":"233fca715f49425581ec0a1b660be886"}'
|
||||
),
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
);
|
||||
const methods = await loadMethodsFromReflection({
|
||||
url: 'foo.com',
|
||||
metadata: [],
|
||||
reflectionApi: {
|
||||
enabled: true,
|
||||
apiKey: 'TEST_KEY',
|
||||
url: 'https://buf.build',
|
||||
module: 'buf.build/connectrpc/eliza',
|
||||
},
|
||||
});
|
||||
expect(methods).toStrictEqual(
|
||||
[
|
||||
{
|
||||
example: undefined,
|
||||
fullPath: '/connectrpc.eliza.v1.ElizaService/Say',
|
||||
type: 'unary',
|
||||
},
|
||||
{
|
||||
example: undefined,
|
||||
fullPath: '/connectrpc.eliza.v1.ElizaService/Converse',
|
||||
type: 'bidi',
|
||||
},
|
||||
{
|
||||
example: undefined,
|
||||
fullPath: '/connectrpc.eliza.v1.ElizaService/Introduce',
|
||||
type: 'server',
|
||||
},
|
||||
]
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,34 @@
|
||||
import { Call, ClientDuplexStream, ClientReadableStream, credentials, makeGenericClientConstructor, Metadata, ServiceError, status, StatusObject } from '@grpc/grpc-js';
|
||||
import {
|
||||
FileDescriptorSet as ProtobufEsFileDescriptorSet,
|
||||
MethodIdempotency,
|
||||
MethodKind,
|
||||
proto3,
|
||||
} from '@bufbuild/protobuf';
|
||||
import { Code, ConnectError, createPromiseClient } from '@connectrpc/connect';
|
||||
import { createConnectTransport } from '@connectrpc/connect-node';
|
||||
import {
|
||||
Call,
|
||||
ClientDuplexStream,
|
||||
ClientReadableStream,
|
||||
credentials,
|
||||
makeGenericClientConstructor,
|
||||
Metadata,
|
||||
ServiceError,
|
||||
status,
|
||||
StatusObject,
|
||||
} from '@grpc/grpc-js';
|
||||
import * as protoLoader from '@grpc/proto-loader';
|
||||
import { AnyDefinition, EnumTypeDefinition, MessageTypeDefinition, PackageDefinition, ServiceDefinition } from '@grpc/proto-loader';
|
||||
import {
|
||||
AnyDefinition,
|
||||
EnumTypeDefinition,
|
||||
MessageTypeDefinition,
|
||||
PackageDefinition,
|
||||
ServiceDefinition,
|
||||
} from '@grpc/proto-loader';
|
||||
import electron, { ipcMain, IpcMainEvent } from 'electron';
|
||||
import * as grpcReflection from 'grpc-reflection-js';
|
||||
|
||||
import { version } from '../../../package.json';
|
||||
import type { RenderedGrpcRequest, RenderedGrpcRequestBody } from '../../common/render';
|
||||
import * as models from '../../models';
|
||||
import type { GrpcRequest, GrpcRequestHeader } from '../../models/grpc-request';
|
||||
@@ -13,6 +38,7 @@ import { invariant } from '../../utils/invariant';
|
||||
import { mockRequestMethods } from './automock';
|
||||
|
||||
const grpcCalls = new Map<string, Call>();
|
||||
|
||||
export interface GrpcIpcRequestParams {
|
||||
request: RenderedGrpcRequest;
|
||||
}
|
||||
@@ -21,6 +47,7 @@ export interface GrpcIpcMessageParams {
|
||||
requestId: string;
|
||||
body: RenderedGrpcRequestBody;
|
||||
}
|
||||
|
||||
export interface gRPCBridgeAPI {
|
||||
start: (options: GrpcIpcRequestParams) => void;
|
||||
sendMessage: (options: GrpcIpcMessageParams) => void;
|
||||
@@ -30,6 +57,7 @@ export interface gRPCBridgeAPI {
|
||||
loadMethodsFromReflection: typeof loadMethodsFromReflection;
|
||||
closeAll: typeof closeAll;
|
||||
}
|
||||
|
||||
export function registergRPCHandlers() {
|
||||
ipcMain.on('grpc.start', start);
|
||||
ipcMain.on('grpc.sendMessage', sendMessage);
|
||||
@@ -39,6 +67,7 @@ export function registergRPCHandlers() {
|
||||
ipcMain.handle('grpc.loadMethods', (_, requestId) => loadMethods(requestId));
|
||||
ipcMain.handle('grpc.loadMethodsFromReflection', (_, requestId) => loadMethodsFromReflection(requestId));
|
||||
}
|
||||
|
||||
const grpcOptions = {
|
||||
keepCase: true,
|
||||
longs: String,
|
||||
@@ -67,6 +96,7 @@ const loadMethods = async (protoFileId: string): Promise<GrpcMethodInfo[]> => {
|
||||
fullPath: method.path,
|
||||
}));
|
||||
};
|
||||
|
||||
interface MethodDefs {
|
||||
path: string;
|
||||
requestStream: boolean;
|
||||
@@ -75,10 +105,109 @@ interface MethodDefs {
|
||||
responseDeserialize: (value: Buffer) => any;
|
||||
example?: Record<string, any>;
|
||||
}
|
||||
const getMethodsFromReflection = async (host: string, metadata: GrpcRequestHeader[]): Promise<MethodDefs[]> => {
|
||||
|
||||
const getMethodsFromReflectionServer = async (
|
||||
reflectionApi: GrpcRequest['reflectionApi']
|
||||
): Promise<MethodDefs[]> => {
|
||||
const { url, module, apiKey } = reflectionApi;
|
||||
const GetFileDescriptorSetRequest = proto3.makeMessageType(
|
||||
'buf.reflect.v1beta1.GetFileDescriptorSetRequest',
|
||||
() => [
|
||||
{ no: 1, name: 'module', kind: 'scalar', T: 9 /* ScalarType.STRING */ },
|
||||
{ no: 2, name: 'version', kind: 'scalar', T: 9 /* ScalarType.STRING */ },
|
||||
{
|
||||
no: 3,
|
||||
name: 'symbols',
|
||||
kind: 'scalar',
|
||||
T: 9 /* ScalarType.STRING */,
|
||||
repeated: true,
|
||||
},
|
||||
]
|
||||
);
|
||||
const GetFileDescriptorSetResponse = proto3.makeMessageType(
|
||||
'buf.reflect.v1beta1.GetFileDescriptorSetResponse',
|
||||
() => [
|
||||
{
|
||||
no: 1,
|
||||
name: 'file_descriptor_set',
|
||||
kind: 'message',
|
||||
T: ProtobufEsFileDescriptorSet,
|
||||
},
|
||||
{ no: 2, name: 'version', kind: 'scalar', T: 9 /* ScalarType.STRING */ },
|
||||
]
|
||||
);
|
||||
const FileDescriptorSetService = {
|
||||
typeName: 'buf.reflect.v1beta1.FileDescriptorSetService',
|
||||
methods: {
|
||||
getFileDescriptorSet: {
|
||||
name: 'GetFileDescriptorSet',
|
||||
I: GetFileDescriptorSetRequest,
|
||||
O: GetFileDescriptorSetResponse,
|
||||
kind: MethodKind.Unary,
|
||||
idempotency: MethodIdempotency.NoSideEffects,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
const transport = createConnectTransport({
|
||||
baseUrl: url,
|
||||
httpVersion: '1.1',
|
||||
});
|
||||
const client = createPromiseClient(FileDescriptorSetService, transport);
|
||||
const headers: HeadersInit = {
|
||||
'User-Agent': `insomnia/${version}`,
|
||||
...(apiKey === '' ? {} : { Authorization: `Bearer ${apiKey}` }),
|
||||
};
|
||||
try {
|
||||
const res = await client.getFileDescriptorSet(
|
||||
{
|
||||
module,
|
||||
},
|
||||
{
|
||||
headers,
|
||||
}
|
||||
);
|
||||
const methodDefs: MethodDefs[] = [];
|
||||
if (res.fileDescriptorSet === undefined) {
|
||||
return [];
|
||||
}
|
||||
const packageDefinition = protoLoader.loadFileDescriptorSetFromBuffer(
|
||||
new Buffer(res.fileDescriptorSet.toBinary())
|
||||
);
|
||||
for (const definition of Object.values(packageDefinition)) {
|
||||
const serviceDefinition = asServiceDefinition(definition);
|
||||
if (serviceDefinition === null) {
|
||||
continue;
|
||||
}
|
||||
const serviceMethods = Object.values(serviceDefinition);
|
||||
methodDefs.push(...serviceMethods);
|
||||
}
|
||||
return methodDefs;
|
||||
} catch (error) {
|
||||
const connectError = ConnectError.from(error);
|
||||
switch (connectError.code) {
|
||||
case Code.Unauthenticated:
|
||||
throw new Error('Invalid reflection server api key');
|
||||
case Code.NotFound:
|
||||
throw new Error(
|
||||
"The reflection server api key doesn't have access to the module or the module does not exists"
|
||||
);
|
||||
default:
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
const getMethodsFromReflection = async (
|
||||
host: string,
|
||||
metadata: GrpcRequestHeader[],
|
||||
reflectionApi: GrpcRequest['reflectionApi']
|
||||
): Promise<MethodDefs[]> => {
|
||||
if (reflectionApi.enabled) {
|
||||
return getMethodsFromReflectionServer(reflectionApi);
|
||||
}
|
||||
try {
|
||||
const { url, enableTls } = parseGrpcUrl(host);
|
||||
const client = new grpcReflection.Client(url,
|
||||
const client = new grpcReflection.Client(
|
||||
url,
|
||||
enableTls ? credentials.createSsl() : credentials.createInsecure(),
|
||||
grpcOptions,
|
||||
filterDisabledMetaData(metadata)
|
||||
@@ -89,15 +218,25 @@ const getMethodsFromReflection = async (host: string, metadata: GrpcRequestHeade
|
||||
const fullService = fileContainingSymbol.lookupService(service);
|
||||
const mockedRequestMethods = mockRequestMethods(fullService);
|
||||
const descriptorMessage = fileContainingSymbol.toDescriptor('proto3');
|
||||
const packageDefinition = protoLoader.loadFileDescriptorSetFromObject(descriptorMessage, {});
|
||||
const packageDefinition = protoLoader.loadFileDescriptorSetFromObject(
|
||||
descriptorMessage,
|
||||
{}
|
||||
);
|
||||
const tryToGetMethods = () => {
|
||||
try {
|
||||
console.log('[grpc] loading service from reflection:', service);
|
||||
const serviceDefinition = asServiceDefinition(packageDefinition[service]);
|
||||
invariant(serviceDefinition, `'${service}' was not a valid ServiceDefinition`);
|
||||
const serviceDefinition = asServiceDefinition(
|
||||
packageDefinition[service]
|
||||
);
|
||||
invariant(
|
||||
serviceDefinition,
|
||||
`'${service}' was not a valid ServiceDefinition`
|
||||
);
|
||||
const serviceMethods = Object.values(serviceDefinition);
|
||||
return serviceMethods.map(m => {
|
||||
const methodName = Object.keys(mockedRequestMethods).find(name => m.path.endsWith(`/${name}`));
|
||||
const methodName = Object.keys(mockedRequestMethods).find(name =>
|
||||
m.path.endsWith(`/${name}`)
|
||||
);
|
||||
if (!methodName) {
|
||||
return m;
|
||||
}
|
||||
@@ -119,21 +258,34 @@ const getMethodsFromReflection = async (host: string, metadata: GrpcRequestHeade
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
export const loadMethodsFromReflection = async (options: { url: string; metadata: GrpcRequestHeader[] }): Promise<GrpcMethodInfo[]> => {
|
||||
export const loadMethodsFromReflection = async (options: {
|
||||
url: string;
|
||||
metadata: GrpcRequestHeader[];
|
||||
reflectionApi: GrpcRequest['reflectionApi'];
|
||||
}): Promise<GrpcMethodInfo[]> => {
|
||||
invariant(options.url, 'gRPC request url not provided');
|
||||
const methods = await getMethodsFromReflection(options.url, options.metadata);
|
||||
const methods = await getMethodsFromReflection(
|
||||
options.url,
|
||||
options.metadata,
|
||||
options.reflectionApi
|
||||
);
|
||||
return methods.map(method => ({
|
||||
type: getMethodType(method),
|
||||
fullPath: method.path,
|
||||
example: method.example,
|
||||
}));
|
||||
};
|
||||
|
||||
export interface GrpcMethodInfo {
|
||||
type: GrpcMethodType;
|
||||
fullPath: string;
|
||||
example?: Record<string, any>;
|
||||
}
|
||||
export const getMethodType = ({ requestStream, responseStream }: any): GrpcMethodType => {
|
||||
|
||||
export const getMethodType = ({
|
||||
requestStream,
|
||||
responseStream,
|
||||
}: any): GrpcMethodType => {
|
||||
if (requestStream && responseStream) {
|
||||
return 'bidi';
|
||||
}
|
||||
@@ -146,16 +298,25 @@ export const getMethodType = ({ requestStream, responseStream }: any): GrpcMetho
|
||||
return 'unary';
|
||||
};
|
||||
|
||||
export const getSelectedMethod = async (request: GrpcRequest): Promise<MethodDefs | undefined> => {
|
||||
export const getSelectedMethod = async (
|
||||
request: GrpcRequest
|
||||
): Promise<MethodDefs | undefined> => {
|
||||
if (request.protoFileId) {
|
||||
const protoFile = await models.protoFile.getById(request.protoFileId);
|
||||
invariant(protoFile?.protoText, `No proto file found for gRPC request ${request._id}`);
|
||||
invariant(
|
||||
protoFile?.protoText,
|
||||
`No proto file found for gRPC request ${request._id}`
|
||||
);
|
||||
const { filePath, dirs } = await writeProtoFile(protoFile);
|
||||
const methods = await loadMethodsFromFilePath(filePath, dirs);
|
||||
invariant(methods, 'No methods found');
|
||||
return methods.find(c => c.path === request.protoMethodName);
|
||||
}
|
||||
const methods = await getMethodsFromReflection(request.url, request.metadata);
|
||||
const methods = await getMethodsFromReflection(
|
||||
request.url,
|
||||
request.metadata,
|
||||
request.reflectionApi
|
||||
);
|
||||
invariant(methods, 'No reflection methods found');
|
||||
return methods.find(c => c.path === request.protoMethodName);
|
||||
};
|
||||
@@ -336,7 +497,7 @@ const onUnaryResponse = (event: IpcMainEvent, requestId: string) => (err: Servic
|
||||
grpcCalls.delete(requestId);
|
||||
};
|
||||
|
||||
const filterDisabledMetaData = (metadata: GrpcRequestHeader[],): Metadata => {
|
||||
const filterDisabledMetaData = (metadata: GrpcRequestHeader[]): Metadata => {
|
||||
const grpcMetadata = new Metadata();
|
||||
for (const entry of metadata) {
|
||||
if (!entry.disabled) {
|
||||
|
||||
@@ -18,6 +18,12 @@ describe('init()', () => {
|
||||
body: {
|
||||
text: '{}',
|
||||
},
|
||||
reflectionApi: {
|
||||
enabled: false,
|
||||
apiKey: '',
|
||||
module: 'buf.build/connectrpc/eliza',
|
||||
url: 'https://buf.build',
|
||||
},
|
||||
metaSortKey: -1478795580200,
|
||||
isPrivate: false,
|
||||
});
|
||||
@@ -47,6 +53,12 @@ describe('create()', () => {
|
||||
body: {
|
||||
text: '{}',
|
||||
},
|
||||
reflectionApi: {
|
||||
enabled: false,
|
||||
apiKey: '',
|
||||
module: 'buf.build/connectrpc/eliza',
|
||||
url: 'https://buf.build',
|
||||
},
|
||||
metaSortKey: -1478795580200,
|
||||
isPrivate: false,
|
||||
type: 'GrpcRequest',
|
||||
|
||||
@@ -28,6 +28,12 @@ interface BaseGrpcRequest {
|
||||
metadata: GrpcRequestHeader[];
|
||||
metaSortKey: number;
|
||||
isPrivate: boolean;
|
||||
reflectionApi: {
|
||||
enabled: boolean;
|
||||
url: string;
|
||||
apiKey: string;
|
||||
module: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type GrpcRequest = BaseModel & BaseGrpcRequest;
|
||||
@@ -53,6 +59,12 @@ export function init(): BaseGrpcRequest {
|
||||
},
|
||||
metaSortKey: -1 * Date.now(),
|
||||
isPrivate: false,
|
||||
reflectionApi: {
|
||||
enabled: false,
|
||||
url: 'https://buf.build',
|
||||
apiKey: '',
|
||||
module: 'buf.build/connectrpc/eliza',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import { ModalBody } from '../base/modal-body';
|
||||
import { ModalHeader } from '../base/modal-header';
|
||||
import { CodeEditorHandle } from '../codemirror/code-editor';
|
||||
import { HelpTooltip } from '../help-tooltip';
|
||||
import { Icon } from '../icon';
|
||||
import { MarkdownEditor } from '../markdown-editor';
|
||||
|
||||
export interface RequestSettingsModalOptions {
|
||||
@@ -71,6 +72,15 @@ export const RequestSettingsModal = ({ request, onHide }: ModalProps & RequestSe
|
||||
const toggleCheckBox = async (event: any) => {
|
||||
patchRequest(request._id, { [event.currentTarget.name]: event.currentTarget.checked ? true : false });
|
||||
};
|
||||
const updateReflectonApi = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
invariant(isGrpcRequest(request), 'Must be gRPC request');
|
||||
patchRequest(request._id, {
|
||||
reflectionApi: {
|
||||
...request.reflectionApi,
|
||||
[event.currentTarget.name]: event.currentTarget.value,
|
||||
},
|
||||
});
|
||||
};
|
||||
const updateDescription = (description: string) => {
|
||||
patchRequest(request._id, { description });
|
||||
setState({
|
||||
@@ -202,10 +212,84 @@ export const RequestSettingsModal = ({ request, onHide }: ModalProps & RequestSe
|
||||
</div>
|
||||
</>)}
|
||||
{request && isGrpcRequest(request) && (
|
||||
<p className="faint italic">
|
||||
Are there any gRPC settings you expect to see? Create a{' '}
|
||||
<a href={'https://github.com/Kong/insomnia/issues/new/choose'}>feature request</a>!
|
||||
</p>
|
||||
<>
|
||||
<div className="form-control form-control--thin pad-top-sm">
|
||||
<label>
|
||||
Use the Buf Schema Registry API
|
||||
<a href="https://buf.build/docs/bsr/reflection/overview" className="pad-left-sm">
|
||||
<Icon icon="external-link" size="sm" />
|
||||
</a>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="reflectionApi"
|
||||
checked={request.reflectionApi.enabled}
|
||||
onChange={event => patchRequest(request._id, {
|
||||
reflectionApi: {
|
||||
...request.reflectionApi,
|
||||
enabled: event.currentTarget.checked,
|
||||
},
|
||||
})}
|
||||
/>̵
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-row pad-top-sm">
|
||||
{request.reflectionApi.enabled && (
|
||||
<>
|
||||
<div className="form-control form-control--outlined">
|
||||
<label>
|
||||
Reflection server URL
|
||||
<a href="https://buf.build/docs/bsr/api-access" className="pad-left-sm">
|
||||
<Icon icon="external-link" size="sm" />
|
||||
</a>
|
||||
<input
|
||||
type="text"
|
||||
name="url"
|
||||
placeholder="https://buf.build"
|
||||
defaultValue={request.reflectionApi.url}
|
||||
onBlur={updateReflectonApi}
|
||||
disabled={!request.reflectionApi.enabled}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-control form-control--outlined">
|
||||
<label>
|
||||
Reflection server API key
|
||||
<a href="https://buf.build/docs/bsr/authentication#manage-tokens" className="pad-left-sm">
|
||||
<Icon icon="external-link" size="sm" />
|
||||
</a>
|
||||
<input
|
||||
type="password"
|
||||
name="apiKey"
|
||||
defaultValue={request.reflectionApi.apiKey}
|
||||
onBlur={updateReflectonApi}
|
||||
disabled={!request.reflectionApi.enabled}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-control form-control--outlined">
|
||||
<label>
|
||||
Module
|
||||
<a href="https://buf.build/docs/bsr/module/manage" className="pad-left-sm">
|
||||
<Icon icon="external-link" size="sm" />
|
||||
</a>
|
||||
<input
|
||||
type="text"
|
||||
name="module"
|
||||
placeholder="buf.build/connectrpc/eliza"
|
||||
defaultValue={request.reflectionApi.module}
|
||||
onBlur={updateReflectonApi}
|
||||
disabled={!request.reflectionApi.enabled}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<p className="faint italic pad-top">
|
||||
Are there any gRPC settings you expect to see? Create a{' '}
|
||||
<a href={'https://github.com/Kong/insomnia/issues/new/choose'}>feature request</a>!
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
{request && isRequest(request) && (
|
||||
<>
|
||||
|
||||
@@ -197,7 +197,16 @@ export const GrpcRequestPane: FunctionComponent<Props> = ({
|
||||
disabled={!activeRequest.url}
|
||||
onClick={async () => {
|
||||
try {
|
||||
const rendered = await tryToInterpolateRequestOrShowRenderErrorModal({ request: activeRequest, environmentId, payload: { url: activeRequest.url, metadata: activeRequest.metadata } });
|
||||
const rendered =
|
||||
await tryToInterpolateRequestOrShowRenderErrorModal({
|
||||
request: activeRequest,
|
||||
environmentId,
|
||||
payload: {
|
||||
url: activeRequest.url,
|
||||
metadata: activeRequest.metadata,
|
||||
reflectionApi: activeRequest.reflectionApi,
|
||||
},
|
||||
});
|
||||
const methods = await window.main.grpc.loadMethodsFromReflection(rendered);
|
||||
setGrpcState({ ...grpcState, methods });
|
||||
patchRequest(requestId, { protoFileId: '', protoMethodName: '' });
|
||||
|
||||
Reference in New Issue
Block a user