/**
* Plugin-helper
*
*/
const View = require("./models/view");
const Field = require("./models/field");
const Table = require("./models/table");
const Trigger = require("./models/trigger");
const { getState } = require("./db/state");
const db = require("./db");
const { contract, is } = require("contractis");
const {
fieldlike,
is_table_query,
is_column,
is_tablely,
} = require("./contracts");
const { link } = require("@saltcorn/markup");
const { button, a, label, text, i } = require("@saltcorn/markup/tags");
const { applyAsync, InvalidConfiguration } = require("./utils");
const { jsexprToSQL } = require("./models/expression");
/**
*
* @param url
* @param label
* @param popup
* @param link_style
* @param link_size
* @param link_icon
* @param textStyle
* @returns {string}
*/
const link_view = (
url,
label,
popup,
link_style = "",
link_size = "",
link_icon = "",
textStyle = ""
) => {
if (popup) {
return button(
{
class: "btn btn-secondary btn-sm",
onClick: `ajax_modal('${url}')`,
},
link_icon ? i({ class: link_icon }) + " " : "",
label
);
} else
return a(
{
href: url,
class: [textStyle, link_style, link_size],
},
link_icon ? i({ class: link_icon }) + " " : "",
text(label)
);
};
const stateToQueryString = contract(
is.fun(is.maybe(is.obj()), is.str),
(state) => {
if (!state || Object.keys(state).length === 0) return "";
return (
"?" +
Object.entries(state)
.map(([k, v]) =>
k === "id"
? null
: `${encodeURIComponent(k)}=${encodeURIComponent(v)}`
)
.filter((s) => !!s)
.join("&")
);
}
);
/**
*
* @type {*|(function(...[*]=): *)}
*/
const calcfldViewOptions = contract(
is.fun(
[is.array(is.class("Field")), is.bool],
is.obj({ field_view_options: is.objVals(is.array(is.str)) })
),
(fields, isEdit) => {
var fvs = {};
const handlesTextStyle = {};
fields.forEach((f) => {
handlesTextStyle[f.name] = [];
if (f.type === "File") {
if (!isEdit) fvs[f.name] = Object.keys(getState().fileviews);
else fvs[f.name] = ["upload"];
} else if (f.type === "Key") {
if (isEdit) fvs[f.name] = Object.keys(getState().keyFieldviews);
else {
if (f.reftable && f.reftable.fields) {
const { field_view_options } = calcfldViewOptions(
f.reftable.fields,
isEdit
);
for (const jf of f.reftable.fields) {
fvs[`${f.name}.${jf.name}`] = field_view_options[jf.name];
}
}
fvs[f.name] = ["show"];
}
Object.entries(getState().keyFieldviews).forEach(([k, v]) => {
if (v && v.handlesTextStyle) handlesTextStyle[f.name].push(k);
});
} else if (f.type && f.type.fieldviews) {
const tfvs = Object.entries(f.type.fieldviews).filter(([k, fv]) =>
f.calculated ? !fv.isEdit : !fv.isEdit || isEdit
);
let tfvs_ordered = [];
if (isEdit) {
tfvs_ordered = [
...tfvs.filter(([k, fv]) => fv.isEdit),
...tfvs.filter(([k, fv]) => !fv.isEdit),
];
} else tfvs_ordered = tfvs;
fvs[f.name] = tfvs_ordered.map(([k, fv]) => {
if (fv && fv.handlesTextStyle) handlesTextStyle[f.name].push(k);
return k;
});
}
});
return { field_view_options: fvs, handlesTextStyle };
}
);
/**
*
* @type {*|(function(...[*]=): *)}
*/
const calcfldViewConfig = contract(
is.fun([is.array(is.class("Field")), is.bool], is.promise(is.obj())),
async (fields, isEdit) => {
const fieldViewConfigForms = {};
for (const f of fields) {
fieldViewConfigForms[f.name] = {};
const fieldviews =
f.type === "Key" ? getState().keyFieldviews : f.type.fieldviews || {};
for (const [nm, fv] of Object.entries(fieldviews)) {
if (fv.configFields)
fieldViewConfigForms[f.name][nm] = await applyAsync(
fv.configFields,
f
);
}
}
return fieldViewConfigForms;
}
);
/**
*
* @type {*|(function(...[*]=): *)}
*/
const get_link_view_opts = contract(
is.fun(
[is_tablely, is.str],
is.promise(is.array(is.obj({ label: is.str, name: is.str })))
),
async (table, viewname) => {
const own_link_views = await View.find_possible_links_to_table(table);
const link_view_opts = own_link_views
.filter((v) => v.name !== viewname)
.map((v) => ({
label: `${v.name} [${v.viewtemplate} ${table.name}]`,
name: `Own:${v.name}`,
}));
const child_views = await get_child_views(table, viewname);
for (const { relation, related_table, views } of child_views) {
for (const view of views) {
link_view_opts.push({
name: `ChildList:${view.name}.${related_table.name}.${relation.name}`,
label: `${view.name} [${view.viewtemplate} ${related_table.name}.${relation.label}]`,
});
}
}
const parent_views = await get_parent_views(table, viewname);
for (const { relation, related_table, views } of parent_views) {
for (const view of views) {
link_view_opts.push({
name: `ParentShow:${view.name}.${related_table.name}.${relation.name}`,
label: `${view.name} [${view.viewtemplate} ${relation.name}.${related_table.name}]`,
});
}
}
return link_view_opts;
}
);
/**
* Get Action configuration fields
* @param action
* @param table
* @returns {Promise<*|[{name: string, label: string, type: string}, {name: string, label: string, type: string, sublabel: string}]|[{name: string, label: string, type: string, sublabel: string}]|[{name: string, type: string}]|[{name: string, label: string, type: string}]|*|*[]>}
*/
const getActionConfigFields = async (action, table) =>
typeof action.configFields === "function"
? await action.configFields({ table })
: action.configFields || [];
/**
*
* @type {*|(function(...[*]=): *)}
*/
const field_picker_fields = contract(
is.fun(
is.obj({ table: is_tablely, viewname: is.str }),
is.promise(is.array(fieldlike))
),
async ({ table, viewname, req }) => {
const __ = (...s) => (req ? req.__(...s) : s.join(""));
const fields = await table.getFields();
for (const field of fields) {
if (field.type === "Key") {
field.reftable = await Table.findOne({ name: field.reftable_name });
if (field.reftable) await field.reftable.getFields();
}
}
const boolfields = fields.filter((f) => f.type && f.type.name === "Bool");
const stateActions = getState().actions;
const actions = [
"Delete",
...boolfields.map((f) => `Toggle ${f.name}`),
...Object.keys(stateActions),
];
const triggers = await Trigger.find({
when_trigger: { or: ["API call", "Never"] },
});
triggers.forEach((tr) => {
actions.push(tr.name);
});
const actionConfigFields = [];
for (const [name, action] of Object.entries(stateActions)) {
const cfgFields = await getActionConfigFields(action, table);
for (const field of cfgFields) {
actionConfigFields.push({
...field,
showIf: {
action_name: name,
type: "Action",
...(field.showIf || {}),
},
});
}
}
const fldOptions = fields.map((f) => f.name);
const { field_view_options } = calcfldViewOptions(fields, false);
const fieldViewConfigForms = await calcfldViewConfig(fields, false);
const fvConfigFields = [];
for (const [field_name, fvOptFields] of Object.entries(
fieldViewConfigForms
)) {
for (const [fieldview, formFields] of Object.entries(fvOptFields)) {
for (const formField of formFields) {
fvConfigFields.push({
...formField,
showIf: {
type: "Field",
field_name,
fieldview,
},
});
}
}
}
const link_view_opts = await get_link_view_opts(table, viewname);
const { parent_field_list } = await table.get_parent_relations(true);
const {
child_field_list,
child_relations,
} = await table.get_child_relations();
const aggStatOptions = {};
const agg_field_opts = child_relations.map(({ table, key_field }) => {
aggStatOptions[`${table.name}.${key_field.name}`] = [
"Count",
"Avg",
"Sum",
"Max",
"Min",
];
table.fields.forEach((f) => {
if (f.type && f.type.name === "Date") {
aggStatOptions[`${table.name}.${key_field.name}`].push(
`Latest ${f.name}`
);
}
});
return {
name: `agg_field`,
label: __("On Field"),
type: "String",
required: true,
attributes: {
options: table.fields
.filter((f) => !f.calculated || f.stored)
.map((f) => f.name),
},
showIf: {
agg_relation: `${table.name}.${key_field.name}`,
type: "Aggregation",
},
};
});
return [
{
name: "type",
label: __("Type"),
type: "String",
required: true,
attributes: {
//TODO omit when no options
options: [
{
name: "Field",
label: __(`Field in %s table`, table.name),
},
{ name: "Action", label: __("Action on row") },
...(link_view_opts.length > 0
? [{ name: "ViewLink", label: __("Link to other view") }]
: []),
{ name: "Link", label: __("Link to anywhere") },
...(parent_field_list.length > 0
? [{ name: "JoinField", label: __("Join Field") }]
: []),
...(child_field_list.length > 0
? [{ name: "Aggregation", label: __("Aggregation") }]
: []),
],
},
},
{
name: "field_name",
label: __("Field"),
type: "String",
required: true,
attributes: {
options: fldOptions,
},
showIf: { type: "Field" },
},
{
name: "fieldview",
label: __("Field view"),
type: "String",
required: false,
attributes: {
calcOptions: ["field_name", field_view_options],
},
showIf: { type: "Field" },
},
...fvConfigFields,
{
name: "action_name",
label: __("Action"),
type: "String",
required: true,
attributes: {
options: actions,
},
showIf: { type: "Action" },
},
{
name: "action_label",
label: __("Action Label"),
type: "String",
showIf: { type: "Action" },
},
{
name: "action_label_formula",
label: __("Action label is a formula?"),
type: "Bool",
required: false,
showIf: { type: "Action" },
},
{
name: "action_style",
label: __("Action Style"),
type: "String",
required: true,
attributes: {
options: [
{ name: "btn-primary", label: "Primary button" },
{ name: "btn-secondary", label: "Secondary button" },
{ name: "btn-success", label: "Success button" },
{ name: "btn-danger", label: "Danger button" },
{ name: "btn-outline-primary", label: "Primary outline button" },
{
name: "btn-outline-secondary",
label: "Secondary outline button",
},
{ name: "btn-link", label: "Link" },
],
},
showIf: { type: "Action" },
},
{
name: "action_size",
label: __("Button size"),
type: "String",
required: true,
attributes: {
options: [
{ name: "", label: "Standard" },
{ name: "btn-lg", label: "Large" },
{ name: "btn-sm", label: "Small" },
{ name: "btn-block", label: "Block" },
{ name: "btn-block btn-lg", label: "Large block" },
],
},
showIf: { type: "Action" },
},
{
name: "confirm",
label: __("User confirmation?"),
type: "Bool",
showIf: { type: "Action" },
},
...actionConfigFields,
{
name: "view",
label: __("View"),
type: "String",
required: true,
attributes: {
options: link_view_opts,
},
showIf: { type: "ViewLink" },
},
{
name: "view_label",
label: __("View label"),
sublabel: __("Leave blank for default label."),
type: "String",
required: false,
showIf: { type: "ViewLink" },
},
{
name: "view_label_formula",
label: __("View label is a formula?"),
type: "Bool",
required: false,
showIf: { type: "ViewLink" },
},
{
name: "in_modal",
label: __("Open in popup modal?"),
type: "Bool",
required: false,
showIf: { type: "ViewLink" },
},
{
name: "link_style",
label: __("Link Style"),
type: "String",
required: true,
attributes: {
options: [
{ name: "", label: "Link" },
{ name: "btn btn-primary", label: "Primary button" },
{ name: "btn btn-secondary", label: "Secondary button" },
{ name: "btn btn-success", label: "Success button" },
{ name: "btn btn-danger", label: "Danger button" },
{
name: "btn btn-outline-primary",
label: "Primary outline button",
},
{
name: "btn btn-outline-secondary",
label: "Secondary outline button",
},
],
},
showIf: { type: "ViewLink" },
},
{
name: "link_size",
label: __("Link size"),
type: "String",
required: true,
attributes: {
options: [
{ name: "", label: "Standard" },
{ name: "btn-lg", label: "Large" },
{ name: "btn-sm", label: "Small" },
{ name: "btn-block", label: "Block" },
{ name: "btn-block btn-lg", label: "Large block" },
],
},
showIf: { type: "ViewLink" },
},
{
name: "link_text",
label: __("Link text"),
type: "String",
required: true,
showIf: { type: "Link" },
},
{
name: "link_text_formula",
label: __("Link text is a formula?"),
type: "Bool",
required: false,
showIf: { type: "Link" },
},
{
name: "link_url",
label: __("Link URL"),
type: "String",
required: true,
showIf: { type: "Link" },
},
{
name: "link_url_formula",
label: __("Link URL is a formula?"),
type: "Bool",
required: false,
showIf: { type: "Link" },
},
{
name: "link_target_blank",
label: __("Open in new tab"),
type: "Bool",
required: false,
showIf: { type: "Link" },
},
{
name: "join_field",
label: __("Join Field"),
type: "String",
required: true,
attributes: {
options: parent_field_list,
},
showIf: { type: "JoinField" },
},
{
name: "agg_relation",
label: __("Relation"),
type: "String",
required: true,
attributes: {
options: child_field_list,
},
showIf: { type: "Aggregation" },
},
...agg_field_opts,
{
name: "stat",
label: __("Statistic"),
type: "String",
required: true,
attributes: {
calcOptions: ["agg_relation", aggStatOptions],
},
showIf: { type: "Aggregation" },
},
{
name: "aggwhere",
label: __("Where"),
sublabel: __("Formula"),
type: "String",
required: false,
showIf: { type: "Aggregation" },
},
{
name: "state_field",
label: __("In search form"),
type: "Bool",
showIf: { type: "Field" },
},
{
name: "header_label",
label: __("Header label"),
type: "String",
},
];
}
);
/**
* get_child_views Contract
* @type {*|(function(...[*]=): *)}
*/
const get_child_views = contract(
is.fun(
[is_tablely, is.str],
is.promise(
is.array(
is.obj({
relation: is.class("Field"),
related_table: is.class("Table"),
views: is.array(is.class("View")),
})
)
)
),
async (table, viewname) => {
const rels = await Field.find({ reftable_name: table.name });
var child_views = [];
for (const relation of rels) {
const related_table = await Table.findOne({ id: relation.table_id });
const views = await View.find_table_views_where(
related_table.id,
({ state_fields, viewrow }) =>
viewrow.name !== viewname && state_fields.every((sf) => !sf.required)
);
child_views.push({ relation, related_table, views });
}
return child_views;
}
);
/**
* get_parent_views Contract
* @type {*|(function(...[*]=): *)}
*/
const get_parent_views = contract(
is.fun(
[is_tablely, is.str],
is.promise(
is.array(
is.obj({
relation: is.class("Field"),
related_table: is.class("Table"),
views: is.array(is.class("View")),
})
)
)
),
async (table, viewname) => {
var parent_views = [];
const parentrels = (await table.getFields()).filter(
(f) => f.is_fkey && f.type !== "File"
);
for (const relation of parentrels) {
const related_table = await Table.findOne({
name: relation.reftable_name,
});
const views = await View.find_table_views_where(
related_table,
({ state_fields, viewrow }) =>
viewrow.name !== viewname &&
state_fields.some((sf) => sf.name === "id")
);
parent_views.push({ relation, related_table, views });
}
return parent_views;
}
);
/**
* picked_fields_to_query Contract
* @type {*|(function(...[*]=): *)}
*/
const picked_fields_to_query = contract(
is.fun([is.array(is_column), is.array(is.class("Field"))], is_table_query),
(columns, fields) => {
var joinFields = {};
var aggregations = {};
(columns || []).forEach((column) => {
if (column.type === "JoinField") {
if (column.join_field && column.join_field.split) {
const kpath = column.join_field.split(".");
if (kpath.length === 2) {
const [refNm, targetNm] = kpath;
joinFields[`${refNm}_${targetNm}`] = {
ref: refNm,
target: targetNm,
};
} else {
const [refNm, through, targetNm] = kpath;
joinFields[`${refNm}_${through}_${targetNm}`] = {
ref: refNm,
target: targetNm,
through,
};
}
} else {
throw new InvalidConfiguration(
`Join field is specified as column but no join field is chosen`
);
}
} else if (column.type === "ViewLink") {
if (column.view && column.view.split) {
const [vtype, vrest] = column.view.split(":");
if (vtype === "ParentShow") {
const [pviewnm, ptbl, pfld] = vrest.split(".");
const field = fields.find((f) => f.name === pfld);
if (field && field.attributes.summary_field)
joinFields[`summary_field_${ptbl.toLowerCase()}`] = {
ref: pfld,
target: field.attributes.summary_field,
};
}
}
} else if (column.type === "Aggregation") {
//console.log(column)
if (column.agg_relation && column.agg_relation.split) {
const [table, fld] = column.agg_relation.split(".");
const field = column.agg_field;
const targetNm = (
column.stat.replace(" ", "") +
"_" +
table +
"_" +
fld +
db.sqlsanitize(column.aggwhere || "")
).toLowerCase();
aggregations[targetNm] = {
table,
ref: fld,
where: jsexprToSQL(column.aggwhere),
field,
aggregate: column.stat,
};
}
}
});
return { joinFields, aggregations };
}
);
/**
*
* @type {*|(function(...[*]=): *)}
*/
const stateFieldsToQuery = contract(
is.fun(is.obj(), is.obj()),
({ state, fields, prefix = "" }) => {
let q = {};
const stateKeys = Object.keys(state);
if (state._sortby) {
const field = fields.find((f) => f.name === state._sortby);
//this is ok because it has to match fieldname
if (field) q.orderBy = state._sortby;
if (state._sortdesc) q.orderDesc = true;
}
if (state._pagesize) q.limit = parseInt(state._pagesize);
if (state._pagesize && state._page)
q.offset = (parseInt(state._page) - 1) * parseInt(state._pagesize);
const latNear = stateKeys.find((k) => k.startsWith("_near_lat_"));
const longNear = stateKeys.find((k) => k.startsWith("_near_long_"));
if (latNear && longNear) {
const latField =
prefix + db.sqlsanitize(latNear.replace("_near_lat_", ""));
const longField =
prefix + db.sqlsanitize(longNear.replace("_near_long_", ""));
const lat = parseFloat(state[latNear]);
const long = parseFloat(state[longNear]);
q.orderBy = { distance: { lat, long, latField, longField } };
}
return q;
}
);
/**
*
* @param container
* @param key
* @param x
*/
const addOrCreateList = (container, key, x) => {
if (container[key]) container[key].push(x);
else container[key] = [x];
};
/**
*
* @type {*|(function(...[*]=): *)}
*/
const stateFieldsToWhere = contract(
is.fun(
is.obj({
fields: is.array(is.class("Field")),
approximate: is.maybe(is.bool),
}),
is.obj()
),
({ fields, state, approximate = true }) => {
var qstate = {};
Object.entries(state).forEach(([k, v]) => {
if (k === "_fts") {
qstate[k] = { searchTerm: v.replace(/\0/g, ""), fields };
return;
}
const field = fields.find((fld) => fld.name == k);
if (k.startsWith("_fromdate_")) {
const datefield = db.sqlsanitize(k.replace("_fromdate_", ""));
const dfield = fields.find((fld) => fld.name == datefield);
if (dfield)
addOrCreateList(qstate, datefield, { gt: new Date(v), equal: true });
} else if (k.startsWith("_todate_")) {
const datefield = db.sqlsanitize(k.replace("_todate_", ""));
const dfield = fields.find((fld) => fld.name == datefield);
if (dfield)
addOrCreateList(qstate, datefield, { lt: new Date(v), equal: true });
} else if (
field &&
field.type.name === "String" &&
!(field.attributes && field.attributes.options) &&
approximate
) {
qstate[k] = { ilike: v };
} else if (field && field.type.name === "Bool" && state[k] === "?") {
// omit
} else if (field && field.type && field.type.read)
qstate[k] = field.type.read(v);
else if (field) qstate[k] = v;
else if (k.includes(".")) {
const kpath = k.split(".");
if (kpath.length === 3) {
const [jtNm, jFieldNm, lblField] = kpath;
qstate.id = [
...(qstate.id || []),
{
// where id in (select jFieldNm from jtnm where lblField=v)
inSelect: {
table: `${db.getTenantSchemaPrefix()}"${db.sqlsanitize(jtNm)}"`,
field: db.sqlsanitize(jFieldNm),
where: { [db.sqlsanitize(lblField)]: v },
},
},
];
}
}
});
return qstate;
}
);
/**
* initial_config_all_fields Contract
* @type {*|(function(...[*]=): *)}
*/
const initial_config_all_fields = contract(
is.fun(
is.bool,
is.fun(
is.obj({ table_id: is.posint }),
is.promise(is.obj({ columns: is.array(is.obj()), layout: is.obj() }))
)
),
(isEdit) => async ({ table_id, exttable_name }) => {
const table = await Table.findOne(
table_id ? { id: table_id } : { name: exttable_name }
);
const fields = (await table.getFields()).filter(
(f) => !f.primary_key && (!isEdit || !f.calculated)
);
var cfg = { columns: [] };
var aboves = [null];
fields.forEach((f) => {
const flabel = {
above: [
null,
{
type: "blank",
block: false,
contents: f.label,
textStyle: "",
...(isEdit ? { labelFor: f.name } : {}),
},
],
};
if (
f.is_fkey &&
f.type !== "File" &&
f.reftable_name !== "users" &&
!isEdit
) {
cfg.columns.push({
type: "JoinField",
join_field: `${f.name}.${f.attributes.summary_field}`,
});
aboves.push({
widths: [2, 10],
besides: [
flabel,
{
above: [
null,
{
type: "join_field",
block: false,
textStyle: "",
join_field: `${f.name}.${f.attributes.summary_field}`,
},
],
},
],
});
} else if (f.reftable_name !== "users") {
const fvNm = f.type.fieldviews
? Object.entries(f.type.fieldviews).find(
([nm, fv]) => fv.isEdit === isEdit
)[0]
: f.type === "File" && !isEdit
? Object.keys(getState().fileviews)[0]
: f.type === "File" && isEdit
? "upload"
: f.type === "Key"
? "select"
: undefined;
cfg.columns.push({
field_name: f.name,
type: "Field",
fieldview: fvNm,
state_field: true,
});
aboves.push({
widths: [2, 10],
besides: [
flabel,
{
above: [
null,
{
type: "field",
block: false,
fieldview: fvNm,
textStyle: "",
field_name: f.name,
},
],
},
],
});
}
aboves.push({ type: "line_break" });
});
if (isEdit)
aboves.push({
type: "action",
block: false,
minRole: 10,
action_name: "Save",
});
cfg.layout = { above: aboves };
return cfg;
}
);
/**
*
* @param x
* @returns {number|undefined}
*/
const strictParseInt = (x) => {
const y = +x;
return !isNaN(y) && y ? y : undefined;
};
/**
*
* @param state
* @param fields
* @returns {*}
*/
const readState = (state, fields) => {
fields.forEach((f) => {
const current = state[f.name];
if (typeof current !== "undefined") {
if (f.type.read) state[f.name] = f.type.read(current);
else if (f.type === "Key" || f.type === "File")
state[f.name] =
current === "null" || current === "" || current === null
? null
: getState().types[f.reftype].read(current);
}
});
return state;
};
/**
*
* @param state
* @param fields
* @returns {boolean|*}
*/
const readStateStrict = (state, fields) => {
let hasErrors = false;
fields.forEach((f) => {
const current = state[f.name];
//console.log(f.name, current, typeof current);
if (typeof current !== "undefined") {
if (f.type.read) {
const readval = f.type.read(current);
if (typeof readval === "undefined") {
if (current === "" && !f.required) delete state[f.name];
else hasErrors = true;
}
if (f.type && f.type.validate) {
const vres = f.type.validate(f.attributes || {})(readval);
if (vres.error) hasErrors = true;
}
state[f.name] = readval;
} else if (f.type === "Key" || f.type === "File")
state[f.name] =
current === "null" || current === "" || current === null
? null
: +current;
} else if (f.required && !f.primary_key) hasErrors = true;
});
return hasErrors ? false : state;
};
/**
*
* @param get_json_list
* @param fields0
* @returns {any[]|{child_relations: *[], child_field_list: *[]}|{readonly min_role_read: *, get_child_relations(): {child_relations: [], child_field_list: []}, external: boolean, getFields(): *, owner_fieldname(): null, getJoinedRows(*=): Promise<*|*>, countRows(*): Promise<*>, distinctValues(*): Promise<*>, getRows: ((function(*=, *=): Promise<*|*>)|*), fields, get_parent_relations(): {parent_relations: [], parent_field_list: []}}|{parent_relations: *[], parent_field_list: *[]}|null|*|number|Promise<*|*>}
*/
const json_list_to_external_table = (get_json_list, fields0) => {
const fields = fields0.map((f) =>
f.constructor.name === Object.name ? new Field(f) : f
);
const getRows = async (where = {}, selopts = {}) => {
let data_in = await get_json_list({ where, ...selopts });
const restricts = Object.entries(where);
const data_filtered =
restricts.length === 0
? data_in
: data_in.filter((x) => restricts.every(([k, v]) => x[k] === v));
if (selopts.orderBy) {
const cmp = selopts.orderDesc
? new Function(
"a,b",
`return b.${selopts.orderBy}-a.${selopts.orderBy}`
)
: new Function(
"a,b",
`return a.${selopts.orderBy}-b.${selopts.orderBy}`
);
data_filtered.sort(cmp);
}
if (selopts.limit)
return data_filtered.slice(
selopts.offset || 0,
(selopts.offset || 0) + selopts.limit
);
else return data_filtered;
};
const tbl = {
getFields() {
return fields;
},
fields,
getRows,
get min_role_read() {
const roles = getState().getConfig("exttables_min_role_read", {});
return roles[tbl.name] || 10;
},
getJoinedRows(opts = {}) {
const { where, ...rest } = opts;
return getRows(where || {}, rest || {});
},
async countRows(where) {
let data_in = await get_json_list({ where });
return data_in.length;
},
get_child_relations() {
return { child_relations: [], child_field_list: [] };
},
get_parent_relations() {
return { parent_relations: [], parent_field_list: [] };
},
external: true,
owner_fieldname() {
return null;
},
async distinctValues(fldNm) {
let data_in = await get_json_list({});
const s = new Set(data_in.map((x) => x[fldNm]));
return [...s];
},
};
return tbl;
};
/**
*
* @param col
* @param req
* @param rest
* @returns {Promise<*>}
*/
const run_action_column = async ({ col, req, ...rest }) => {
let state_action = getState().actions[col.action_name];
let configuration;
if (state_action) configuration = col.configuration;
else {
const trigger = await Trigger.findOne({ name: col.action_name });
state_action = getState().actions[trigger.action];
configuration = trigger.configuration;
}
return await state_action.run({
configuration,
user: req.user,
...rest,
});
};
module.exports = {
field_picker_fields,
picked_fields_to_query,
get_child_views,
get_parent_views,
stateFieldsToWhere,
stateFieldsToQuery,
initial_config_all_fields,
calcfldViewOptions,
get_link_view_opts,
is_column,
readState,
readStateStrict,
stateToQueryString,
link_view,
getActionConfigFields,
calcfldViewConfig,
strictParseInt,
run_action_column,
json_list_to_external_table,
};