Source: saltcorn-data/base-plugin/actions.js

/**
 * Action description
 *
 */

const fetch = require("node-fetch");
const vm = require("vm");
const Table = require("../models/table");
const View = require("../models/view");
const { getState } = require("../db/state");
const User = require("../models/user");
const {
  getMailTransport,
  transformBootstrapEmail,
} = require("../models/email");
const { mockReqRes } = require("../tests/mocks");
const { get_async_expression_function } = require("../models/expression");
const { div, code } = require("@saltcorn/markup/tags");

//action use cases: field modify, like/rate (insert join), notify, send row to webhook
// todo add translation
module.exports = {
  webhook: {
    configFields: [
      { name: "url", label: "URL", type: "String", sublabel: "Trigger will call specified URL" },
      {
        name: "body",
        label: "JSON body",
        sublabel: "Leave blank to use row from table",
        type: "String",
        fieldview: "textarea", // I think that textarea is better
      },
    ],
    run: async ({ row, configuration: { url, body } }) => {
      return await fetch(url, {
        method: "post",
        body: body || JSON.stringify(row),
        headers: { "Content-Type": "application/json" },
      });
    },
  },
  send_email: {
    configFields: async ({ table }) => {
      if (!table) return [];
      const views = await View.find_table_views_where(
        table,
        ({ viewtemplate }) => viewtemplate.runMany || viewtemplate.renderRows
      );

      const view_opts = views.map((v) => v.name);
      const fields = await table.getFields();
      const field_opts = fields
        .filter((f) => f.type.name === "String" || f.reftable_name === "users")
        .map((f) => f.name);
      return [
        {
          name: "viewname",
          label: "View to send",
          sublabel:
            "Select a view that can render a single record - for instance, of the Show template.",
          input_type: "select",
          options: view_opts,
        },
        {
          name: "to_email",
          label: "Recipient email address",
          sublabel: "Select email addresses for send email. Choose option to get more information",
          input_type: "select",
          required: true,

          options: ["Fixed", "User", "Field"],
        },
        {
          name: "to_email_field",
          label: "Field with email address",
          sublabel:
            "Field with email address a String, or Key to user who will receive email",
          input_type: "select",

          options: field_opts,

          showIf: { to_email: "Field" },
        },
        {
          name: "to_email_fixed",
          label: "Fixed address",
          sublabel: "Email address to send emails", // todo send to few addresses?
          type: "String",
          showIf: { to_email: "Fixed" },
        },
        {
          name: "subject",
          label: "Subject",
          sublabel: "Subject of email",
          type: "String",
          required: true,
        },
      ];
    },
    requireRow: true,
    run: async ({
      row,
      table,
      configuration: {
        viewname,
        subject,
        to_email,
        to_email_field,
        to_email_fixed,
      },
      user,
    }) => {
      let to_addr;
      switch (to_email) {
        case "Fixed":
          to_addr = to_email_fixed;
          break;
        case "User":
          to_addr = user.email;
          break;
        case "Field":
          const fields = await table.getFields();
          const field = fields.find((f) => f.name === to_email_field);
          if (field && field.type.name === "String")
            to_addr = row[to_email_field];
          else if (field && field.reftable_name === "users") {
            const refuser = await User.findOne({ id: row[to_email_field] });
            to_addr = refuser.email;
          }
          break;
      }
      const view = await View.findOne({ name: viewname });
      const htmlBs = await view.run({ id: row.id }, mockReqRes);
      const html = await transformBootstrapEmail(htmlBs);
      console.log("Sending email from %s to %s with subject %s to_email",
          getState().getConfig("email_from"), to_addr, subject, to_addr);
      const email = {
        from: getState().getConfig("email_from"),
        to: to_addr,
        subject,
        html,
      };
      //console.log(email);
      await getMailTransport().sendMail(email);
      return { notify: `E-mail sent to ${to_addr}` };
    },
  },
  insert_joined_row: {
    configFields: async ({ table }) => {
      if (!table) return [];
      const { child_field_list } = await table.get_child_relations();
      return [
        {
          name: "joined_table",
          label: "Relation",
          sublabel: "Relation", // todo more detailed explanation
          input_type: "select",
          options: child_field_list,
        },
      ];
    },
    requireRow: true,
    run: async ({ row, table, configuration: { joined_table }, user }) => {
      const [join_table_name, join_field] = joined_table.split(".");
      const joinTable = await Table.findOne({ name: join_table_name });
      const fields = await joinTable.getFields();
      const newRow = { [join_field]: row.id };
      for (const field of fields) {
        if (
          field.type === "Key" &&
          field.reftable_name === "users" &&
          user &&
          user.id
        )
          newRow[field.name] = user.id;
      }
      return await joinTable.insertRow(newRow);
    },
  },
  duplicate_row: {
    configFields: () => [],
    requireRow: true,
    run: async ({ row, table, user }) => {
      const newRow = { ...row };
      await table.getFields();
      delete newRow[table.pk_name];
      await table.insertRow(newRow);
      return { reload_page: true };
    },
  },
  insert_any_row: {
    configFields: async ({ table }) => {
      const tables = await Table.find();
      return [
        {
          name: "table",
          label: "Table",
          sublabel: "Table", //todo more detailed explanation
          input_type: "select",
          options: tables.map((t) => t.name),
        },
        {
          name: "row_expr",
          label: "Row expression",
          sublabel: "Expression for JavaScript object",
          type: "String",
          fieldview: "textarea",
        },
      ];
    },
    run: async ({ row, configuration: { row_expr, table }, user, ...rest }) => {
      const f = get_async_expression_function(row_expr, [], {
        row: row || {},
        user,
        console,
      });
      const calcrow = await f({});
      const table_for_insert = await Table.findOne({ name: table });
      const res = await table_for_insert.tryInsertRow(calcrow, user && user.id);
      if (res.error) return res;
      else return true;
    },
  },
  run_js_code: {
    configFields: async ({ table }) => {
      const fields = table ? (await table.getFields()).map((f) => f.name) : [];
      const vars = [      
        ...(table ? ["row"] : []),
        "user",
        "console",
        "Actions",
        "Table",
        ...(table ? ["table"] : []),
        ...fields,
      ]
        .map((f) => code(f))
        .join(", ");
      return [
        {
          name: "code",
          label: "Code",
          input_type: "code",
          attributes: { mode: "application/javascript" },
          sublabel: div("Variables in scope: ", vars),
        },
      ];
    },
    run: async ({ row, table, configuration: { code }, user, ...rest }) => {
      const Actions = {};
      Object.entries(getState().actions).forEach(([k, v]) => {
        Actions[k] = (args = {}) => {
          v.run({ row, table, user, configuration: args, ...rest, ...args });
        };
      });
      const f = vm.runInNewContext(`async () => {${code}}`, {
        Table,
        table,
        row,
        user,
        console,
        Actions,
        ...(row || {}),
        ...getState().function_context,
        ...rest,
      });
      return await f();
    },
  },
};