Source: saltcorn-data/models/expression.js

const vm = require("vm");
let acorn = require("acorn");
const estraverse = require("estraverse");
const astring = require("astring");
function expressionValidator(s) {
  if (!s || s.length == 0) return "Missing formula";
  try {
    const f = new vm.Script(s);
    return true;
  } catch (e) {
    return e.message;
  }
}
function jsexprToSQL(expression) {
  if (!expression) return expression;
  return expression.replace(/===/g, "=").replace(/==/g, "=").replace(/"/g, "'");
}
function transform_for_async(expression, statefuns) {
  var isAsync = false;
  const ast = acorn.parseExpressionAt(expression, 0, {
    ecmaVersion: 2020,
    allowAwaitOutsideFunction: true,
    locations: false,
  });
  estraverse.replace(ast, {
    leave: function (node) {
      if (node.type === "CallExpression") {
        const sf = statefuns[node.callee.name];
        if (sf && sf.isAsync) {
          isAsync = true;
          return { type: "AwaitExpression", argument: node };
        }
      }
    },
  });

  return { isAsync, expr_string: astring.generate(ast) };
}

function get_expression_function(expression, fields) {
  const args = `{${fields.map((f) => f.name).join()}}`;
  const { getState } = require("../db/state");
  return vm.runInNewContext(
    `(${args})=>(${expression})`,
    getState().function_context
  );
}
function get_async_expression_function(expression, fields, extraContext = {}) {
  const args = `{${fields.map((f) => f.name).join()}}`;
  const { getState } = require("../db/state");
  const { expr_string } = transform_for_async(expression, getState().functions);
  const evalStr = `async (${args})=>(${expr_string})`;
  return vm.runInNewContext(evalStr, {
    ...getState().function_context,
    ...extraContext,
  });
}
function apply_calculated_fields(rows, fields) {
  let hasExprs = false;
  let transform = (x) => x;
  for (const field of fields) {
    if (field.calculated && !field.stored) {
      hasExprs = true;
      let f;
      try {
        f = get_expression_function(field.expression, fields);
      } catch (e) {
        throw new Error(`Error in calculating "${field.name}": ${e.message}`);
      }
      const oldf = transform;
      transform = (row) => {
        try {
          const x = f(row);
          row[field.name] = x;
        } catch (e) {
          throw new Error(`Error in calculating "${field.name}": ${e.message}`);
        }
        return oldf(row);
      };
    }
  }
  if (hasExprs) {
    return rows.map(transform);
  } else return rows;
}
const apply_calculated_fields_stored = async (row, fields) => {
  let hasExprs = false;
  let transform = (x) => x;
  for (const field of fields) {
    if (field.calculated && field.stored) {
      hasExprs = true;
      let f;
      try {
        f = get_async_expression_function(field.expression, fields);
      } catch (e) {
        throw new Error(`Error in calculating "${field.name}": ${e.message}`);
      }
      const oldf = transform;
      transform = async (row) => {
        try {
          const x = await f(row);
          row[field.name] = x;
        } catch (e) {
          throw new Error(`Error in calculating "${field.name}": ${e.message}`);
        }
        return await oldf(row);
      };
    }
  }
  if (hasExprs) {
    return await transform(row);
  } else return row;
};
/**
 * Recalculate calculated columns that are stored in db
 * @param table - table object
 * @returns {Promise<void>}
 */
const recalculate_for_stored = async (table) => {
  let rows = [];
  let maxid = 0;
  do {
    rows = await table.getRows(
      { id: { gt: maxid } },
      { orderBy: "id", limit: 20 }
    );
    for (const row of rows) {
      try {
        await table.updateRow({}, row.id);
      } catch (e) {
        console.error(e);
      }
    }
    if (rows.length > 0) maxid = rows[rows.length - 1].id;
  } while (rows.length === 20);
};
module.exports = {
  expressionValidator,
  apply_calculated_fields,
  get_async_expression_function,
  get_expression_function,
  recalculate_for_stored,
  transform_for_async,
  apply_calculated_fields_stored,
  jsexprToSQL,
};