Updated import screen, better curl paste, and better model defaults

This commit is contained in:
Gregory Schier 2016-11-18 23:11:10 -08:00
parent e9c7aff251
commit 0cb7266599
16 changed files with 87 additions and 297 deletions

View File

@ -1,6 +1,6 @@
import * as harUtils from '../har';
import * as db from '../../common/database';
import * as render from '../../common/render';
import * as db from '../database';
import * as render from '../render';
import * as models from '../../models';
describe('exportHarWithRequest()', () => {

View File

@ -208,3 +208,16 @@ describe('debounce()', () => {
expect(resultList).toEqual([['foo', 'bar3']]);
})
});
describe('strictObjectAssign()', () => {
it('handles assignment', () => {
const actual = misc.strictObjectAssign(
{foo: 'hi', bar: {baz: 'qux'}},
{foo: 'hi again', a: 'b'},
{a: 'c', foo: 'final foo'},
);
expect(actual).toEqual({
foo: 'final foo', bar: {baz: 'qux'}
});
})
});

View File

@ -4,6 +4,7 @@ import fsPath from 'path';
import {DB_PERSIST_INTERVAL} from './constants';
import {generateId} from './misc';
import {getModel, initModel} from '../models';
import * as misc from './misc';
export const CHANGE_INSERT = 'insert';
export const CHANGE_UPDATE = 'update';
@ -128,7 +129,7 @@ export function find (type, query = {}) {
const modelDefaults = initModel(type);
const docs = rawDocs.map(rawDoc => {
return Object.assign({}, modelDefaults, rawDoc);
return Object.assign(modelDefaults, rawDoc);
});
resolve(docs);
@ -153,7 +154,7 @@ export function getWhere (type, query) {
}
const modelDefaults = initModel(type);
resolve(Object.assign({}, modelDefaults, rawDocs[0]));
resolve(Object.assign(modelDefaults, rawDocs[0]));
})
})
}
@ -245,11 +246,11 @@ export function removeBulkSilently (type, query) {
// ~~~~~~~~~~~~~~~~~~~ //
export function docUpdate (originalDoc, patch = {}) {
const doc = Object.assign(
const doc = misc.strictObjectAssign(
initModel(originalDoc.type),
originalDoc,
patch,
{modified: Date.now()}
{modified: Date.now()},
);
return update(doc);
@ -262,9 +263,9 @@ export function docCreate (type, patch = {}) {
throw new Error(`No ID prefix for ${type}`)
}
const doc = Object.assign(
{_id: generateId(idPrefix)},
const doc = misc.strictObjectAssign(
initModel(type),
{_id: generateId(idPrefix)},
patch,
// Fields that the user can't touch

View File

@ -1,7 +1,7 @@
import * as models from '../models';
import {getRenderedRequest} from '../common/render';
import {jarFromCookies} from '../common/cookies';
import * as util from '../common/misc';
import {getRenderedRequest} from './render';
import {jarFromCookies} from './cookies';
import * as util from './misc';
export function exportHarWithRequest (renderedRequest, addContentLength = false) {
if (addContentLength) {

View File

@ -1,8 +1,8 @@
import * as importers from 'insomnia-importers';
import * as db from '../common/database';
import * as db from './database';
import * as models from '../models';
import {getAppVersion} from '../common/constants';
import * as misc from '../common/misc';
import {getAppVersion} from './constants';
import * as misc from './misc';
const EXPORT_TYPE_REQUEST = 'request';
const EXPORT_TYPE_REQUEST_GROUP = 'request_group';
@ -21,10 +21,10 @@ const MODELS = {
[EXPORT_TYPE_ENVIRONMENT]: models.environment,
};
export async function importJSON (workspace, json, generateNewIds = false) {
export async function importRaw (workspace, rawContent, generateNewIds = false) {
let data;
try {
data = importers.import(json);
data = importers.import(rawContent);
} catch (e) {
console.error('Failed to import data', e);
return;

View File

@ -149,3 +149,15 @@ export function debounce (callback, millis = DEBOUNCE_MILLIS) {
callback.apply(null, results['__key__'])
}, millis).bind(null, '__key__');
}
/** Same as Object.assign but only add fields that exist on target */
export function strictObjectAssign (target, ...sources) {
const patch = Object.assign(...sources);
for (const key of Object.keys(target)) {
if (patch.hasOwnProperty(key)) {
target[key] = patch[key];
}
}
return target;
}

View File

@ -1,195 +0,0 @@
const FLAGS = [
'cacert', 'capath', 'E', 'cert', 'cert-type', 'ciphers', 'K', 'config',
'connect-timeout', 'C', 'continue-at', 'b',
'cookie', // TODO: Handle this
'c', 'cookie-jar', 'crlfile', 'd', 'data', 'data-ascii', 'data-binary',
'data-urlencode', 'delegation', 'D', 'dump-header', 'egd-file',
'engine', 'F', 'form', 'form-string', 'ftp-account', 'ftp-method',
'ftp-port', 'H', 'header', 'hostpubmd5', 'interface', 'keepalive-time',
'key', 'key-type', 'krb', 'lib-curl', 'limit-rate', 'local-port',
'mail-from', 'mail-rcpt', 'mail-auth', 'max-filesize', 'max-redirs',
'max-time', 'netrtc-file', 'output', 'pass', 'proto', 'proto-redir',
'proxy', 'proxy-user', 'proxy1.0', 'pubkey', 'Q', 'quote',
'random-file', 'range', 'X', 'request', 'resolve', 'retry',
'retry-delay', 'retry-max-time', 'socks4', 'socks4a', 'socks5',
'socks5-hostname', 'socks5-gssapi-service', 'Y', 'speed-limit',
'y', 'speed-time', 'stderr', 'tftp-blksize', 'z', 'time-cond', 'trace',
'trace-ascii', 'T', 'upload-file',
'url',
'u', 'user', 'tlsuser', 'tlspassword', 'tlsauthtype',
'A', 'user-agent', // TODO: Handle this
'w', 'write-out'
];
const FLAG_REGEXES = [
/\s--([\w\-]{2,})\s+"((?:[^"\\]|\\.)*)"/, // --my-flag "hello"
/\s--([\w\-]{2,})\s+'((?:[^'\\]|\\.)*)'/, // --my-flag 'hello'
/\s--([\w\-]{2,})\s+([^\s]+)/, // --my-flag hello
/\s-([\w])\s*'((?:[^'\\]|\\.)*)'/, // -X 'hello'
/\s-([\w])\s*"((?:[^"\\]|\\.)*)"/, // -X "hello"
/\s-([\w])\s*([^\s]+)/, // -X hello
/\s--([\w\-]+)/ // --switch (cleanup at the end)
];
function getFlags (cmd) {
const flags = {};
const switches = {};
for (var i = 0; i < FLAG_REGEXES.length; i++) {
var matches = [];
var match;
var key;
var val;
var matchedFlag;
// Stop at 1000 to prevent infinite loops
// TODO: Make this recursive
for (var j = 0; (matches = FLAG_REGEXES[i].exec(cmd)); j++) {
if (j > 1000) {
console.error('INFINITE LOOP');
break;
}
match = matches[0];
key = matches[1];
val = matches[2];
matchedFlag = !!val;
if (matchedFlag) {
if (isFlag(key)) {
// Matched a flag
flags[key] = flags[key] || [];
flags[key].push(val);
cmd = cmd.replace(match, '');
} else {
// Matched a flag that was actually a switch
cmd = cmd.replace('--' + key, '');
}
} else {
// Matched a switch directly without a value
switches[key] = true;
cmd = cmd.replace(match, '');
}
}
}
return {cmd, flags, switches};
}
function isFlag (key) {
for (var i = 0; i < FLAGS.length; i++) {
if (key === FLAGS[i]) {
return true;
}
}
return false;
}
function splitHeaders (flags) {
var headers = (flags['H'] || []).concat(flags['header'] || []);
var parsed = [];
for (var i = 0; i < headers.length; i++) {
var header = headers[i].split(':');
var name = header[0].trim();
var value = header[1].trim();
parsed.push({name: name, value: value});
}
return parsed;
}
function getContentType (headers) {
for (var i = 0; i < headers.length; i++) {
var header = headers[i];
if (header.name.toLowerCase() === 'content-type') {
return header.value;
}
}
return null;
}
function getHttpMethod (flags, hasBody) {
var method = (flags['X'] || flags['request'] || [])[0];
// If there is no method specified, but there is a body, default
// to POST, else default to GET
if (!method) {
method = hasBody ? 'POST' : 'GET';
}
return method;
}
function getBasicAuth (flags) {
var authString = flags.u || flags.user;
var auth = {
username: '',
password: ''
};
if (authString) {
var authSplit = authString[0].split(':');
auth.username = (authSplit[0] || '').trim();
auth.password = (authSplit[1] || '').trim();
}
return auth;
}
export function importCurl (blob) {
if (!blob || blob.toLowerCase().indexOf('curl ') !== 0) {
return false;
}
// Rip out the flags
let {cmd, flags, switches} = getFlags(blob);
// Final values
const headers = splitHeaders(flags);
let body = (
flags.d ||
flags.data ||
flags['data-binary'] ||
flags['data-ascii'] ||
[]
)[0] || '';
const contentType = getContentType(headers) || null;
if (contentType && contentType.toLowerCase() === 'application/json') {
try {
body = JSON.stringify(JSON.parse(body), null, '\t');
} catch (e) {
}
}
const httpMethod = getHttpMethod(flags, !!body);
const authentication = getBasicAuth(flags);
// Clean up the remaining URL
cmd = (cmd + ' ')
.replace(/\\ /g, ' ').replace(/ \\/g, ' ') // slashes
.replace(/\s/g, ' ') // whitespaces
.replace(/ "/g, ' ').replace(/" /g, ' ') // double-quotes
.replace(/ '/g, ' ').replace(/' /g, ' '); // single-quotes
let url;
if (flags.url && flags.url.length) {
url = flags.url[0];
} else {
url = /curl\s+['"]?((?!('|")).*)['"]?/.exec(cmd)[1].trim();
}
return {
url: url,
body: body,
headers: headers,
contentType: contentType,
method: httpMethod,
authentication: authentication
};
}

