@saltcorn/server 0.9.0-beta.0 → 0.9.0-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/routes/admin.js CHANGED
@@ -285,6 +285,9 @@ router.get(
285
285
  backupForm.values.auto_backup_expire_days = getState().getConfig(
286
286
  "auto_backup_expire_days"
287
287
  );
288
+ backupForm.values.backup_with_event_log = getState().getConfig(
289
+ "backup_with_event_log"
290
+ );
288
291
  //
289
292
  const aSnapshotForm = snapshotForm(req);
290
293
  aSnapshotForm.values.snapshots_enabled =
@@ -721,6 +724,15 @@ const autoBackupForm = (req) =>
721
724
  auto_backup_destination: "Local directory",
722
725
  },
723
726
  },
727
+ {
728
+ type: "Bool",
729
+ label: req.__("Include Event Logs"),
730
+ sublabel: req.__("Backup with event logs"),
731
+ name: "backup_with_event_log",
732
+ showIf: {
733
+ auto_backup_frequency: ["Daily", "Weekly"],
734
+ },
735
+ },
724
736
  ],
725
737
  });
726
738
 
@@ -1514,6 +1526,8 @@ router.get(
1514
1526
  const plugins = (await Plugin.find()).filter(
1515
1527
  (plugin) => ["base", "sbadmin2"].indexOf(plugin.name) < 0
1516
1528
  );
1529
+ const builderSettings =
1530
+ getState().getConfig("mobile_builder_settings") || {};
1517
1531
  send_admin_page({
1518
1532
  res,
1519
1533
  req,
@@ -1543,7 +1557,7 @@ router.get(
1543
1557
  input({
1544
1558
  type: "hidden",
1545
1559
  name: "entryPointType",
1546
- value: "view",
1560
+ value: builderSettings.entryPointType || "view",
1547
1561
  id: "entryPointTypeID",
1548
1562
  }),
1549
1563
  div(
@@ -1574,7 +1588,15 @@ router.get(
1574
1588
  onClick: "showEntrySelect('view')",
1575
1589
  },
1576
1590
  div(
1577
- { class: "nav-link active", id: "viewNavLinkID" },
1591
+ {
1592
+ class: `nav-link ${
1593
+ !builderSettings.entryPointType ||
1594
+ builderSettings.entryPointType === "view"
1595
+ ? "active"
1596
+ : ""
1597
+ }`,
1598
+ id: "viewNavLinkID",
1599
+ },
1578
1600
  req.__("View")
1579
1601
  )
1580
1602
  ),
@@ -1584,7 +1606,14 @@ router.get(
1584
1606
  onClick: "showEntrySelect('page')",
1585
1607
  },
1586
1608
  div(
1587
- { class: "nav-link", id: "pageNavLinkID" },
1609
+ {
1610
+ class: `nav-link ${
1611
+ builderSettings.entryPointType === "page"
1612
+ ? "active"
1613
+ : ""
1614
+ }`,
1615
+ id: "pageNavLinkID",
1616
+ },
1588
1617
  req.__("Page")
1589
1618
  )
1590
1619
  )
@@ -1592,25 +1621,56 @@ router.get(
1592
1621
  // select entry-view
1593
1622
  select(
1594
1623
  {
1595
- class: "form-select",
1596
- name: "entryPoint",
1624
+ class: `form-select ${
1625
+ builderSettings.entryPointType === "page"
1626
+ ? "d-none"
1627
+ : ""
1628
+ }`,
1629
+ ...(!builderSettings.entryPointType ||
1630
+ builderSettings.entryPointType === "view"
1631
+ ? { name: "entryPoint" }
1632
+ : {}),
1597
1633
  id: "viewInputID",
1598
1634
  },
1599
1635
  views
1600
1636
  .map((view) =>
1601
- option({ value: view.name }, view.name)
1637
+ option(
1638
+ {
1639
+ value: view.name,
1640
+ selected:
1641
+ builderSettings.entryPointType === "view" &&
1642
+ builderSettings.entryPoint === view.name,
1643
+ },
1644
+ view.name
1645
+ )
1602
1646
  )
1603
1647
  .join(",")
1604
1648
  ),
1605
1649
  // select entry-page
1606
1650
  select(
1607
1651
  {
1608
- class: "form-select d-none",
1652
+ class: `form-select ${
1653
+ !builderSettings.entryPointType ||
1654
+ builderSettings.entryPointType === "view"
1655
+ ? "d-none"
1656
+ : ""
1657
+ }`,
1658
+ ...(builderSettings.entryPointType === "page"
1659
+ ? { name: "entryPoint" }
1660
+ : {}),
1609
1661
  id: "pageInputID",
1610
1662
  },
1611
1663
  pages
1612
1664
  .map((page) =>
1613
- option({ value: page.name }, page.name)
1665
+ option(
1666
+ {
1667
+ value: page.name,
1668
+ selected:
1669
+ builderSettings.entryPointType === "page" &&
1670
+ builderSettings.entryPoint === page.name,
1671
+ },
1672
+ page.name
1673
+ )
1614
1674
  )
1615
1675
  .join("")
1616
1676
  )
@@ -1631,6 +1691,7 @@ router.get(
1631
1691
  name: "androidPlatform",
1632
1692
  id: "androidCheckboxId",
1633
1693
  onClick: "toggle_android_platform()",
1694
+ checked: builderSettings.androidPlatform === "on",
1634
1695
  })
1635
1696
  )
1636
1697
  ),
@@ -1645,6 +1706,7 @@ router.get(
1645
1706
  class: "form-check-input",
1646
1707
  name: "iOSPlatform",
1647
1708
  id: "iOSCheckboxId",
1709
+ checked: builderSettings.iOSPlatform === "on",
1648
1710
  })
