mirror of
https://github.com/Kong/insomnia.git
synced 2026-05-19 06:12:37 -04:00
Merge branch 'master' of github.com:gschier/insomnia
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import * as models from '../../models';
|
||||
|
||||
export default {
|
||||
export const data = {
|
||||
[models.workspace.type]: [{
|
||||
_id: 'wrk_1',
|
||||
name: 'Wrk 1'
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import * as db from '../database';
|
||||
import * as models from '../../models';
|
||||
import * as db from '../database';
|
||||
|
||||
function loadFixture (name) {
|
||||
const fixtures = require(`../__fixtures__/${name}`);
|
||||
const fixtures = require(`../__fixtures__/${name}`).data;
|
||||
const promises = [];
|
||||
for (const type of Object.keys(fixtures)) {
|
||||
for (const doc of fixtures[type]) {
|
||||
promises.push(db.insert(Object.assign({}, doc, {type})));
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
describe('init()', () => {
|
||||
@@ -86,9 +88,7 @@ describe('bufferChanges()', () => {
|
||||
});
|
||||
|
||||
describe('requestCreate()', () => {
|
||||
beforeEach(() => {
|
||||
return db.init(models.types(), {inMemoryOnly: true}, true);
|
||||
});
|
||||
beforeEach(() => db.init(models.types(), {inMemoryOnly: true}, true));
|
||||
|
||||
it('creates a valid request', async () => {
|
||||
const now = Date.now();
|
||||
|
||||
@@ -4,29 +4,33 @@ import * as models from '../../models';
|
||||
jest.mock('electron');
|
||||
|
||||
describe('render()', () => {
|
||||
it('renders hello world', () => {
|
||||
const rendered = renderUtils.render('Hello {{ msg }}!', {msg: 'World'});
|
||||
it('renders hello world', async () => {
|
||||
const rendered = await renderUtils.render('Hello {{ msg }}!', {msg: 'World'});
|
||||
expect(rendered).toBe('Hello World!');
|
||||
});
|
||||
|
||||
it('renders custom tag: uuid', () => {
|
||||
const rendered = renderUtils.render('Hello {% uuid %}!');
|
||||
it('renders custom tag: uuid', async () => {
|
||||
const rendered = await renderUtils.render('Hello {% uuid %}!');
|
||||
expect(rendered).toMatch(/Hello [a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}!/);
|
||||
});
|
||||
|
||||
it('renders custom tag: timestamp', () => {
|
||||
const rendered = renderUtils.render('Hello {% timestamp %}!');
|
||||
it('renders custom tag: timestamp', async () => {
|
||||
const rendered = await renderUtils.render('Hello {% timestamp %}!');
|
||||
expect(rendered).toMatch(/Hello \d{13}!/);
|
||||
});
|
||||
|
||||
it('fails on invalid template', () => {
|
||||
const fn = () => renderUtils.render('Hello {{ msg }!', {msg: 'World'});
|
||||
expect(fn).toThrowError('expected variable end');
|
||||
it('fails on invalid template', async () => {
|
||||
try {
|
||||
await renderUtils.render('Hello {{ msg }!', {msg: 'World'});
|
||||
fail('Render should have failed');
|
||||
} catch (err) {
|
||||
expect(err.message).toContain('expected variable end');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildRenderContext()', () => {
|
||||
it('cascades properly', () => {
|
||||
it('cascades properly', async () => {
|
||||
const ancestors = [
|
||||
{
|
||||
type: models.requestGroup.type,
|
||||
@@ -48,7 +52,7 @@ describe('buildRenderContext()', () => {
|
||||
data: {foo: 'sub', sub: true}
|
||||
};
|
||||
|
||||
const context = renderUtils.buildRenderContext(
|
||||
const context = await renderUtils.buildRenderContext(
|
||||
ancestors,
|
||||
rootEnvironment,
|
||||
subEnvironment
|
||||
@@ -62,14 +66,14 @@ describe('buildRenderContext()', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('rendered recursive should not infinite loop', () => {
|
||||
it('rendered recursive should not infinite loop', async () => {
|
||||
const ancestors = [{
|
||||
// Sub Environment
|
||||
type: models.requestGroup.type,
|
||||
environment: {recursive: '{{ recursive }}/hello'}
|
||||
}];
|
||||
|
||||
const context = renderUtils.buildRenderContext(ancestors);
|
||||
const context = await renderUtils.buildRenderContext(ancestors);
|
||||
|
||||
// This is longer than 3 because it multiplies every time (1 -> 2 -> 4 -> 8)
|
||||
expect(context).toEqual({
|
||||
@@ -77,7 +81,7 @@ describe('buildRenderContext()', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('render up to 3 recursion levels', () => {
|
||||
it('render up to 3 recursion levels', async () => {
|
||||
const ancestors = [{
|
||||
// Sub Environment
|
||||
type: models.requestGroup.type,
|
||||
@@ -90,7 +94,7 @@ describe('buildRenderContext()', () => {
|
||||
}
|
||||
}];
|
||||
|
||||
const context = renderUtils.buildRenderContext(ancestors);
|
||||
const context = await renderUtils.buildRenderContext(ancestors);
|
||||
|
||||
expect(context).toEqual({
|
||||
d: '/d',
|
||||
@@ -101,7 +105,7 @@ describe('buildRenderContext()', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('rendered sibling environment variables', () => {
|
||||
it('rendered sibling environment variables', async () => {
|
||||
const ancestors = [{
|
||||
// Sub Environment
|
||||
type: models.requestGroup.type,
|
||||
@@ -111,12 +115,12 @@ describe('buildRenderContext()', () => {
|
||||
}
|
||||
}];
|
||||
|
||||
const context = renderUtils.buildRenderContext(ancestors);
|
||||
const context = await renderUtils.buildRenderContext(ancestors);
|
||||
|
||||
expect(context).toEqual({sibling: 'sibling', test: 'sibling/hello'});
|
||||
});
|
||||
|
||||
it('rendered parent environment variables', () => {
|
||||
it('rendered parent environment variables', async () => {
|
||||
const ancestors = [{
|
||||
name: 'Parent',
|
||||
type: models.requestGroup.type,
|
||||
@@ -131,12 +135,12 @@ describe('buildRenderContext()', () => {
|
||||
}
|
||||
}];
|
||||
|
||||
const context = renderUtils.buildRenderContext(ancestors);
|
||||
const context = await renderUtils.buildRenderContext(ancestors);
|
||||
|
||||
expect(context).toEqual({grandparent: 'grandparent', test: 'grandparent parent'});
|
||||
});
|
||||
|
||||
it('rendered parent same name environment variables', () => {
|
||||
it('rendered parent same name environment variables', async () => {
|
||||
const ancestors = [{
|
||||
name: 'Parent',
|
||||
type: models.requestGroup.type,
|
||||
@@ -151,12 +155,12 @@ describe('buildRenderContext()', () => {
|
||||
}
|
||||
}];
|
||||
|
||||
const context = renderUtils.buildRenderContext(ancestors);
|
||||
const context = await renderUtils.buildRenderContext(ancestors);
|
||||
|
||||
expect(context).toEqual({base_url: 'https://insomnia.rest/resource'});
|
||||
});
|
||||
|
||||
it('rendered parent, ignoring sibling environment variables', () => {
|
||||
it('rendered parent, ignoring sibling environment variables', async () => {
|
||||
const ancestors = [{
|
||||
name: 'Parent',
|
||||
type: models.requestGroup.type,
|
||||
@@ -180,13 +184,13 @@ describe('buildRenderContext()', () => {
|
||||
}
|
||||
}];
|
||||
|
||||
const context = renderUtils.buildRenderContext(ancestors);
|
||||
const result = renderUtils.render('{{ urls.admin }}/foo', context);
|
||||
const context = await renderUtils.buildRenderContext(ancestors);
|
||||
const result = await renderUtils.render('{{ urls.admin }}/foo', context);
|
||||
|
||||
expect(result).toEqual('https://parent.com/admin/foo');
|
||||
});
|
||||
|
||||
it('renders child environment variables', () => {
|
||||
it('renders child environment variables', async () => {
|
||||
const ancestors = [{
|
||||
name: 'Parent',
|
||||
type: models.requestGroup.type,
|
||||
@@ -201,12 +205,12 @@ describe('buildRenderContext()', () => {
|
||||
}
|
||||
}];
|
||||
|
||||
const context = renderUtils.buildRenderContext(ancestors);
|
||||
const context = await renderUtils.buildRenderContext(ancestors);
|
||||
|
||||
expect(context).toEqual({parent: 'parent', test: 'parent grandparent'});
|
||||
});
|
||||
|
||||
it('cascades properly and renders', () => {
|
||||
it('cascades properly and renders', async () => {
|
||||
const ancestors = [
|
||||
{
|
||||
type: models.requestGroup.type,
|
||||
@@ -235,7 +239,7 @@ describe('buildRenderContext()', () => {
|
||||
data: {winner: 'root', root: true, base_url: 'ignore this'}
|
||||
};
|
||||
|
||||
const context = renderUtils.buildRenderContext(ancestors,
|
||||
const context = await renderUtils.buildRenderContext(ancestors,
|
||||
rootEnvironment,
|
||||
subEnvironment
|
||||
);
|
||||
@@ -250,12 +254,12 @@ describe('buildRenderContext()', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('works with minimal parameters', () => {
|
||||
it('works with minimal parameters', async () => {
|
||||
const ancestors = null;
|
||||
const rootEnvironment = null;
|
||||
const subEnvironment = null;
|
||||
|
||||
const context = renderUtils.buildRenderContext(
|
||||
const context = await renderUtils.buildRenderContext(
|
||||
ancestors,
|
||||
rootEnvironment,
|
||||
subEnvironment
|
||||
@@ -266,8 +270,8 @@ describe('buildRenderContext()', () => {
|
||||
});
|
||||
|
||||
describe('recursiveRender()', () => {
|
||||
it('correctly renders simple Object', () => {
|
||||
const newObj = renderUtils.recursiveRender({
|
||||
it('correctly renders simple Object', async () => {
|
||||
const newObj = await renderUtils.recursiveRender({
|
||||
foo: '{{ foo }}',
|
||||
bar: 'bar',
|
||||
baz: '{{ bad }}'
|
||||
@@ -280,13 +284,14 @@ describe('recursiveRender()', () => {
|
||||
})
|
||||
});
|
||||
|
||||
it('correctly renders complex Object', () => {
|
||||
it('correctly renders complex Object', async () => {
|
||||
const d = new Date();
|
||||
const obj = {
|
||||
foo: '{{ foo }}',
|
||||
null: null,
|
||||
bool: true,
|
||||
date: d,
|
||||
undef: undefined,
|
||||
num: 1234,
|
||||
nested: {
|
||||
foo: '{{ foo }}',
|
||||
@@ -294,13 +299,14 @@ describe('recursiveRender()', () => {
|
||||
}
|
||||
};
|
||||
|
||||
const newObj = renderUtils.recursiveRender(obj, {foo: 'bar'});
|
||||
const newObj = await renderUtils.recursiveRender(obj, {foo: 'bar'});
|
||||
|
||||
expect(newObj).toEqual({
|
||||
foo: 'bar',
|
||||
null: null,
|
||||
bool: true,
|
||||
date: d,
|
||||
undef: undefined,
|
||||
num: 1234,
|
||||
nested: {
|
||||
foo: 'bar',
|
||||
@@ -314,13 +320,16 @@ describe('recursiveRender()', () => {
|
||||
expect(obj.nested.arr[2]).toBe('{{ foo }}');
|
||||
});
|
||||
|
||||
it('fails on bad template', () => {
|
||||
const fn = () => renderUtils.recursiveRender({
|
||||
foo: '{{ foo }',
|
||||
bar: 'bar',
|
||||
baz: '{{ bad }}'
|
||||
}, {foo: 'bar'});
|
||||
|
||||
expect(fn).toThrowError('expected variable end');
|
||||
it('fails on bad template', async () => {
|
||||
try {
|
||||
await renderUtils.recursiveRender({
|
||||
foo: '{{ foo }',
|
||||
bar: 'bar',
|
||||
baz: '{{ bad }}'
|
||||
}, {foo: 'bar'});
|
||||
fail('Render should have failed');
|
||||
} catch (err) {
|
||||
expect(err.message).toContain('expected variable end');
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
@@ -1,65 +1,14 @@
|
||||
import nunjucks from 'nunjucks';
|
||||
import traverse from 'traverse';
|
||||
import uuid from 'uuid';
|
||||
import clone from 'clone';
|
||||
import * as models from '../models';
|
||||
import {getBasicAuthHeader, hasAuthHeader, setDefaultProtocol} from './misc';
|
||||
import * as db from './database';
|
||||
import * as templating from '../templating';
|
||||
|
||||
const nunjucksEnvironment = nunjucks.configure({
|
||||
autoescape: false
|
||||
});
|
||||
|
||||
class NoArgsExtension {
|
||||
parse (parser, nodes, lexer) {
|
||||
const args = parser.parseSignature(null, true);
|
||||
parser.skip(lexer.TOKEN_BLOCK_END);
|
||||
return new nodes.CallExtension(this, 'run', args);
|
||||
}
|
||||
export async function render (template, context = {}) {
|
||||
return templating.render(template, {context});
|
||||
}
|
||||
|
||||
// class ArgsExtension {
|
||||
// parse (parser, nodes, lexer) {
|
||||
// const tok = parser.nextToken();
|
||||
// const args = parser.parseSignature(null, true);
|
||||
// parser.advanceAfterBlockEnd(tok.value);
|
||||
// return new nodes.CallExtension(this, 'run', args);
|
||||
// }
|
||||
// }
|
||||
|
||||
class TimestampExtension extends NoArgsExtension {
|
||||
constructor () {
|
||||
super();
|
||||
this.tags = ['timestamp'];
|
||||
}
|
||||
|
||||
run (context) {
|
||||
return Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
class UuidExtension extends NoArgsExtension {
|
||||
constructor () {
|
||||
super();
|
||||
this.tags = ['uuid'];
|
||||
}
|
||||
|
||||
run (context) {
|
||||
return uuid.v4();
|
||||
}
|
||||
}
|
||||
|
||||
nunjucksEnvironment.addExtension('uuid', new UuidExtension());
|
||||
nunjucksEnvironment.addExtension('timestamp', new TimestampExtension());
|
||||
|
||||
export function render (template, context = {}) {
|
||||
try {
|
||||
return nunjucksEnvironment.renderString(template, context);
|
||||
} catch (e) {
|
||||
throw new Error(e.message.replace(/\(unknown path\)\s*/, ''));
|
||||
}
|
||||
}
|
||||
|
||||
export function buildRenderContext (ancestors, rootEnvironment, subEnvironment) {
|
||||
export async function buildRenderContext (ancestors, rootEnvironment, subEnvironment) {
|
||||
if (!Array.isArray(ancestors)) {
|
||||
ancestors = [];
|
||||
}
|
||||
@@ -88,7 +37,7 @@ export function buildRenderContext (ancestors, rootEnvironment, subEnvironment)
|
||||
for (const environment of environments) {
|
||||
// Do an Object.assign, but render each property as it overwrites. This
|
||||
// way we can keep same-name variables from the parent context.
|
||||
_objectDeepAssignRender(renderContext, environment);
|
||||
await _objectDeepAssignRender(renderContext, environment);
|
||||
}
|
||||
|
||||
// Render the context with itself to fill in the rest.
|
||||
@@ -96,30 +45,59 @@ export function buildRenderContext (ancestors, rootEnvironment, subEnvironment)
|
||||
|
||||
// Render up to 5 levels of recursive references.
|
||||
for (let i = 0; i < 3; i++) {
|
||||
finalRenderContext = recursiveRender(finalRenderContext, finalRenderContext);
|
||||
finalRenderContext = await recursiveRender(finalRenderContext, finalRenderContext);
|
||||
}
|
||||
|
||||
return finalRenderContext;
|
||||
}
|
||||
|
||||
export function recursiveRender (obj, context) {
|
||||
/**
|
||||
* Recursively render any JS object and return a new one
|
||||
* @param {*} originalObj - object to render
|
||||
* @param {object} context - context to render against
|
||||
* @return {Promise.<*>}
|
||||
*/
|
||||
export async function recursiveRender (originalObj, context) {
|
||||
const obj = clone(originalObj);
|
||||
const toS = obj => Object.prototype.toString.call(obj);
|
||||
|
||||
// Make a copy so no one gets mad :)
|
||||
const newObj = traverse.clone(obj);
|
||||
|
||||
traverse(newObj).forEach(function (x) {
|
||||
try {
|
||||
if (typeof x === 'string') {
|
||||
const str = render(x, context);
|
||||
this.update(str);
|
||||
async function next (x) {
|
||||
// Leave these types alone
|
||||
if (
|
||||
toS(x) === '[object Date]' ||
|
||||
toS(x) === '[object RegExp]' ||
|
||||
toS(x) === '[object Error]' ||
|
||||
toS(x) === '[object Boolean]' ||
|
||||
toS(x) === '[object Number]' ||
|
||||
toS(x) === '[object Null]' ||
|
||||
toS(x) === '[object Undefined]'
|
||||
) {
|
||||
// Do nothing to these types
|
||||
} else if (toS(x) === '[object String]') {
|
||||
try {
|
||||
x = render(x, context);
|
||||
} catch (err) {
|
||||
// Failed to render Request
|
||||
// const path = this.path.join('.');
|
||||
const path = 'TODO"';
|
||||
throw new Error(`Failed to render Request.${path}: "${err.message}"`);
|
||||
}
|
||||
} else if (Array.isArray(x)) {
|
||||
for (let i = 0; i < x.length; i++) {
|
||||
x[i] = await next(x[i]);
|
||||
}
|
||||
} else if (typeof x === 'object') {
|
||||
const keys = Object.keys(x);
|
||||
for (const key of keys) {
|
||||
x[key] = await next(x[key]);
|
||||
}
|
||||
} catch (e) {
|
||||
// Failed to render Request
|
||||
const path = this.path.join('.');
|
||||
throw new Error(`Failed to render Request.${path}: "${e.message}"`);
|
||||
}
|
||||
});
|
||||
|
||||
return newObj;
|
||||
return x;
|
||||
}
|
||||
|
||||
return next(obj);
|
||||
}
|
||||
|
||||
export async function getRenderContext (request, environmentId, ancestors = null) {
|
||||
@@ -147,7 +125,7 @@ export async function getRenderedRequest (request, environmentId) {
|
||||
const renderContext = await getRenderContext(request, environmentId, ancestors);
|
||||
|
||||
// Render all request properties
|
||||
const renderedRequest = recursiveRender(request, renderContext);
|
||||
const renderedRequest = await recursiveRender(request, renderContext);
|
||||
|
||||
// Remove disabled params
|
||||
renderedRequest.parameters = renderedRequest.parameters.filter(p => !p.disabled);
|
||||
@@ -182,7 +160,7 @@ export async function getRenderedRequest (request, environmentId) {
|
||||
return renderedRequest;
|
||||
}
|
||||
|
||||
function _objectDeepAssignRender (base, obj) {
|
||||
async function _objectDeepAssignRender (base, obj) {
|
||||
for (const key of Object.keys(obj)) {
|
||||
/*
|
||||
* If we're overwriting a string, try to render it first with the base as
|
||||
@@ -196,7 +174,7 @@ function _objectDeepAssignRender (base, obj) {
|
||||
* original base_url of google.com would be lost.
|
||||
*/
|
||||
if (typeof base[key] === 'string') {
|
||||
base[key] = render(obj[key], base);
|
||||
base[key] = await render(obj[key], base);
|
||||
} else {
|
||||
base[key] = obj[key];
|
||||
}
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
let counter = 0;
|
||||
let v1Counter = 0;
|
||||
let v4Counter = 0;
|
||||
|
||||
const uuids = [
|
||||
const v1UUIDs = [
|
||||
'f7272c80-f493-11e6-bc64-92361f002671',
|
||||
'f7272f0a-f493-11e6-bc64-92361f002671',
|
||||
'f72733a6-f493-11e6-bc64-92361f002671',
|
||||
'f72735c2-f493-11e6-bc64-92361f002671',
|
||||
'f7273798-f493-11e6-bc64-92361f002671',
|
||||
'f7273c3e-f493-11e6-bc64-92361f002671',
|
||||
'f7273dba-f493-11e6-bc64-92361f002671',
|
||||
'f7273eaa-f493-11e6-bc64-92361f002671',
|
||||
'f7273f7c-f493-11e6-bc64-92361f002671',
|
||||
'f7274300-f493-11e6-bc64-92361f002671',
|
||||
];
|
||||
|
||||
const v4UUIDs = [
|
||||
'dd2ccc1a-2745-477a-881a-9e8ef9d42403',
|
||||
'e3e96e5f-dd68-4229-8b66-dee1f0940f3d',
|
||||
'a262d22b-5fa8-491c-9bd9-58fba03e301e',
|
||||
@@ -77,10 +91,19 @@ const uuids = [
|
||||
'185dc090-3fcc-44e3-8a17-4e2fa792f91a',
|
||||
];
|
||||
|
||||
function v4 () {
|
||||
const uuid = uuids[counter++];
|
||||
function v1 () {
|
||||
const uuid = v1UUIDs[v1Counter++];
|
||||
if (!uuid) {
|
||||
throw new Error('Not enough mocked UUIDs to go around');
|
||||
throw new Error('Not enough mocked v1 UUIDs to go around');
|
||||
}
|
||||
|
||||
return uuid;
|
||||
}
|
||||
|
||||
function v4 () {
|
||||
const uuid = v4UUIDs[v4Counter++];
|
||||
if (!uuid) {
|
||||
throw new Error('Not enough mocked v4 UUIDs to go around');
|
||||
}
|
||||
|
||||
return uuid;
|
||||
@@ -88,3 +111,4 @@ function v4 () {
|
||||
|
||||
module.exports = () => v4();
|
||||
module.exports.v4 = () => v4();
|
||||
module.exports.v1 = () => v1();
|
||||
|
||||
@@ -41,6 +41,11 @@ export function findRecentForRequest (requestId, limit) {
|
||||
return db.findMostRecentlyModified(type, {parentId: requestId}, limit);
|
||||
}
|
||||
|
||||
export async function getLatestForRequest (requestId) {
|
||||
const responses = await findRecentForRequest(requestId, 1);
|
||||
return responses[0] || null;
|
||||
}
|
||||
|
||||
export async function create (patch = {}) {
|
||||
if (!patch.parentId) {
|
||||
throw new Error('New Response missing `parentId`');
|
||||
|
||||
@@ -6,11 +6,12 @@
|
||||
"longName": "Insomnia REST Client",
|
||||
"description": "A simple and beautiful REST API client",
|
||||
"homepage": "http://insomnia.rest",
|
||||
"author": "Gregory Schier <support@insomnia.rest>",
|
||||
"author": "Insomnia <support@insomnia.rest>",
|
||||
"main": "main.js",
|
||||
"dependencies": {
|
||||
"analytics-node": "^2.1.0",
|
||||
"classnames": "^2.2.3",
|
||||
"clone": "^2.1.0",
|
||||
"electron-context-menu": "^0.4.0",
|
||||
"electron-squirrel-startup": "^1.0.0",
|
||||
"hkdf": "0.0.2",
|
||||
@@ -27,7 +28,6 @@
|
||||
"request": "^2.71.0",
|
||||
"srp-js": "0.2.0",
|
||||
"tough-cookie": "^2.3.1",
|
||||
"traverse": "^0.6.6",
|
||||
"uuid": "^3.0.0",
|
||||
"vkbeautify": "^0.99.1",
|
||||
"whatwg-fetch": "^2.0.1",
|
||||
|
||||
@@ -740,7 +740,7 @@ export async function getOrCreateAllActiveResources (resourceGroupId = null) {
|
||||
activeResourceMap[doc._id] = await createResourceForDoc(doc);
|
||||
created++;
|
||||
} catch (e) {
|
||||
logger.error(`Failed to create resource for ${doc._id}`, e, {doc});
|
||||
logger.error(`Failed to create resource for ${doc._id} ${e}`, {doc});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
26
app/templating/extensions/NowExtension.js
Normal file
26
app/templating/extensions/NowExtension.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import BaseExtension from './base/BaseExtension';
|
||||
|
||||
export default class NowExtension extends BaseExtension {
|
||||
constructor () {
|
||||
super();
|
||||
this.tags = ['now'];
|
||||
}
|
||||
|
||||
run (context, format = 'iso-8601') {
|
||||
format = typeof format === 'string' ? format.toLowerCase() : 'unknown';
|
||||
const now = new Date();
|
||||
|
||||
switch (format) {
|
||||
case 'millis':
|
||||
case 'ms':
|
||||
return now.getTime();
|
||||
case 'unix':
|
||||
case 'seconds':
|
||||
case 's':
|
||||
return Math.round(now.getTime() / 1000);
|
||||
case 'iso-8601':
|
||||
default:
|
||||
return now.toISOString();
|
||||
}
|
||||
}
|
||||
}
|
||||
51
app/templating/extensions/ResponseJsonPathExtension.js
Normal file
51
app/templating/extensions/ResponseJsonPathExtension.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import jq from 'jsonpath';
|
||||
import * as models from '../../models';
|
||||
|
||||
import BaseExtension from './base/BaseExtension';
|
||||
|
||||
const TAG_NAME = 'JSONPath';
|
||||
|
||||
export default class ResponseJsonPathExtension extends BaseExtension {
|
||||
constructor () {
|
||||
super();
|
||||
this.tags = [TAG_NAME];
|
||||
}
|
||||
|
||||
async run (context, id, query) {
|
||||
const request = await models.request.getById(id);
|
||||
if (!request) {
|
||||
throw new Error(`[${TAG_NAME}] Could not find request ${id}`)
|
||||
}
|
||||
|
||||
const response = await models.response.getLatestForRequest(id);
|
||||
|
||||
if (!response) {
|
||||
throw new Error(`[${TAG_NAME}] No responses for request ${id}`);
|
||||
}
|
||||
|
||||
const bodyBuffer = new Buffer(response.body, response.encoding);
|
||||
const bodyStr = bodyBuffer.toString();
|
||||
|
||||
let body;
|
||||
try {
|
||||
body = JSON.parse(bodyStr);
|
||||
} catch (err) {
|
||||
throw new Error(`[${TAG_NAME}] Invalid JSON: ${err.message}`)
|
||||
}
|
||||
|
||||
let results;
|
||||
try {
|
||||
results = jq.query(body, query);
|
||||
} catch (err) {
|
||||
throw new Error(`[${TAG_NAME}] Invalid JSONPath query: ${query}`)
|
||||
}
|
||||
|
||||
if (results.length === 0) {
|
||||
throw new Error(`[${TAG_NAME}] Returned no results: ${query}`)
|
||||
} else if (results.length > 1) {
|
||||
throw new Error(`[${TAG_NAME}] Returned more than one result: ${query}`)
|
||||
}
|
||||
|
||||
return `${results[0] || ''}`;
|
||||
}
|
||||
}
|
||||
12
app/templating/extensions/TimestampExtension.js
Normal file
12
app/templating/extensions/TimestampExtension.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import BaseExtension from './base/BaseExtension';
|
||||
|
||||
export default class TimestampExtension extends BaseExtension {
|
||||
constructor () {
|
||||
super();
|
||||
this.tags = ['timestamp'];
|
||||
}
|
||||
|
||||
run (context) {
|
||||
return Date.now();
|
||||
}
|
||||
}
|
||||
27
app/templating/extensions/UuidExtension.js
Normal file
27
app/templating/extensions/UuidExtension.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import uuid from 'uuid';
|
||||
import BaseExtension from './base/BaseExtension';
|
||||
|
||||
export default class UuidExtension extends BaseExtension {
|
||||
constructor () {
|
||||
super();
|
||||
this.tags = ['uuid'];
|
||||
}
|
||||
|
||||
run (context, version) {
|
||||
if (typeof version === 'number') {
|
||||
version += '';
|
||||
} else if (typeof version === 'string') {
|
||||
version = version.toLowerCase();
|
||||
}
|
||||
|
||||
switch (version) {
|
||||
case '1':
|
||||
case 'v1':
|
||||
return uuid.v1();
|
||||
case '4':
|
||||
case 'v4':
|
||||
default:
|
||||
return uuid.v4();
|
||||
}
|
||||
}
|
||||
}
|
||||
23
app/templating/extensions/__tests__/NowExtension.test.js
Normal file
23
app/templating/extensions/__tests__/NowExtension.test.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import * as templating from '../../index';
|
||||
|
||||
function assertTemplate (txt, expected) {
|
||||
return async function () {
|
||||
const result = await templating.render(txt);
|
||||
expect(result).toMatch(expected);
|
||||
}
|
||||
}
|
||||
|
||||
const isoRe = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/;
|
||||
const secondsRe = /^\d{10}$/;
|
||||
const millisRe = /^\d{13}$/;
|
||||
|
||||
describe('NowExtension', () => {
|
||||
it('renders default ISO', assertTemplate('{% now %}', isoRe));
|
||||
it('renders ISO-8601', assertTemplate('{% now "ISO-8601" %}', isoRe));
|
||||
it('renders seconds', assertTemplate('{% now "seconds" %}', secondsRe));
|
||||
it('renders s', assertTemplate('{% now "s" %}', secondsRe));
|
||||
it('renders unix', assertTemplate('{% now "unix" %}', secondsRe));
|
||||
it('renders millis', assertTemplate('{% now "millis" %}', millisRe));
|
||||
it('renders ms', assertTemplate('{% now "ms" %}', millisRe));
|
||||
it('renders default fallback', assertTemplate('{% now "foo" %}', isoRe));
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
import * as templating from '../../index';
|
||||
import * as db from '../../../common/database';
|
||||
import * as models from '../../../models';
|
||||
|
||||
describe('ResponseJsonPathExtension', async () => {
|
||||
beforeEach(() => db.init(models.types(), {inMemoryOnly: true}, true));
|
||||
|
||||
it('renders basic JSONPath query', async () => {
|
||||
const request = await models.request.create({parentId: 'foo'});
|
||||
await models.response.create({parentId: request._id, body: '{"foo": "bar"}'});
|
||||
|
||||
const result = await templating.render(`{% JSONPath "${request._id}", "$.foo" %}`);
|
||||
|
||||
expect(result).toBe('bar');
|
||||
});
|
||||
|
||||
it('fails on invalid JSON', async () => {
|
||||
const request = await models.request.create({parentId: 'foo'});
|
||||
await models.response.create({parentId: request._id, body: '{"foo": "bar"'});
|
||||
|
||||
try {
|
||||
await templating.render(`{% JSONPath "${request._id}", "$.foo" %}`);
|
||||
fail('JSON should have failed to parse');
|
||||
} catch (err) {
|
||||
expect(err.message).toContain('Invalid JSON: Unexpected end of JSON input');
|
||||
}
|
||||
});
|
||||
|
||||
it('fails on no responses', async () => {
|
||||
const request = await models.request.create({parentId: 'foo'});
|
||||
|
||||
try {
|
||||
await templating.render(`{% JSONPath "${request._id}", "$.foo" %}`);
|
||||
fail('JSON should have failed to parse');
|
||||
} catch (err) {
|
||||
expect(err.message).toContain('No responses for request');
|
||||
}
|
||||
});
|
||||
|
||||
it('fails on no request', async () => {
|
||||
await models.response.create({parentId: 'req_test', body: '{"foo": "bar"}'});
|
||||
|
||||
try {
|
||||
await templating.render(`{% JSONPath "req_test", "$.foo" %}`);
|
||||
fail('JSON should have failed to parse')
|
||||
} catch (err) {
|
||||
expect(err.message).toContain('Could not find request req_test');
|
||||
}
|
||||
});
|
||||
|
||||
it('fails on invalid query', async () => {
|
||||
const request = await models.request.create({parentId: 'foo'});
|
||||
await models.response.create({parentId: request._id, body: '{"foo": "bar"}'});
|
||||
|
||||
try {
|
||||
await templating.render(`{% JSONPath "${request._id}", "$$" %}`);
|
||||
fail('JSON should have failed to parse')
|
||||
} catch (err) {
|
||||
expect(err.message).toContain('Invalid JSONPath query: $$');
|
||||
}
|
||||
});
|
||||
|
||||
it('fails on no results', async () => {
|
||||
const request = await models.request.create({parentId: 'foo'});
|
||||
await models.response.create({parentId: request._id, body: '{"foo": "bar"}'});
|
||||
|
||||
try {
|
||||
await templating.render(`{% JSONPath "${request._id}", "$.missing" %}`);
|
||||
fail('JSON should have failed to parse')
|
||||
} catch (err) {
|
||||
expect(err.message).toContain('Returned no results: $.missing');
|
||||
}
|
||||
});
|
||||
|
||||
it('fails on more than 1 result', async () => {
|
||||
const request = await models.request.create({parentId: 'foo'});
|
||||
await models.response.create({parentId: request._id, body: '{"array": [1,2,3]}'});
|
||||
|
||||
try {
|
||||
await templating.render(`{% JSONPath "${request._id}", "$.array.*" %}`);
|
||||
fail('JSON should have failed to parse')
|
||||
} catch (err) {
|
||||
expect(err.message).toContain('Returned more than one result: $.array.*');
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
import * as templating from '../../index';
|
||||
|
||||
function assertTemplate (txt, expected) {
|
||||
return async function () {
|
||||
const result = await templating.render(txt);
|
||||
expect(result).toMatch(expected);
|
||||
}
|
||||
}
|
||||
|
||||
const millisRe = /^\d{13}$/;
|
||||
|
||||
describe('TimestampExtension', () => {
|
||||
it('renders basic', assertTemplate('{% timestamp %}', millisRe));
|
||||
});
|
||||
19
app/templating/extensions/__tests__/UuidExtension.test.js
Normal file
19
app/templating/extensions/__tests__/UuidExtension.test.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import * as templating from '../../index';
|
||||
|
||||
function assertTemplate (txt, expected) {
|
||||
return async function () {
|
||||
const result = await templating.render(txt);
|
||||
expect(result).toMatch(expected);
|
||||
}
|
||||
}
|
||||
|
||||
describe('UuidExtension', () => {
|
||||
it('renders default v4', assertTemplate('{% uuid %}', 'dd2ccc1a-2745-477a-881a-9e8ef9d42403'));
|
||||
it('renders 4', assertTemplate('{% uuid "4" %}', 'e3e96e5f-dd68-4229-8b66-dee1f0940f3d'));
|
||||
it('renders 4 num', assertTemplate('{% uuid 4 %}', 'a262d22b-5fa8-491c-9bd9-58fba03e301e'));
|
||||
it('renders v4', assertTemplate('{% uuid "v4" %}', '2e7c2688-09ee-44b8-900d-5cbbaa7d3a19'));
|
||||
it('renders 1', assertTemplate('{% uuid "1" %}', 'f7272c80-f493-11e6-bc64-92361f002671'));
|
||||
it('renders 1 num', assertTemplate('{% uuid 1 %}', 'f7272f0a-f493-11e6-bc64-92361f002671'));
|
||||
it('renders v1', assertTemplate('{% uuid "v1" %}', 'f72733a6-f493-11e6-bc64-92361f002671'));
|
||||
it('renders default fallback', assertTemplate('{% uuid "foo" %}', 'e7d698c4-c7d2-409c-90c6-22bcc94ba4ab'));
|
||||
});
|
||||
49
app/templating/extensions/base/BaseExtension.js
Normal file
49
app/templating/extensions/base/BaseExtension.js
Normal file
@@ -0,0 +1,49 @@
|
||||
export default class NowExtension {
|
||||
constructor () {
|
||||
// TODO: Subclass should set this
|
||||
// this.tags = ['now'];
|
||||
}
|
||||
|
||||
parse (parser, nodes, lexer) {
|
||||
const tok = parser.nextToken();
|
||||
|
||||
let args;
|
||||
if (parser.peekToken().type !== lexer.TOKEN_BLOCK_END) {
|
||||
args = parser.parseSignature(null, true);
|
||||
} else {
|
||||
// Not sure why this is needed, but it fails without it
|
||||
args = new nodes.NodeList(tok.lineno, tok.colno);
|
||||
args.addChild(new nodes.Literal(0, 0, ""));
|
||||
}
|
||||
|
||||
parser.advanceAfterBlockEnd(tok.value);
|
||||
return new nodes.CallExtensionAsync(this, 'asyncRun', args);
|
||||
}
|
||||
|
||||
asyncRun (...runArgs) {
|
||||
// Pull the callback off the end
|
||||
const callback = runArgs[runArgs.length - 1];
|
||||
const args = runArgs.slice(0, runArgs.length - 1);
|
||||
|
||||
let result;
|
||||
try {
|
||||
result = this.run(...args);
|
||||
} catch (err) {
|
||||
// In case a synchronous render fails
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
// If the result is a promise, resolve it async
|
||||
if (result instanceof Promise) {
|
||||
result.then(
|
||||
r => callback(null, r),
|
||||
err => callback(err)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// If the result is not a Promise, return it synchronously
|
||||
callback(null, result);
|
||||
}
|
||||
}
|
||||
13
app/templating/extensions/index.js
Normal file
13
app/templating/extensions/index.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import TimestampExtension from './TimestampExtension';
|
||||
import UuidExtension from './UuidExtension';
|
||||
import NowExtension from './NowExtension';
|
||||
import ResponseJsonPathExtension from './ResponseJsonPathExtension';
|
||||
|
||||
export function all () {
|
||||
return [
|
||||
TimestampExtension,
|
||||
UuidExtension,
|
||||
NowExtension,
|
||||
ResponseJsonPathExtension,
|
||||
]
|
||||
}
|
||||
42
app/templating/index.js
Normal file
42
app/templating/index.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import nunjucks from 'nunjucks';
|
||||
import * as extensions from './extensions';
|
||||
|
||||
/**
|
||||
* Render text based on stuff
|
||||
* @param {String} text - Nunjucks template in text form
|
||||
* @param {Object} [config] - Config options for rendering
|
||||
* @param {Object} [config.context] - Context to render with
|
||||
*/
|
||||
export async function render (text, config = {}) {
|
||||
const context = config.context || {};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
_getEnv().renderString(text, context, (err, result) => {
|
||||
if (err) {
|
||||
const sanitizedMsg = err.message.replace(/\(unknown path\)\s*/, '');
|
||||
reject(new Error(sanitizedMsg));
|
||||
} else {
|
||||
resolve(result);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ~~~~~~~~~~~~~ //
|
||||
// Private Stuff //
|
||||
// ~~~~~~~~~~~~~ //
|
||||
|
||||
let _nunjucksEnvironment = null;
|
||||
function _getEnv () {
|
||||
if (!_nunjucksEnvironment) {
|
||||
_nunjucksEnvironment = nunjucks.configure({
|
||||
autoescape: false,
|
||||
});
|
||||
|
||||
for (const Cls of extensions.all()) {
|
||||
_nunjucksEnvironment.addExtension(Cls.name, new Cls());
|
||||
}
|
||||
}
|
||||
|
||||
return _nunjucksEnvironment;
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import Link from './base/Link';
|
||||
import * as fetch from '../../common/fetch';
|
||||
import {trackEvent} from '../../analytics/index';
|
||||
import * as models from '../../models/index';
|
||||
import * as querystring from '../../common/querystring';
|
||||
import * as constants from '../../common/constants';
|
||||
import * as db from '../../common/database';
|
||||
|
||||
|
||||
@@ -167,7 +167,9 @@ class WorkspaceEnvironmentsEditModal extends Component {
|
||||
<DropdownItem onClick={this._handleAddEnvironment} value={false}>
|
||||
<i className="fa fa-eye"/> Environment
|
||||
</DropdownItem>
|
||||
<DropdownItem onClick={this._handleAddEnvironment} value={true}>
|
||||
<DropdownItem onClick={this._handleAddEnvironment}
|
||||
value={true}
|
||||
title="Environment will not be exported or synced">
|
||||
<i className="fa fa-eye-slash"/> Private Environment
|
||||
</DropdownItem>
|
||||
</Dropdown>
|
||||
@@ -184,7 +186,7 @@ class WorkspaceEnvironmentsEditModal extends Component {
|
||||
<button onClick={() => this._handleShowEnvironment(environment)}>
|
||||
{environment.isPrivate ? (
|
||||
<i className="fa fa-eye-slash faint"
|
||||
title="Private Environment will not be exported or synced"
|
||||
title="Environment will not be exported or synced"
|
||||
/>
|
||||
) : (
|
||||
<i className="fa fa-blank faint"/>
|
||||
|
||||
@@ -166,7 +166,7 @@ class App extends Component {
|
||||
const {activeEnvironment, activeRequest} = this.props;
|
||||
const environmentId = activeEnvironment ? activeEnvironment._id : null;
|
||||
const context = await render.getRenderContext(activeRequest, environmentId);
|
||||
return render.render(text, context);
|
||||
return await render.render(text, context);
|
||||
};
|
||||
|
||||
_handleGenerateCodeForActiveRequest = () => {
|
||||
|
||||
@@ -15,12 +15,14 @@ environment:
|
||||
# Things to install after repo clone
|
||||
install:
|
||||
- ps: Install-Product node $env:nodejs_version $env:Platform
|
||||
- npm config set msvs_version 2015
|
||||
- npm install -g npm@latest > Nul
|
||||
- npm install > NUL
|
||||
- npm config set msvs_version 2015
|
||||
|
||||
cache:
|
||||
- '%APPDATA%\npm-cache'
|
||||
- '%USERPROFILE%\.electron'
|
||||
- 'node_modules'
|
||||
|
||||
#---------------------------------#
|
||||
# tests configuration #
|
||||
|
||||
16
package.json
16
package.json
@@ -42,9 +42,9 @@
|
||||
"plugins": [
|
||||
"transform-object-rest-spread",
|
||||
"transform-class-properties",
|
||||
"add-module-exports",
|
||||
"transform-regenerator",
|
||||
"transform-runtime"
|
||||
"transform-runtime",
|
||||
"add-module-exports"
|
||||
],
|
||||
"env": {
|
||||
"development": {
|
||||
@@ -100,6 +100,7 @@
|
||||
"dependencies": {
|
||||
"analytics-node": "^2.1.0",
|
||||
"classnames": "^2.2.3",
|
||||
"clone": "^2.1.0",
|
||||
"codemirror": "^5.23.0",
|
||||
"electron-context-menu": "^0.4.0",
|
||||
"electron-squirrel-startup": "^1.0.0",
|
||||
@@ -128,7 +129,6 @@
|
||||
"reselect": "^2.5.4",
|
||||
"srp-js": "0.2.0",
|
||||
"tough-cookie": "^2.3.1",
|
||||
"traverse": "^0.6.6",
|
||||
"uuid": "^3.0.0",
|
||||
"vkbeautify": "^0.99.1",
|
||||
"whatwg-fetch": "^2.0.1",
|
||||
@@ -136,20 +136,20 @@
|
||||
"xpath": "^0.0.23"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-cli": "^6.18.0",
|
||||
"babel-core": "^6.21.0",
|
||||
"babel-cli": "^6.23.0",
|
||||
"babel-core": "^6.23.1",
|
||||
"babel-jest": "^18.0.0",
|
||||
"babel-loader": "^6.2.10",
|
||||
"babel-loader": "^6.3.2",
|
||||
"babel-plugin-add-module-exports": "^0.2.1",
|
||||
"babel-plugin-transform-class-properties": "^6.19.0",
|
||||
"babel-plugin-transform-object-rest-spread": "^6.6.5",
|
||||
"babel-plugin-transform-regenerator": "^6.21.0",
|
||||
"babel-plugin-transform-regenerator": "^6.22.0",
|
||||
"babel-plugin-transform-runtime": "^6.15.0",
|
||||
"babel-polyfill": "^6.20.0",
|
||||
"babel-preset-es2015": "^6.6.0",
|
||||
"babel-preset-es2017": "^6.22.0",
|
||||
"babel-preset-react": "^6.5.0",
|
||||
"babel-preset-react-hmre": "^1.1.1",
|
||||
"babel-runtime": "^6.20.0",
|
||||
"concurrently": "^2.0.0",
|
||||
"cross-env": "^2.0.0",
|
||||
"css-loader": "^0.23.1",
|
||||
|
||||
Reference in New Issue
Block a user