2023-02-07 16:31:35 +01:00
|
|
|
const jsonQuery = require('json-query');
|
2023-02-20 06:35:49 +01:00
|
|
|
const { get } = require("@usebruno/query");
|
2023-02-07 16:31:35 +01:00
|
|
|
|
2023-02-11 04:55:05 +01:00
|
|
|
const JS_KEYWORDS = `
|
|
|
|
break case catch class const continue debugger default delete do
|
|
|
|
else export extends false finally for function if import in instanceof
|
|
|
|
new null return super switch this throw true try typeof var void while with
|
|
|
|
undefined let static yield arguments of
|
|
|
|
`.split(/\s+/).filter(word => word.length > 0);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates a function from a Javascript expression
|
|
|
|
*
|
|
|
|
* When the function is called, the variables used in this expression are picked up from the context
|
|
|
|
*
|
|
|
|
* ```js
|
|
|
|
* res.data.pets.map(pet => pet.name.toUpperCase())
|
|
|
|
*
|
|
|
|
* function(context) {
|
|
|
|
* const { res, pet } = context;
|
|
|
|
* return res.data.pets.map(pet => pet.name.toUpperCase())
|
|
|
|
* }
|
|
|
|
* ```
|
|
|
|
*/
|
|
|
|
const compileJsExpression = (expr) => {
|
|
|
|
// get all dotted identifiers (foo, bar.baz, .baz)
|
|
|
|
const matches = expr.match(/([\w\.$]+)/g) ?? [];
|
|
|
|
|
|
|
|
// get valid js identifiers (foo, bar)
|
2023-02-11 15:57:27 +01:00
|
|
|
const vars = new Set(
|
|
|
|
matches
|
|
|
|
.filter(match => /^[a-zA-Z$_]/.test(match)) // starts with valid js identifier (foo.bar)
|
|
|
|
.map(match => match.split('.')[0]) // top level identifier (foo)
|
|
|
|
.filter(name => !JS_KEYWORDS.includes(name)) // exclude js keywords
|
|
|
|
);
|
|
|
|
|
|
|
|
// globals such as Math
|
|
|
|
const globals = [...vars].filter(name => name in globalThis);
|
|
|
|
|
|
|
|
const code = {
|
|
|
|
vars: [...vars].join(", "),
|
|
|
|
// pick global from context or globalThis
|
|
|
|
globals: globals
|
|
|
|
.map(name => ` ${name} = ${name} ?? globalThis.${name};`)
|
|
|
|
.join('')
|
|
|
|
};
|
|
|
|
|
|
|
|
const body = `let { ${code.vars} } = context; ${code.globals}; return ${expr}`;
|
2023-02-11 04:55:05 +01:00
|
|
|
|
|
|
|
return new Function("context", body);
|
|
|
|
};
|
|
|
|
|
|
|
|
const internalExpressionCache = new Map();
|
|
|
|
|
2023-02-07 16:31:35 +01:00
|
|
|
const evaluateJsExpression = (expression, context) => {
|
2023-02-11 04:55:05 +01:00
|
|
|
let fn = internalExpressionCache.get(expression);
|
|
|
|
if (fn == null) {
|
|
|
|
internalExpressionCache.set(expression, fn = compileJsExpression(expression));
|
|
|
|
}
|
|
|
|
return fn(context);
|
2023-02-07 16:31:35 +01:00
|
|
|
};
|
|
|
|
|
2023-02-21 10:56:12 +01:00
|
|
|
const evaluateJsTemplateLiteral = (templateLiteral, context) => {
|
|
|
|
if(!templateLiteral || !templateLiteral.length || typeof templateLiteral !== 'string') {
|
|
|
|
return templateLiteral;
|
|
|
|
}
|
|
|
|
|
|
|
|
templateLiteral = templateLiteral.trim();
|
|
|
|
|
|
|
|
if(templateLiteral === 'true') {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if(templateLiteral === 'false') {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if(templateLiteral === 'null') {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if(templateLiteral === 'undefined') {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
if(templateLiteral.startsWith('"') && templateLiteral.endsWith('"')) {
|
|
|
|
return templateLiteral.slice(1, -1);
|
|
|
|
}
|
|
|
|
|
|
|
|
if(templateLiteral.startsWith("'") && templateLiteral.endsWith("'")) {
|
|
|
|
return templateLiteral.slice(1, -1);
|
|
|
|
}
|
|
|
|
|
|
|
|
if(!isNaN(templateLiteral)) {
|
|
|
|
return Number(templateLiteral);
|
|
|
|
}
|
|
|
|
|
|
|
|
templateLiteral = "`" + templateLiteral + "`";
|
|
|
|
|
|
|
|
return evaluateJsExpression(templateLiteral, context);
|
|
|
|
};
|
|
|
|
|
2023-02-07 20:43:21 +01:00
|
|
|
const createResponseParser = (response = {}) => {
|
2023-02-20 06:35:49 +01:00
|
|
|
const res = (expr, ...fns) => {
|
|
|
|
return get(response.data, expr, ...fns);
|
2023-02-11 04:55:05 +01:00
|
|
|
};
|
2023-02-07 16:31:35 +01:00
|
|
|
|
2023-02-07 20:43:21 +01:00
|
|
|
res.status = response.status;
|
|
|
|
res.statusText = response.statusText;
|
|
|
|
res.headers = response.headers;
|
|
|
|
res.body = response.data;
|
|
|
|
|
2023-02-20 06:35:49 +01:00
|
|
|
res.jq = (expr) => {
|
|
|
|
const output = jsonQuery(expr, { data: response.data });
|
|
|
|
return output ? output.value : null;
|
|
|
|
};
|
2023-02-13 00:27:54 +01:00
|
|
|
|
2023-02-07 20:43:21 +01:00
|
|
|
return res;
|
2023-02-07 16:31:35 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
module.exports = {
|
|
|
|
evaluateJsExpression,
|
2023-02-21 10:56:12 +01:00
|
|
|
evaluateJsTemplateLiteral,
|
2023-02-11 04:55:05 +01:00
|
|
|
createResponseParser,
|
|
|
|
internalExpressionCache
|
2023-02-07 16:31:35 +01:00
|
|
|
};
|