1649
1711
  )
1650
1712
  )
@@ -1658,7 +1720,8 @@ router.get(
1658
1720
  class: "form-check-input",
1659
1721
  name: "useDocker",
1660
1722
  id: "dockerCheckboxId",
1661
- hidden: true,
1723
+ hidden: builderSettings.androidPlatform !== "on",
1724
+ checked: builderSettings.useDocker === "on",
1662
1725
  })
1663
1726
  )
1664
1727
  ),
@@ -1680,6 +1743,7 @@ router.get(
1680
1743
  name: "appName",
1681
1744
  id: "appNameInputId",
1682
1745
  placeholder: "SaltcornMobileApp",
1746
+ value: builderSettings.appName || "",
1683
1747
  })
1684
1748
  )
1685
1749
  ),
@@ -1701,6 +1765,7 @@ router.get(
1701
1765
  name: "appVersion",
1702
1766
  id: "appVersionInputId",
1703
1767
  placeholder: "1.0.0",
1768
+ value: builderSettings.appVersion || "",
1704
1769
  })
1705
1770
  )
1706
1771
  ),
@@ -1721,6 +1786,7 @@ router.get(
1721
1786
  class: "form-control",
1722
1787
  name: "serverURL",
1723
1788
  id: "serverURLInputId",
1789
+ value: builderSettings.serverURL || "",
1724
1790
  placeholder: getState().getConfig("base_url") || "",
1725
1791
  })
1726
1792
  )
@@ -1746,7 +1812,14 @@ router.get(
1746
1812
  [
1747
1813
  option({ value: "" }, ""),
1748
1814
  ...images.map((image) =>
1749
- option({ value: image.location }, image.filename)
1815
+ option(
1816
+ {
1817
+ value: image.location,
1818
+ selected:
1819
+ builderSettings.appIcon === image.location,
1820
+ },
1821
+ image.filename
1822
+ )
1750
1823
  ),
1751
1824
  ].join("")
1752
1825
  )
@@ -1772,7 +1845,14 @@ router.get(
1772
1845
  [
1773
1846
  option({ value: "" }, ""),
1774
1847
  ...pages.map((page) =>
1775
- option({ value: page.name }, page.name)
1848
+ option(
1849
+ {
1850
+ value: page.name,
1851
+ selected:
1852
+ builderSettings.splashPage === page.name,
1853
+ },
1854
+ page.name
1855
+ )
1776
1856
  ),
1777
1857
  ].join("")
1778
1858
  )
@@ -1788,8 +1868,7 @@ router.get(
1788
1868
  id: "autoPublLoginId",
1789
1869
  class: "form-check-input me-2",
1790
1870
  name: "autoPublicLogin",
1791
- value: "autoPublicLogin",
1792
- checked: false,
1871
+ checked: builderSettings.autoPublicLogin === "on",
1793
1872
  }),
