mirror of
https://github.com/Kong/insomnia
synced 2024-11-07 22:30:15 +00:00
Updated import screen, better curl paste, and better model defaults
This commit is contained in:
parent
e9c7aff251
commit
0cb7266599
@ -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()', () => {
|
@ -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'}
|
||||
});
|
||||
})
|
||||
});
|
||||
|
@ -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
|
||||
|
@ -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) {
|
@ -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;
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
};
|
||||
}
|
@ -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
|
||||
});
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
|
@ -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');
|
||||
|
@ -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>
|
||||
);
|
||||
|
||||
|
@ -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 () {
|
||||
|
@ -102,6 +102,7 @@ code, pre, .monospace {
|
||||
}
|
||||
|
||||
.notice {
|
||||
text-align: center;
|
||||
color: @font-light-bg !important;
|
||||
padding: @padding-sm;
|
||||
border-radius: @radius-md;
|
||||
|
@ -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');
|
||||
});
|
||||
})
|
||||
|
@ -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",
|
||||
|
Loading…
Reference in New Issue
Block a user