gRPC with Insomnia export format V4 (#2830)

* feat: initial commit to export grpc entities

* feat: grpc requests and protofiles in v4 export format

* test(export): add grpc requests and protofiles in export tests

* test(models): add isProtoFile tests
This commit is contained in:
Opender Singh 2020-11-14 12:31:18 +13:00 committed by GitHub
parent 8f8acf83f2
commit ccd270a9fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 118 additions and 27 deletions

View File

@ -187,6 +187,24 @@ describe('export', () => {
name: 'Request 1', name: 'Request 1',
parentId: w._id, parentId: w._id,
}); });
const pf1 = await models.protoFile.create({
name: 'ProtoFile 1',
parentId: w._id,
});
const gr1 = await models.grpcRequest.create({
name: 'Grpc Request 1',
parentId: w._id,
protoFileId: pf1._id,
});
const pf2 = await models.protoFile.create({
name: 'ProtoFile 2',
parentId: w._id,
});
const gr2 = await models.grpcRequest.create({
name: 'Grpc Request 2',
parentId: w._id,
protoFileId: pf2._id,
});
const f2 = await models.requestGroup.create({ const f2 = await models.requestGroup.create({
name: 'Folder 2', name: 'Folder 2',
parentId: w._id, parentId: w._id,
@ -229,13 +247,17 @@ describe('export', () => {
expect.objectContaining({ _id: f2._id }), expect.objectContaining({ _id: f2._id }),
expect.objectContaining({ _id: r2._id }), expect.objectContaining({ _id: r2._id }),
expect.objectContaining({ _id: ePub._id }), expect.objectContaining({ _id: ePub._id }),
expect.objectContaining({ _id: gr1._id }),
expect.objectContaining({ _id: pf1._id }),
expect.objectContaining({ _id: gr2._id }),
expect.objectContaining({ _id: pf2._id }),
]), ]),
}); });
expect(exportWorkspacesDataJson.resources.length).toBe(8); expect(exportWorkspacesDataJson.resources.length).toBe(12);
// Test export some requests only. // Test export some requests only.
const exportRequestsJson = await importUtil.exportRequestsData([r1], false, 'json'); const exportRequestsJson = await importUtil.exportRequestsData([r1, gr1], false, 'json');
const exportRequestsYaml = await importUtil.exportRequestsData([r1], false, 'yaml'); const exportRequestsYaml = await importUtil.exportRequestsData([r1, gr1], false, 'yaml');
const exportRequestsDataJSON = JSON.parse(exportRequestsJson); const exportRequestsDataJSON = JSON.parse(exportRequestsJson);
const exportRequestsDataYAML = YAML.parse(exportRequestsYaml); const exportRequestsDataYAML = YAML.parse(exportRequestsYaml);
@ -250,11 +272,14 @@ describe('export', () => {
expect.objectContaining({ _id: jar._id }), expect.objectContaining({ _id: jar._id }),
expect.objectContaining({ _id: r1._id }), expect.objectContaining({ _id: r1._id }),
expect.objectContaining({ _id: ePub._id }), expect.objectContaining({ _id: ePub._id }),
expect.objectContaining({ _id: gr1._id }),
expect.objectContaining({ _id: pf1._id }),
expect.objectContaining({ _id: pf2._id }),
]), ]),
}); });
expect(exportRequestsDataJSON.resources.length).toBe(6); expect(exportRequestsDataJSON.resources.length).toBe(9);
expect(exportRequestsDataYAML.resources.length).toBe(6); expect(exportRequestsDataYAML.resources.length).toBe(9);
// Ensure JSON and YAML are the same // Ensure JSON and YAML are the same
expect(exportRequestsDataJSON.resources).toEqual(exportRequestsDataYAML.resources); expect(exportRequestsDataJSON.resources).toEqual(exportRequestsDataYAML.resources);
@ -266,10 +291,19 @@ describe('export', () => {
contents: 'openapi: "3.0.0"', contents: 'openapi: "3.0.0"',
}); });
const jar = await models.cookieJar.getOrCreateForParentId(w._id); const jar = await models.cookieJar.getOrCreateForParentId(w._id);
const pf1 = await models.protoFile.create({
name: 'ProtoFile 1',
parentId: w._id,
});
const r1 = await models.request.create({ const r1 = await models.request.create({
name: 'Request 1', name: 'Request 1',
parentId: w._id, parentId: w._id,
}); });
const gr1 = await models.grpcRequest.create({
name: 'Grpc Request 1',
parentId: w._id,
protoFileId: pf1._id,
});
const f2 = await models.requestGroup.create({ const f2 = await models.requestGroup.create({
name: 'Folder 2', name: 'Folder 2',
parentId: w._id, parentId: w._id,
@ -278,6 +312,11 @@ describe('export', () => {
name: 'Request 2', name: 'Request 2',
parentId: f2._id, parentId: f2._id,
}); });
const gr2 = await models.grpcRequest.create({
name: 'Grpc Request 2',
parentId: f2._id,
protoFileId: pf1._id,
});
const uts1 = await models.unitTestSuite.create({ const uts1 = await models.unitTestSuite.create({
name: 'Unit Test Suite One', name: 'Unit Test Suite One',
parentId: w._id, parentId: w._id,
@ -308,8 +347,11 @@ describe('export', () => {
expect.objectContaining({ _id: w._id }), expect.objectContaining({ _id: w._id }),
expect.objectContaining({ _id: eBase._id }), expect.objectContaining({ _id: eBase._id }),
expect.objectContaining({ _id: jar._id }), expect.objectContaining({ _id: jar._id }),
expect.objectContaining({ _id: pf1._id }),
expect.objectContaining({ _id: r1._id }), expect.objectContaining({ _id: r1._id }),
expect.objectContaining({ _id: r2._id }), expect.objectContaining({ _id: r2._id }),
expect.objectContaining({ _id: gr1._id }),
expect.objectContaining({ _id: gr2._id }),
expect.objectContaining({ _id: uts1._id }), expect.objectContaining({ _id: uts1._id }),
expect.objectContaining({ _id: ut1._id }), expect.objectContaining({ _id: ut1._id }),
expect.objectContaining({ _id: ePub._id }), expect.objectContaining({ _id: ePub._id }),

View File

@ -12,6 +12,7 @@ import fs from 'fs';
import { fnOrString, generateId } from './misc'; import { fnOrString, generateId } from './misc';
import YAML from 'yaml'; import YAML from 'yaml';
import { trackEvent } from './analytics'; import { trackEvent } from './analytics';
import { isGrpcRequest, isProtoFile, isRequest, isRequestGroup } from '../models/helpers/is-model';
const WORKSPACE_ID_KEY = '__WORKSPACE_ID__'; const WORKSPACE_ID_KEY = '__WORKSPACE_ID__';
const BASE_ENVIRONMENT_ID_KEY = '__BASE_ENVIRONMENT_ID__'; const BASE_ENVIRONMENT_ID_KEY = '__BASE_ENVIRONMENT_ID__';
@ -19,6 +20,7 @@ const BASE_ENVIRONMENT_ID_KEY = '__BASE_ENVIRONMENT_ID__';
const EXPORT_FORMAT = 4; const EXPORT_FORMAT = 4;
const EXPORT_TYPE_REQUEST = 'request'; const EXPORT_TYPE_REQUEST = 'request';
const EXPORT_TYPE_GRPC_REQUEST = 'grpc_request';
const EXPORT_TYPE_REQUEST_GROUP = 'request_group'; const EXPORT_TYPE_REQUEST_GROUP = 'request_group';
const EXPORT_TYPE_UNIT_TEST_SUITE = 'unit_test_suite'; const EXPORT_TYPE_UNIT_TEST_SUITE = 'unit_test_suite';
const EXPORT_TYPE_UNIT_TEST = 'unit_test'; const EXPORT_TYPE_UNIT_TEST = 'unit_test';
@ -26,12 +28,14 @@ const EXPORT_TYPE_WORKSPACE = 'workspace';
const EXPORT_TYPE_COOKIE_JAR = 'cookie_jar'; const EXPORT_TYPE_COOKIE_JAR = 'cookie_jar';
const EXPORT_TYPE_ENVIRONMENT = 'environment'; const EXPORT_TYPE_ENVIRONMENT = 'environment';
const EXPORT_TYPE_API_SPEC = 'api_spec'; const EXPORT_TYPE_API_SPEC = 'api_spec';
const EXPORT_TYPE_PROTO_FILE = 'proto_file';
// If we come across an ID of this form, we will replace it with a new one // If we come across an ID of this form, we will replace it with a new one
const REPLACE_ID_REGEX = /__\w+_\d+__/g; const REPLACE_ID_REGEX = /__\w+_\d+__/g;
const MODELS = { const MODELS = {
[EXPORT_TYPE_REQUEST]: models.request, [EXPORT_TYPE_REQUEST]: models.request,
[EXPORT_TYPE_GRPC_REQUEST]: models.grpcRequest,
[EXPORT_TYPE_REQUEST_GROUP]: models.requestGroup, [EXPORT_TYPE_REQUEST_GROUP]: models.requestGroup,
[EXPORT_TYPE_UNIT_TEST_SUITE]: models.unitTestSuite, [EXPORT_TYPE_UNIT_TEST_SUITE]: models.unitTestSuite,
[EXPORT_TYPE_UNIT_TEST]: models.unitTest, [EXPORT_TYPE_UNIT_TEST]: models.unitTest,
@ -39,6 +43,7 @@ const MODELS = {
[EXPORT_TYPE_COOKIE_JAR]: models.cookieJar, [EXPORT_TYPE_COOKIE_JAR]: models.cookieJar,
[EXPORT_TYPE_ENVIRONMENT]: models.environment, [EXPORT_TYPE_ENVIRONMENT]: models.environment,
[EXPORT_TYPE_API_SPEC]: models.apiSpec, [EXPORT_TYPE_API_SPEC]: models.apiSpec,
[EXPORT_TYPE_PROTO_FILE]: models.protoFile,
}; };
export type ImportResult = { export type ImportResult = {
@ -197,7 +202,7 @@ export async function importRaw(
// Hack to switch to GraphQL based on finding `graphql` in the URL path // Hack to switch to GraphQL based on finding `graphql` in the URL path
// TODO: Support this in a better way // TODO: Support this in a better way
if ( if (
model.type === models.request.type && isRequest(model) &&
resource.body && resource.body &&
typeof resource.body.text === 'string' && typeof resource.body.text === 'string' &&
typeof resource.url === 'string' && typeof resource.url === 'string' &&
@ -209,7 +214,7 @@ export async function importRaw(
// Try adding Content-Type JSON if no Content-Type exists // Try adding Content-Type JSON if no Content-Type exists
if ( if (
model.type === models.request.type && isRequest(model) &&
resource.body && resource.body &&
typeof resource.body.text === 'string' && typeof resource.body.text === 'string' &&
Array.isArray(resource.headers) && Array.isArray(resource.headers) &&
@ -278,7 +283,7 @@ export async function exportWorkspacesHAR(
includePrivateDocs: boolean = false, includePrivateDocs: boolean = false,
): Promise<string> { ): Promise<string> {
const docs: Array<BaseModel> = await getDocWithDescendants(parentDoc, includePrivateDocs); const docs: Array<BaseModel> = await getDocWithDescendants(parentDoc, includePrivateDocs);
const requests: Array<BaseModel> = docs.filter(doc => doc.type === models.request.type); const requests: Array<BaseModel> = docs.filter(isRequest);
return exportRequestsHAR(requests, includePrivateDocs); return exportRequestsHAR(requests, includePrivateDocs);
} }
@ -342,7 +347,7 @@ export async function exportWorkspacesData(
format: 'json' | 'yaml', format: 'json' | 'yaml',
): Promise<string> { ): Promise<string> {
const docs: Array<BaseModel> = await getDocWithDescendants(parentDoc, includePrivateDocs); const docs: Array<BaseModel> = await getDocWithDescendants(parentDoc, includePrivateDocs);
const requests: Array<BaseModel> = docs.filter(doc => doc.type === models.request.type); const requests: Array<BaseModel> = docs.filter(doc => isRequest(doc) || isGrpcRequest(doc));
return exportRequestsData(requests, includePrivateDocs, format); return exportRequestsData(requests, includePrivateDocs, format);
} }
@ -385,7 +390,8 @@ export async function exportRequestsData(
d.type === models.environment.type || d.type === models.environment.type ||
d.type === models.apiSpec.type || d.type === models.apiSpec.type ||
d.type === models.unitTestSuite.type || d.type === models.unitTestSuite.type ||
d.type === models.unitTest.type d.type === models.unitTest.type ||
isProtoFile(d)
); );
}); });
docs.push(...descendants); docs.push(...descendants);
@ -398,8 +404,10 @@ export async function exportRequestsData(
!( !(
d.type === models.unitTestSuite.type || d.type === models.unitTestSuite.type ||
d.type === models.unitTest.type || d.type === models.unitTest.type ||
d.type === models.request.type || isRequest(d) ||
d.type === models.requestGroup.type || isGrpcRequest(d) ||
isRequestGroup(d) ||
isProtoFile(d) ||
d.type === models.workspace.type || d.type === models.workspace.type ||
d.type === models.cookieJar.type || d.type === models.cookieJar.type ||
d.type === models.environment.type || d.type === models.environment.type ||
@ -422,10 +430,14 @@ export async function exportRequestsData(
d._type = EXPORT_TYPE_UNIT_TEST_SUITE; d._type = EXPORT_TYPE_UNIT_TEST_SUITE;
} else if (d.type === models.unitTest.type) { } else if (d.type === models.unitTest.type) {
d._type = EXPORT_TYPE_UNIT_TEST; d._type = EXPORT_TYPE_UNIT_TEST;
} else if (d.type === models.requestGroup.type) { } else if (isRequestGroup(d)) {
d._type = EXPORT_TYPE_REQUEST_GROUP; d._type = EXPORT_TYPE_REQUEST_GROUP;
} else if (d.type === models.request.type) { } else if (isRequest(d)) {
d._type = EXPORT_TYPE_REQUEST; d._type = EXPORT_TYPE_REQUEST;
} else if (isGrpcRequest(d)) {
d._type = EXPORT_TYPE_GRPC_REQUEST;
} else if (isProtoFile(d)) {
d._type = EXPORT_TYPE_PROTO_FILE;
} else if (d.type === models.apiSpec.type) { } else if (d.type === models.apiSpec.type) {
d._type = EXPORT_TYPE_API_SPEC; d._type = EXPORT_TYPE_API_SPEC;
} }

View File

@ -3,6 +3,7 @@ import { difference } from 'lodash';
import { import {
isGrpcRequest, isGrpcRequest,
isGrpcRequestId, isGrpcRequestId,
isProtoFile,
isRequest, isRequest,
isRequestGroup, isRequestGroup,
isRequestId, isRequestId,
@ -76,3 +77,16 @@ describe('isRequestGroup', () => {
expect(isRequestGroup({ type })).toBe(false); expect(isRequestGroup({ type })).toBe(false);
}); });
}); });
describe('isProtoFile', () => {
const supported = [models.protoFile.type];
const unsupported = difference(allTypes, supported);
it.each(supported)('should return true: "%s"', type => {
expect(isProtoFile({ type })).toBe(true);
});
it.each(unsupported)('should return false: "%s"', type => {
expect(isProtoFile({ type })).toBe(false);
});
});

View File

@ -1,6 +1,6 @@
// @flow // @flow
import type { BaseModel } from '../index'; import type { BaseModel } from '../index';
import { grpcRequest, request, requestGroup } from '../index'; import { grpcRequest, request, requestGroup, protoFile } from '../index';
export function isGrpcRequest(obj: BaseModel): boolean { export function isGrpcRequest(obj: BaseModel): boolean {
return obj.type === grpcRequest.type; return obj.type === grpcRequest.type;
@ -21,3 +21,7 @@ export function isRequestId(id: string): boolean {
export function isRequestGroup(obj: BaseModel): boolean { export function isRequestGroup(obj: BaseModel): boolean {
return obj.type === requestGroup.type; return obj.type === requestGroup.type;
} }
export function isProtoFile(obj: BaseModel): boolean {
return obj.type === protoFile.type;
}

View File

@ -1,6 +1,13 @@
// @flow // @flow
import * as models from '../index'; import * as models from '../index';
import { isGrpcRequest } from './is-model'; import { isGrpcRequest, isGrpcRequestId } from './is-model';
import type { GrpcRequest } from '../grpc-request';
export function getById(requestId: string): Promise<Request | GrpcRequest | null> {
return isGrpcRequestId(requestId)
? models.grpcRequest.getById(requestId)
: models.request.getById(requestId);
}
export function remove<T>(request: T): Promise<void> { export function remove<T>(request: T): Promise<void> {
return isGrpcRequest(request) return isGrpcRequest(request)

View File

@ -3,11 +3,14 @@ import React, { PureComponent } from 'react';
import autobind from 'autobind-decorator'; import autobind from 'autobind-decorator';
import MethodTag from '../tags/method-tag'; import MethodTag from '../tags/method-tag';
import type { Request } from '../../../models/request'; import type { Request } from '../../../models/request';
import type { GrpcRequest } from '../../../models/grpc-request';
import { isGrpcRequest } from '../../../models/helpers/is-model';
import GrpcTag from '../tags/grpc-tag';
type Props = { type Props = {
handleSetItemSelected: Function, handleSetItemSelected: Function,
isSelected: boolean, isSelected: boolean,
request: Request, request: Request | GrpcRequest,
}; };
@autobind @autobind
@ -21,6 +24,8 @@ class RequestRow extends PureComponent<Props> {
render() { render() {
const { request, isSelected } = this.props; const { request, isSelected } = this.props;
const isGrpc = isGrpcRequest(request);
return ( return (
<li className="tree__row"> <li className="tree__row">
<div className="tree__item tree__item--request"> <div className="tree__item tree__item--request">
@ -28,7 +33,7 @@ class RequestRow extends PureComponent<Props> {
<input type="checkbox" checked={isSelected} onChange={this.handleSelect} /> <input type="checkbox" checked={isSelected} onChange={this.handleSelect} />
</div> </div>
<button className="wide"> <button className="wide">
<MethodTag method={request.method} /> {isGrpc ? <GrpcTag /> : <MethodTag method={request.method} />}
<span className="inline-block">{request.name}</span> <span className="inline-block">{request.name}</span>
</button> </button>
</div> </div>

View File

@ -2,10 +2,11 @@
import * as React from 'react'; import * as React from 'react';
import RequestRow from './request-row'; import RequestRow from './request-row';
import RequestGroupRow from './request-group-row'; import RequestGroupRow from './request-group-row';
import * as models from '../../../models/index';
import type { Node } from '../modals/export-requests-modal'; import type { Node } from '../modals/export-requests-modal';
import type { Request } from '../../../models/request'; import type { Request } from '../../../models/request';
import type { RequestGroup } from '../../../models/request-group'; import type { RequestGroup } from '../../../models/request-group';
import { isGrpcRequest, isRequest } from '../../../models/helpers/is-model';
import type { GrpcRequest } from '../../../models/grpc-request';
type Props = { type Props = {
root: ?Node, root: ?Node,
@ -19,9 +20,9 @@ class Tree extends React.PureComponent<Props> {
return null; return null;
} }
if (node.doc.type === models.request.type) { if (isRequest(node.doc) || isGrpcRequest(node.doc)) {
// Directly cast to Request will result in error, so cast it to any first. // Directly casting will result in error, so cast it to any first.
const request: Request = ((node.doc: any): Request); const request: Request | GrpcRequest = ((node.doc: any): Request | GrpcRequest);
return ( return (
<RequestRow <RequestRow
key={node.doc._id} key={node.doc._id}

View File

@ -9,9 +9,11 @@ import Tree from '../export-requests/tree';
import type { Request } from '../../../models/request'; import type { Request } from '../../../models/request';
import type { RequestGroup } from '../../../models/request-group'; import type { RequestGroup } from '../../../models/request-group';
import * as models from '../../../models'; import * as models from '../../../models';
import type { GrpcRequest } from '../../../models/grpc-request';
import { isGrpcRequest, isRequest, isRequestGroup } from '../../../models/helpers/is-model';
export type Node = { export type Node = {
doc: Request | RequestGroup, doc: Request | GrpcRequest | RequestGroup,
children: Array<Node>, children: Array<Node>,
collapsed: boolean, collapsed: boolean,
totalRequests: number, totalRequests: number,
@ -64,7 +66,8 @@ class ExportRequestsModal extends PureComponent<Props, State> {
} }
getSelectedRequestIds(node: Node): Array<string> { getSelectedRequestIds(node: Node): Array<string> {
if (node.doc.type === models.request.type && node.selectedRequests === node.totalRequests) { const docIsRequest = isRequest(node.doc) || isGrpcRequest(node.doc);
if (docIsRequest && node.selectedRequests === node.totalRequests) {
return [node.doc._id]; return [node.doc._id];
} }
const requestIds: Array<string> = []; const requestIds: Array<string> = [];
@ -105,7 +108,9 @@ class ExportRequestsModal extends PureComponent<Props, State> {
let totalRequests = children let totalRequests = children
.map(child => child.totalRequests) .map(child => child.totalRequests)
.reduce((acc, totalRequests) => acc + totalRequests, 0); .reduce((acc, totalRequests) => acc + totalRequests, 0);
if (item.doc.type === models.request.type) {
const docIsRequest = isRequest(item.doc) || isGrpcRequest(item.doc);
if (docIsRequest) {
totalRequests++; totalRequests++;
} }
return { return {
@ -146,7 +151,7 @@ class ExportRequestsModal extends PureComponent<Props, State> {
} }
setRequestGroupCollapsed(node: Node, isCollapsed: boolean, requestGroupId: string): boolean { setRequestGroupCollapsed(node: Node, isCollapsed: boolean, requestGroupId: string): boolean {
if (node.doc.type !== models.requestGroup.type) { if (!isRequestGroup(node.doc)) {
return false; return false;
} }
if (node.doc._id === requestGroupId) { if (node.doc._id === requestGroupId) {

View File

@ -13,6 +13,7 @@ import AlertModal from '../../components/modals/alert-modal';
import PaymentNotificationModal from '../../components/modals/payment-notification-modal'; import PaymentNotificationModal from '../../components/modals/payment-notification-modal';
import LoginModal from '../../components/modals/login-modal'; import LoginModal from '../../components/modals/login-modal';
import * as models from '../../../models'; import * as models from '../../../models';
import * as requestOperations from '../../../models/helpers/request-operations';
import SelectModal from '../../components/modals/select-modal'; import SelectModal from '../../components/modals/select-modal';
import { showError, showModal } from '../../components/modals/index'; import { showError, showModal } from '../../components/modals/index';
import * as db from '../../../common/database'; import * as db from '../../../common/database';
@ -535,7 +536,7 @@ export function exportRequestsToFile(requestIds) {
const privateEnvironments = []; const privateEnvironments = [];
const workspaceLookup = {}; const workspaceLookup = {};
for (const requestId of requestIds) { for (const requestId of requestIds) {
const request = await models.request.getById(requestId); const request = await requestOperations.getById(requestId);
if (request == null) { if (request == null) {
continue; continue;
} }