1794
1873
  label(
1795
1874
  {
@@ -1810,9 +1889,8 @@ router.get(
1810
1889
  id: "offlineModeBoxId",
1811
1890
  class: "form-check-input me-2",
1812
1891
  name: "allowOfflineMode",
1813
- value: "allowOfflineMode",
1814
1892
  onClick: "toggle_tbl_sync()",
1815
- checked: true,
1893
+ checked: builderSettings.allowOfflineMode === "on",
1816
1894
  }),
1817
1895
  label(
1818
1896
  {
@@ -1828,6 +1906,7 @@ router.get(
1828
1906
  {
1829
1907
  id: "tblSyncSelectorId",
1830
1908
  class: "row pb-3",
1909
+ hidden: builderSettings.allowOfflineMode !== "on",
1831
1910
  },
1832
1911
  div(
1833
1912
  label(
@@ -1864,6 +1943,12 @@ router.get(
1864
1943
  id: `${table.name}_unsynched_opt`,
1865
1944
  value: table.name,
1866
1945
  label: table.name,
1946
+ hidden:
1947
+ builderSettings.synchedTables?.indexOf(
1948
+ table.name
1949
+ ) >= 0
1950
+ ? true
1951
+ : false,
1867
1952
  })
1868
1953
  )
1869
1954
  )
@@ -1908,7 +1993,12 @@ router.get(
1908
1993
  id: `${table.name}_synched_opt`,
1909
1994
  value: table.name,
1910
1995
  label: table.name,
1911
- hidden: "true",
1996
+ hidden:
1997
+ builderSettings.synchedTables?.indexOf(
1998
+ table.name
1999
+ ) >= 0
2000
+ ? false
2001
+ : true,
1912
2002
  })
1913
2003
  )
1914
2004
  )
@@ -1954,7 +2044,12 @@ router.get(
1954
2044
  id: `${plugin.name}_excluded_opt`,
1955
2045
  value: plugin.name,
1956
2046
  label: plugin.name,
1957
- hidden: "true",
2047
+ hidden:
2048
+ builderSettings.excludedPlugins?.indexOf(
2049
+ plugin.name
2050
+ ) >= 0
2051
+ ? false
2052
+ : true,
1958
2053
  })
1959
2054
  )
1960
2055
  )
@@ -1999,7 +2094,12 @@ router.get(
1999
2094
  id: `${plugin.name}_included_opt`,
2000
2095
  value: plugin.name,
2001
2096
  label: plugin.name,
2002
- // hidden: "true",
2097
+ hidden:
2098
+ builderSettings.excludedPlugins?.indexOf(
2099
+ plugin.name
2100
+ ) >= 0
2101
+ ? true
2102
+ : false,
2003
2103
  })
2004
2104
  )
2005
2105
  )
@@ -2116,6 +2216,8 @@ router.post(
2116
2216
  synchedTables,
2117
2217
  includedPlugins,
2118
2218
  } = req.body;