View File

@ -1,66 +0,0 @@
import * as models from '../models';
import {getContentTypeFromHeaders} from '../common/constants';
const FORMAT_MAP = {
json: 'application/json',
xml: 'application/xml',
form: 'application/x-www-form-urlencoded',
text: 'text/plain'
};
export async function importRequestGroupLegacy (importedRequestGroup, parentId, index = 1) {
const requestGroup = await models.requestGroup.create({
parentId,
name: importedRequestGroup.name,
environment: (importedRequestGroup.environments || {}).base || {},
metaCollapsed: true,
metaSortKey: index * 1000
});
// Sometimes (maybe all the time, I can't remember) requests will be nested
if (importedRequestGroup.hasOwnProperty('requests')) {
// Let's process them oldest to newest
importedRequestGroup.requests.map(
(r, i) => importRequestLegacy(r, requestGroup._id, index * 1000 + i)
);
}
}
export function importRequestLegacy (importedRequest, parentId, index = 1) {
let auth = {};
if (importedRequest.authentication.username) {
auth = {
username: importedRequest.authentication.username,
password: importedRequest.authentication.password
}
}
// Add the content type header
const headers = importedRequest.headers || [];
const contentType = getContentTypeFromHeaders(headers);
if (!contentType) {
const derivedContentType = FORMAT_MAP[importedRequest.__insomnia.format];
if (derivedContentType) {
headers.push({
name: 'Content-Type',
value: FORMAT_MAP[importedRequest.__insomnia.format]
});
}
}
models.request.create({
parentId,
_id: importedRequest._id,
name: importedRequest.name,
url: importedRequest.url,
method: importedRequest.method,
body: importedRequest.body,
headers: headers,
parameters: importedRequest.params || [],
metaSortKey: index * 1000,
contentType: FORMAT_MAP[importedRequest.__insomnia.format] || 'text/plain',
authentication: auth
});
}