2219
+ if (!includedPlugins) includedPlugins = [];
2220
+ if (!synchedTables) synchedTables = [];
2119
2221
  if (!androidPlatform && !iOSPlatform) {
2120
2222
  return res.json({
2121
2223
  error: req.__("Please select at least one platform (android or iOS)."),
@@ -2167,9 +2269,9 @@ router.post(
2167
2269
  if (splashPage) spawnParams.push("--splashPage", splashPage);
2168
2270
  if (allowOfflineMode) spawnParams.push("--allowOfflineMode");
2169
2271
  if (autoPublicLogin) spawnParams.push("--autoPublicLogin");
2170
- if (synchedTables?.length > 0)
2272
+ if (synchedTables.length > 0)
2171
2273
  spawnParams.push("--synchedTables", ...synchedTables.map((tbl) => tbl));
2172
- if (includedPlugins?.length > 0)
2274
+ if (includedPlugins.length > 0)
2173
2275
  spawnParams.push(
2174
2276
  "--includedPlugins",
2175
2277
  ...includedPlugins.map((pluginName) => pluginName)
@@ -2180,6 +2282,30 @@ router.post(
2180
2282
  ) {
2181
2283
  spawnParams.push("--tenantAppName", db.getTenantSchema());
2182
2284
  }
2285
+ const excludedPlugins = (await Plugin.find())
2286
+ .filter(
2287
+ (plugin) =>
2288
+ ["base", "sbadmin2"].indexOf(plugin.name) < 0 &&
2289
+ includedPlugins.indexOf(plugin.name) < 0
2290
+ )
2291
+ .map((plugin) => plugin.name);
2292
+ await getState().setConfig("mobile_builder_settings", {
2293
+ entryPoint,
2294
+ entryPointType,
2295
+ androidPlatform,
2296
+ iOSPlatform,
2297
+ useDocker,
2298
+ appName,
2299
+ appVersion,
2300
+ appIcon,
2301
+ serverURL,
2302
+ splashPage,
2303
+ autoPublicLogin,
2304
+ allowOfflineMode,
2305
+ synchedTables: synchedTables,
2306
+ includedPlugins: includedPlugins,
2307
+ excludedPlugins,
2308
+ });
2183
2309
  // end http call, return the out directory name
2184
2310
  // the gui polls for results
2185
2311
  res.json({ build_dir_name: outDirName });
package/routes/fields.js CHANGED
@@ -409,7 +409,7 @@ const fieldFlow = (req) =>
409
409
  instance_options[model.name].push(...instances.map((i) => i.name));
410
410
 
411
411
  const outputs = await applyAsync(
412
- model.templateObj.prediction_outputs || [],
412
+ model.templateObj?.prediction_outputs || [], // unit tests can have templateObj undefined
413
413
  { table, configuration: model.configuration }
414
414
  );
415
415
  output_options[model.name] = outputs.map((o) => o.name);
@@ -840,6 +840,11 @@ router.post(
840
840
  const table = Table.findOne({ name: tableName });
841
841
  const role = req.user && req.user.id ? req.user.role_id : 100;
842
842
 
843
+ getState().log(
844
+ 5,
845
+ `Route /fields/show-calculated/${tableName}/${fieldName}/${fieldview} user=${req.user?.id}`
846
+ );
847
+
843
848
  const fields = table.getFields();
844
849
  let row = { ...req.body };
845
850
  if (row && Object.keys(row).length > 0) readState(row, fields);
@@ -1018,6 +1023,13 @@ router.post(
1018
1023
  const { tableName, fieldName, fieldview } = req.params;
1019
1024
  const table = Table.findOne({ name: tableName });
1020
1025
  const fields = table.getFields();
1026
+ const state = getState();
1027
+
1028
+ state.log(
1029
+ 5,
1030
+ `Route /fields/preview/${tableName}/${fieldName}/${fieldview} user=${req.user?.id}`
1031
+ );
1032
+
1021
1033
  let field, row, value;
1022
1034
  if (fieldName.includes(".")) {
1023
1035
  const [refNm, targetNm] = fieldName.split(".");
@@ -1048,9 +1060,9 @@ router.post(
1048
1060
  }
1049
1061
  const fieldviews =
1050
1062
  field.type === "Key"
1051
- ? getState().keyFieldviews
1063
+ ? state.keyFieldviews
1052
1064
  : field.type === "File"
1053
- ? getState().fileviews
1065
+ ? state.fileviews
1054
1066
  : field.type.fieldviews;
1055
1067
  if (!field.type || !fieldviews) {
1056
1068
  res.send("");
package/routes/menu.js CHANGED
@@ -44,7 +44,7 @@ const menuForm = async (req) => {
44
44
  const views = await View.find({}, { orderBy: "name", nocase: true });
45
45
  const pages = await Page.find({}, { orderBy: "name", nocase: true });
46
46
  const roles = await User.get_roles();
47
- const tables = await Table.find({});
47
+ const tables = await Table.find_with_external({});
48
48
  const dynTableOptions = tables.map((t) => t.name);
49
49
  const dynOrderFieldOptions = {},
50
50
  dynSectionFieldOptions = {};
package/routes/packs.js CHANGED
@@ -6,18 +6,19 @@
6
6
 
7
7
  const Router = require("express-promise-router");
8
8
  const { isAdmin, error_catcher } = require("./utils.js");
9
- const { mkTable, renderForm, link, post_btn } = require("@saltcorn/markup");
10
- const { getState } = require("@saltcorn/data/db/state");
9
+ const { renderForm } = require("@saltcorn/markup");
11
10
  const Table = require("@saltcorn/data/models/table");
12
11
  const Form = require("@saltcorn/data/models/form");
13
12
  const View = require("@saltcorn/data/models/view");
14
- const Field = require("@saltcorn/data/models/field");
15
13
  const Plugin = require("@saltcorn/data/models/plugin");
16
14
  const Page = require("@saltcorn/data/models/page");
15
+ const Tag = require("@saltcorn/data/models/tag");
16
+ const EventLog = require("@saltcorn/data/models/eventlog");
17
+ const Model = require("@saltcorn/data/models/model");
18
+ const ModelInstance = require("@saltcorn/data/models/model_instance");
17
19
  const load_plugins = require("../load_plugins");
18
20
 
19
21
  const { is_pack } = require("@saltcorn/data/contracts");
20
- const { contract, is } = require("contractis");
21
22
  const {
22
23
  table_pack,
23
24
  view_pack,
@@ -26,15 +27,20 @@ const {
26
27
  role_pack,
27
28
  library_pack,
28
29
  trigger_pack,
30
+ tag_pack,
31
+ model_pack,
32
+ model_instance_pack,
29
33
  install_pack,
30
34
  fetch_pack_by_name,
31
35
  can_install_pack,
32
36
  uninstall_pack,
37
+ event_log_pack,
33
38
  } = require("@saltcorn/admin-models/models/pack");
34
- const { h5, pre, code, p, text, text_attr } = require("@saltcorn/markup/tags");
39
+ const { pre, code, p, text, text_attr } = require("@saltcorn/markup/tags");
35
40
  const Library = require("@saltcorn/data/models/library");
36
41
  const Trigger = require("@saltcorn/data/models/trigger");
37
42
  const Role = require("@saltcorn/data/models/role");
43
+ const fs = require("fs");
38
44
 
39
45
  /**
40
46
  * @type {object}
@@ -98,6 +104,52 @@ router.get(
98
104
  name: `role.${l.role}`,
99
105
  type: "Bool",
100
106
  }));
107
+ const tags = await Tag.find({});
108
+ const tagFields = tags.map((t) => ({
109
+ label: `${t.name} tag`,
110
+ name: `tag.${t.name}`,
111
+ type: "Bool",
112
+ }));
113
+ const models = await Model.find({});
114
+ const modelFields = models.map((m) => {
115
+ const modelTbl = Table.findOne({ id: m.table_id });
116
+ return {
117
+ label: `${m.name} model, table: ${
118
+ modelTbl.name || req.__("Table not found")
119
+ }`,
120
+ name: `model.${m.name}.${modelTbl.name}`,
121
+ type: "Bool",
122
+ };
123
+ });
124
+ const modelInstances = await ModelInstance.find({});
125
+ const modelInstanceFields = (
126
+ await Promise.all(
127
+ modelInstances.map(async (instance) => {
128
+ const model = await Model.findOne({ id: instance.model_id });
129
+ if (!model) {
130
+ req.flash(
131
+ "warning",
132
+ req.__(`Model with '${instance.model_id}' not found`)
133
+ );
134
+ return null;
135
+ }
136
+ const mTable = await Table.findOne({ id: model.table_id });
137
+ if (!mTable) {
138
+ req.flash(
139
+ "warning",
140
+ req.__(`Table of model '${model.name}' not found`)
141
+ );
142
+ return null;
143
+ }
144
+ return {
145
+ label: `${instance.name} model instance, model: ${model.name}, table: ${mTable.name}`,
146
+ name: `model_instance.${instance.name}.${model.name}.${mTable.name}`,
147
+ type: "Bool",
148
+ };
149
+ })
150
+ )
151
+ ).filter((f) => f);
152
+
101
153
  const form = new Form({
102
154
  action: "/packs/create",
103
155
  fields: [
@@ -108,6 +160,14 @@ router.get(
108
160
  ...trigFields,
109
161
  ...roleFields,
110
162
  ...libFields,
163
+ ...tagFields,
164
+ ...modelFields,
165
+ ...modelInstanceFields,
166
+ {
167
+ name: "with_event_logs",
168
+ label: req.__("Include Event Logs"),
169
+ type: "Bool",
170
+ },
111
171
  ],
112
172
  });
113
173
  res.sendWrap(req.__(`Create Pack`), {
@@ -140,7 +200,7 @@ router.post(
140
200
  "/create",
141
201
  isAdmin,
142
202
  error_catcher(async (req, res) => {
143
- var pack = {
203
+ const pack = {
144
204
  tables: [],
145
205
  views: [],
146
206
  plugins: [],
@@ -148,9 +208,13 @@ router.post(
148
208
  roles: [],
149
209
  library: [],
150
210
  triggers: [],
211
+ tags: [],
212
+ models: [],
213
+ model_instances: [],
214
+ event_logs: [],
151
215
  };
152
216
  for (const k of Object.keys(req.body)) {
153
- const [type, name] = k.split(".");
217
+ const [type, name, ...rest] = k.split(".");
154
218
  switch (type) {
155
219
  case "table":
156
220
  pack.tables.push(await table_pack(name));
@@ -173,7 +237,32 @@ router.post(
173
237
  case "trigger":
174
238
  pack.triggers.push(await trigger_pack(name));
175
239
  break;
176
-
240
+ case "tag":
241
+ pack.tags.push(await tag_pack(name));
242
+ break;
243
+ case "model": {
244
+ const table = rest[0];
245
+ if (!table) throw new Error(`Table for model '${name}' not found`);
246
+ pack.models.push(await model_pack(name, table));
247
+ break;
248
+ }
249
+ case "model_instance": {
250
+ const model = rest[0];
251
+ if (!model)
252
+ throw new Error(`Model of Model Instance '${name}' not found`);
253
+ const table = rest[1];
254
+ if (!table) throw new Error(`Table of Model '${model}' not found`);
255
+ pack.model_instances.push(
256
+ await model_instance_pack(name, model, table)
257
+ );
258
+ break;
259
+ }
260
+ case "with_event_logs":
261
+ const logs = await EventLog.find({});
262
+ pack.event_logs = await Promise.all(
263
+ logs.map(async (l) => await event_log_pack(l))
264
+ );
265
+ break;
177
266
  default:
178
267
  break;
179
268
  }
@@ -217,11 +306,33 @@ const install_pack_form = (req) =>
217
306
  action: "/packs/install",
218
307
  submitLabel: req.__("Install"),
219
308
  fields: [
309
+ {
310
+ name: "source",
311
+ label: req.__("Source"),
312
+ type: "String",
313
+ attributes: {
314
+ options: [
315
+ { label: "from text", name: "from_text" },
316
+ { label: "from file", name: "from_file" },
317
+ ],
318
+ },
319
+ default: "from_text",
320
+ required: true,
321
+ },
220
322
  {
221
323
  name: "pack",
222
324
  label: req.__("Pack"),
223
325
  type: "String",
224
326
  fieldview: "textarea",
327
+ showIf: { source: "from_text" },
328
+ },
329
+ {
330
+ name: "pack_file",
331
+ label: req.__("Pack file"),
332
+ class: "form-control",
333
+ type: "File",
334
+ sublabel: req.__("Upload a pack file"),
335
+ showIf: { source: "from_file" },
225
336
  },
226
337
  ],
227
338
  });
@@ -267,8 +378,22 @@ router.post(
267
378
  isAdmin,
268
379
  error_catcher(async (req, res) => {
269
380
  var pack, error;
381
+ const source = req.body.source || "from_text";
270
382
  try {
271
- pack = JSON.parse(req.body.pack);
383
+ switch (source) {
384
+ case "from_text":
385
+ pack = JSON.parse(req.body.pack);
386
+ break;
387
+ case "from_file":
388
+ if (req.files?.pack_file?.tempFilePath)
389
+ pack = JSON.parse(
390
+ fs.readFileSync(req.files?.pack_file?.tempFilePath)
391
+ );
392
+ else throw new Error(req.__("No file uploaded"));
393
+ break;
394
+ default:
395
+ throw new Error(req.__("Invalid source"));
396
+ }
272
397
  } catch (e) {
273
398
  error = e.message;
274
399
  }