View File

@ -42,6 +42,8 @@ export function getModel (type) {
export function initModel (type) {
const baseDefaults = {
_id: null,
type,
modified: Date.now(),
created: Date.now(),
parentId: null

View File

@ -16,7 +16,7 @@
"electron-squirrel-startup": "^1.0.0",
"hkdf": "0.0.2",
"httpsnippet": "git@github.com:getinsomnia/httpsnippet.git#a3a2c0a0167fa844bf92df52a1442fa1d68a9053",
"insomnia-importers": "^0.1.3",
"insomnia-importers": "^0.2.0",
"json-lint": "^0.1.0",
"jsonpath-plus": "^0.15.0",
"mime-types": "^2.1.12",

View File

@ -8,7 +8,7 @@ import Modal from '../base/Modal';
import ModalBody from '../base/ModalBody';
import ModalHeader from '../base/ModalHeader';
import ModalFooter from '../base/ModalFooter';
import {exportHar} from '../../../export/har';
import {exportHar} from '../../../common/har';
const DEFAULT_TARGET = availableTargets().find(t => t.key === 'shell');
const DEFAULT_CLIENT = DEFAULT_TARGET.clients.find(t => t.key === 'curl');

View File

@ -9,8 +9,7 @@ const SettingsImportExport = ({
<div className="pad">
<h1>Data Import and Export</h1>
<p>
Be aware that you may be exporting <strong>private data</strong>.
Also, any imported data may overwrite existing data.
Import format will be automatically detected (<strong>Insomnia, Postman, HAR, cURL</strong>)
</p>
<Dropdown outline={true}>
<DropdownButton className="btn btn--super-compact btn--outlined">
@ -30,9 +29,9 @@ const SettingsImportExport = ({
<button className="btn btn--super-compact btn--outlined" onClick={e => handleImport()}>
Import Data
</button>
{/*<p className="faint txt-sm">*/}
{/** Tip: You can also paste Curl commands into the URL bar*/}
{/*</p>*/}
<p className="italic faint pad-top">
* Tip: You can also paste Curl commands into the URL bar
</p>
</div>
);

View File

@ -1,6 +1,7 @@
import React, {Component, PropTypes} from 'react';
import {ipcRenderer} from 'electron';
import ReactDOM from 'react-dom';
import * as importers from 'insomnia-importers';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import HTML5Backend from 'react-dnd-html5-backend';
@ -22,7 +23,7 @@ import * as requestMetaActions from '../redux/modules/requestMeta';
import * as requestGroupMetaActions from '../redux/modules/requestGroupMeta';
import * as db from '../../common/database';
import * as models from '../../models';
import {importCurl} from '../../export/curl';
import {importRaw} from '../../common/import';
import {trackEvent, trackLegacyEvent} from '../../analytics';
import {PREVIEW_MODE_SOURCE} from '../../common/constants';
@ -130,14 +131,36 @@ class App extends Component {
}
async _handleUrlChanged (request, url) {
// TODO: Should this be moved elsewhere?
const requestPatch = importCurl(url);
if (requestPatch) {
await models.request.update(request, requestPatch);
this._forceHardRefresh();
} else {
models.request.update(request, {url});
// Allow user to paste any import file into the url. If it results in
// only one item, it will overwrite the current request.
try {
const {resources} = importers.import(url);
const r = resources[0];
if (r && r._type === 'request') {
const cookieHeaders = r.cookies.map(({name, value}) => (
{name: 'cookie', value: `${name}=${value}`}
));
// Only pull fields that we want to update
await models.request.update(request, {
url: r.url,
method: r.method,
headers: [...r.headers, ...cookieHeaders],
body: r.body,
authentication: r.authentication,
parameters: r.parameters,
});
this._forceHardRefresh();
return;
}
} catch (e) {
// Import failed, that's alright
}
models.request.update(request, {url});
}
_startDragSidebar () {

View File

@ -102,6 +102,7 @@ code, pre, .monospace {
}
.notice {
text-align: center;
color: @font-light-bg !important;
padding: @padding-sm;
border-radius: @radius-md;

View File

@ -2,10 +2,10 @@ import electron from 'electron';
import {combineReducers} from 'redux';
import fs from 'fs';
import {importJSON, exportJSON} from '../../../export/insomnia';
import {importRaw, exportJSON} from '../../../common/import';
import {trackEvent} from '../../../analytics';
import AlertModal from '../../components/modals/AlertModal';
import {showModal} from '../../components/modals/index';
import {showModal} from '../../components/modals';
import PaymentNotificationModal from '../../components/modals/PaymentNotificationModal';
import LoginModal from '../../components/modals/LoginModal';
import * as models from '../../../models';
@ -135,7 +135,7 @@ export function importFile (workspaceId) {
properties: ['openFile'],
filters: [{
// Allow empty extension and JSON
name: 'Insomnia Import', extensions: ['', 'json']
name: 'Insomnia Import', extensions: ['', 'sh', 'txt', 'json']
}]
};
@ -158,7 +158,7 @@ export function importFile (workspaceId) {
return;
}
importJSON(workspace, data);
importRaw(workspace, data);
trackEvent('Import', 'Success');
});
})

View File

@ -85,7 +85,7 @@
"electron-squirrel-startup": "^1.0.0",
"hkdf": "0.0.2",
"httpsnippet": "git@github.com:getinsomnia/httpsnippet.git#a3a2c0a0167fa844bf92df52a1442fa1d68a9053",
"insomnia-importers": "^0.1.3",
"insomnia-importers": "^0.2.0",
"json-lint": "^0.1.0",
"jsonpath-plus": "^0.15.0",
"mime-types": "^2.1.12",