@saltcorn/server 1.0.0-beta.9 → 1.0.0-rc.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
@@ -309,13 +309,13 @@ router.get(
309
309
  backupForm.values.auto_backup_expire_days = getState().getConfig(
310
310
  "auto_backup_expire_days"
311
311
  );
312
- backupForm.values.backup_with_event_log = getState().getConfig(
312
+ aBackupFilePrefixForm.values.backup_with_event_log = getState().getConfig(
313
313
  "backup_with_event_log"
314
314
  );
315
- backupForm.values.backup_with_system_zip = getState().getConfig(
315
+ aBackupFilePrefixForm.values.backup_with_system_zip = getState().getConfig(
316
316
  "backup_with_system_zip"
317
317
  );
318
- backupForm.values.backup_system_zip_level = getState().getConfig(
318
+ aBackupFilePrefixForm.values.backup_system_zip_level = getState().getConfig(
319
319
  "backup_system_zip_level"
320
320
  );
321
321
  //
@@ -378,7 +378,9 @@ router.get(
378
378
  : { type: "blank", contents: "" },
379
379
  {
380
380
  type: "card",
381
- title: req.__("Snapshots"),
381
+ title:
382
+ req.__("Snapshots") +
383
+ `<a href="javascript:ajax_modal('/admin/help/Snapshots?')"><i class="fas fa-question-circle ms-1"></i></a>`,
382
384
  titleAjaxIndicator: true,
383
385
  contents: div(
384
386
  p(
@@ -673,7 +675,10 @@ router.get(
673
675
  const backup_file_prefix = getState().getConfig("backup_file_prefix");
674
676
  if (
675
677
  !isRoot ||
676
- !(filename.startsWith(backup_file_prefix) && filename.endsWith(".zip"))
678
+ !(
679
+ path.resolve(filename).startsWith(backup_file_prefix) &&
680
+ filename.endsWith(".zip")
681
+ )
677
682
  ) {
678
683
  res.redirect("/admin/backup");
679
684
  return;
@@ -708,6 +713,33 @@ const backupFilePrefixForm = (req) =>
708
713
  sublabel: req.__("Include table history in backup"),
709
714
  default: true,
710
715
  },
716
+ {
717
+ type: "Bool",
718
+ label: req.__("Include Event Logs"),
719
+ sublabel: req.__("Backup with event logs"),
720
+ name: "backup_with_event_log",
721
+ },
722
+ {
723
+ type: "Bool",
724
+ label: req.__("Use system zip"),
725
+ sublabel: req.__(
726
+ "Recommended. Executable <code>zip</code> must be installed"
727
+ ),
728
+ name: "backup_with_system_zip",
729
+ },
730
+ {
731
+ type: "Integer",
732
+ label: req.__("Zip compression level"),
733
+ sublabel: req.__("1=Fast, larger file, 9=Slow, smaller files"),
734
+ name: "backup_system_zip_level",
735
+ attributes: {
736
+ min: 1,
737
+ max: 9,
738
+ },
739
+ showIf: {
740
+ backup_with_system_zip: true,
741
+ },
742
+ },
711
743
  ],
712
744
  });
713
745
 
@@ -835,40 +867,6 @@ const autoBackupForm = (req) => {
835
867
  },
836
868
  ]
837
869
  : []),
838
- {
839
- type: "Bool",
840
- label: req.__("Include Event Logs"),
841
- sublabel: req.__("Backup with event logs"),
842
- name: "backup_with_event_log",
843
- showIf: {
844
- auto_backup_frequency: ["Daily", "Weekly"],
845
- },
846
- },
847
- {
848
- type: "Bool",
849
- label: req.__("Use system zip"),
850
- sublabel: req.__(
851
- "Recommended. Executable <code>zip</code> must be installed"
852
- ),
853
- name: "backup_with_system_zip",
854
- showIf: {
855
- auto_backup_frequency: ["Daily", "Weekly"],
856
- },
857
- },
858
- {
859
- type: "Integer",
860
- label: req.__("Zip compression level"),
861
- sublabel: req.__("1=Fast, larger file, 9=Slow, smaller files"),
862
- name: "backup_system_zip_level",
863
- attributes: {
864
- min: 1,
865
- max: 9,
866
- },
867
- showIf: {
868
- auto_backup_frequency: ["Daily", "Weekly"],
869
- backup_with_system_zip: true,
870
- },
871
- },
872
870
  ],
873
871
  });
874
872
  };
@@ -1270,6 +1268,50 @@ const pullCordovaBuilder = (req, res) => {
1270
1268
  });
1271
1269
  };
1272
1270
 
1271
+ const tryInstallSdNotify = (req, res) => {
1272
+ const child = spawn("npm", ["install", "-g", "sd-notify"], {
1273
+ stdio: ["ignore", "pipe", "pipe"],
1274
+ });
1275
+ return new Promise((resolve, reject) => {
1276
+ child.stdout.on("data", (data) => {
1277
+ res.write(data);
1278
+ });
1279
+ child.stderr?.on("data", (data) => {
1280
+ res.write(data);
1281
+ });
1282
+ child.on("exit", function (code, signal) {
1283
+ resolve(code);
1284
+ });
1285
+ child.on("error", (msg) => {
1286
+ const message = msg.message ? msg.message : msg.code;
1287
+ res.write(req.__("Error: ") + message + "\n");
1288
+ resolve(msg.code);
1289
+ });
1290
+ });
1291
+ };
1292
+
1293
+ const pruneDocker = (req, res) => {
1294
+ const child = spawn("docker", ["image", "prune", "-f"], {
1295
+ stdio: ["ignore", "pipe", "pipe"],
1296
+ });
1297
+ return new Promise((resolve, reject) => {
1298
+ child.stdout.on("data", (data) => {
1299
+ res.write(data);
1300
+ });
1301
+ child.stderr?.on("data", (data) => {
1302
+ res.write(data);
1303
+ });
1304
+ child.on("exit", function (code, signal) {
1305
+ resolve(code);
1306
+ });
1307
+ child.on("error", (msg) => {
1308
+ const message = msg.message ? msg.message : msg.code;
1309
+ res.write(req.__("Error: ") + message + "\n");
1310
+ resolve(msg.code);
1311
+ });
1312
+ });
1313
+ };
1314
+
1273
1315
  /*
1274
1316
  * fetch available saltcorn versions and show a dialog to select one
1275
1317
  */
@@ -1285,6 +1327,7 @@ router.get(
1285
1327
  throw new Error(req.__("Unable to fetch versions"));
1286
1328
  const versions = Object.keys(pkgInfo.versions);
1287
1329
  if (versions.length === 0) throw new Error(req.__("No versions found"));
1330
+ const tags = pkgInfo["dist-tags"] || {};
1288
1331
  res.set("Page-Title", req.__("%s versions", "Saltcorn"));
1289
1332
  let selected = packagejson.version;
1290
1333
  res.send(
@@ -1294,6 +1337,7 @@ router.get(
1294
1337
  method: "post",
1295
1338
  },
1296
1339
  input({ type: "hidden", name: "_csrf", value: req.csrfToken() }),
1340
+ // version select
1297
1341
  div(
1298
1342
  { class: "form-group" },
1299
1343
  label(
@@ -1319,6 +1363,54 @@ router.get(
1319
1363
  )
1320
1364
  )
1321
1365
  ),
1366
+ // tag select
1367
+ div(
1368
+ { class: "form-group" },
1369
+ label(
1370
+ {
1371
+ for: "tag_select",
1372
+ class: "form-label fw-bold",
1373
+ },
1374
+ req.__("Tags")
1375
+ ),
1376
+ select(
1377
+ {
1378
+ id: "tag_select",
1379
+ class: "form-control form-select",
1380
+ },
1381
+ option({
1382
+ id: "empty_opt",
1383
+ value: "",
1384
+ label: req.__("Select tag"),
1385
+ selected: true,
1386
+ }),
1387
+ Object.keys(tags).map((tag) =>
1388
+ option({
1389
+ id: `${tag}_opt`,
1390
+ value: tags[tag],
1391
+ label: `${tag} (${tags[tag]})`,
1392
+ })
1393
+ )
1394
+ )
1395
+ ),
1396
+ // deep clean checkbox
1397
+ div(
1398
+ { class: "form-group" },
1399
+ input({
1400
+ id: "deep_clean",
1401
+ class: "form-check-input",
1402
+ type: "checkbox",
1403
+ name: "deep_clean",
1404
+ checked: false,
1405
+ }),
1406
+ label(
1407
+ {
1408
+ for: "deep_clean",
1409
+ class: "form-label ms-2",
1410
+ },
1411
+ req.__("clean node_modules")
1412
+ )
1413
+ ),
1322
1414
  div(
1323
1415
  { class: "d-flex justify-content-end" },
1324
1416
  button(
@@ -1338,7 +1430,18 @@ router.get(
1338
1430
  req.__("Install")
1339
1431
  )
1340
1432
  )
1341
- )
1433
+ ) +
1434
+ script(
1435
+ domReady(`
1436
+ document.getElementById('tag_select').addEventListener('change', () => {
1437
+ const version = document.getElementById('tag_select').value;
1438
+ if (version) document.getElementById('version_select').value = version;
1439
+ });
1440
+ document.getElementById('version_select').addEventListener('change', () => {
1441
+ document.getElementById('tag_select').value = '';
1442
+ });
1443
+ `)
1444
+ )
1342
1445
  );
1343
1446
  } catch (error) {
1344
1447
  getState().log(
@@ -1350,7 +1453,17 @@ router.get(
1350
1453
  })
1351
1454
  );
1352
1455
 
1353
- const doInstall = async (req, res, version, runPull) => {
1456
+ const cleanNodeModules = async () => {
1457
+ const topSaltcornDir = path.join(__dirname, "..", "..", "..", "..", "..");
1458
+ if (path.basename(topSaltcornDir) === "@saltcorn")
1459
+ await fs.promises.rm(topSaltcornDir, { recursive: true, force: true });
1460
+ else
1461
+ throw new Error(
1462
+ `'${topSaltcornDir}' is not a Saltcorn installation directory`
1463
+ );
1464
+ };
1465
+
1466
+ const doInstall = async (req, res, version, deepClean, runPull) => {
1354
1467
  if (db.getTenantSchema() !== db.connectObj.default_schema) {
1355
1468
  req.flash("error", req.__("Not possible for tenant"));
1356
1469
  res.redirect("/admin");
@@ -1360,6 +1473,14 @@ const doInstall = async (req, res, version, runPull) => {
1360
1473
  ? req.__("Starting upgrade, please wait...\n")
1361
1474
  : req.__("Installing %s, please wait...\n", version)
1362
1475
  );
1476
+ if (deepClean) {
1477
+ res.write(req.__("Cleaning node_modules...\n"));
1478
+ try {
1479
+ await cleanNodeModules();
1480
+ } catch (e) {
1481
+ res.write(req.__("Error cleaning node_modules: %s\n", e.message));
1482
+ }
1483
+ }
1363
1484
  const child = spawn(
1364
1485
  "npm",
1365
1486
  ["install", "-g", `@saltcorn/cli@${version}`, "--unsafe"],
@@ -1374,10 +1495,26 @@ const doInstall = async (req, res, version, runPull) => {
1374
1495
  res.write(data);
1375
1496
  });
1376
1497
  child.on("exit", async function (code, signal) {
1377
- if (code === 0 && runPull) {
1378
- res.write(req.__("Pulling the cordova-builder docker image...") + "\n");
1379
- const pullCode = await pullCordovaBuilder(req, res);
1380
- res.write(req.__("Pull done with code %s", pullCode) + "\n");
1498
+ if (code === 0) {
1499
+ if (deepClean) {
1500
+ res.write(req.__("Installing sd-notify") + "\n");
1501
+ const sdNotifyCode = await tryInstallSdNotify(req, res);
1502
+ res.write(
1503
+ req.__("sd-notify install done with code %s", sdNotifyCode) + "\n"
1504
+ );
1505
+ }
1506
+ if (runPull) {
1507
+ res.write(
1508
+ req.__("Pulling the cordova-builder docker image...") + "\n"
1509
+ );
1510
+ const pullCode = await pullCordovaBuilder(req, res);
1511
+ res.write(req.__("Pull done with code %s", pullCode) + "\n");
1512
+ if (pullCode === 0) {
1513
+ res.write(req.__("Pruning docker...") + "\n");
1514
+ const pruneCode = await pruneDocker(req, res);
1515
+ res.write(req.__("Prune done with code %s", pruneCode) + "\n");
1516
+ }
1517
+ }
1381
1518
  }
1382
1519
  res.end(
1383
1520
  version === "latest"
@@ -1397,8 +1534,8 @@ const doInstall = async (req, res, version, runPull) => {
1397
1534
  };
1398
1535
 
1399
1536
  router.post("/install", isAdmin, async (req, res) => {
1400
- const { version } = req.body;
1401
- await doInstall(req, res, version, false);
1537
+ const { version, deep_clean } = req.body;
1538
+ await doInstall(req, res, version, deep_clean === "on", false);
1402
1539
  });
1403
1540
 
1404
1541
  /**
@@ -1411,7 +1548,7 @@ router.post(
1411
1548
  "/upgrade",
1412
1549
  isAdmin,
1413
1550
  error_catcher(async (req, res) => {
1414
- await doInstall(req, res, "latest", true);
1551
+ await doInstall(req, res, "latest", false, true);
1415
1552
  })
1416
1553
  );
1417
1554
  /**
@@ -1749,9 +1886,10 @@ router.get(
1749
1886
  });
1750
1887
  })
1751
1888
  );
1752
- const buildDialogScript = (cordovaBuilderAvailable) => {
1753
- return `<script>
1889
+ const buildDialogScript = (cordovaBuilderAvailable, isSbadmin2) =>
1890
+ `<script>
1754
1891
  var cordovaBuilderAvailable = ${cordovaBuilderAvailable};
1892
+ var isSbadmin2 = ${isSbadmin2};
1755
1893
  function showEntrySelect(type) {
1756
1894
  for( const currentType of ["view", "page", "pagegroup"]) {
1757
1895
  const tab = $('#' + currentType + 'NavLinkID');
@@ -1776,7 +1914,6 @@ const buildDialogScript = (cordovaBuilderAvailable) => {
1776
1914
  notifyAlert("Building the app, please wait.", true)
1777
1915
  }
1778
1916
  </script>`;
1779
- };
1780
1917
 
1781
1918
  const imageAvailable = async () => {
1782
1919
  try {
@@ -1834,19 +1971,29 @@ router.get(
1834
1971
  const plugins = (await Plugin.find()).filter(
1835
1972
  (plugin) => ["base", "sbadmin2"].indexOf(plugin.name) < 0
1836
1973
  );
1974
+ const pluginsReadyForMobile = plugins
1975
+ .filter((plugin) => plugin.ready_for_mobile())
1976
+ .map((plugin) => plugin.name);
1837
1977
  const builderSettings =
1838
1978
  getState().getConfig("mobile_builder_settings") || {};
1839
1979
  const dockerAvailable = await imageAvailable();
1840
1980
  const xcodeCheckRes = await checkXcodebuild();
1841
1981
  const xcodebuildAvailable = xcodeCheckRes.installed;
1842
1982
  const xcodebuildVersion = xcodeCheckRes.version;
1983
+ const layout = getState().getLayout(req.user);
1984
+ const isSbadmin2 = layout === getState().layouts.sbadmin2;
1843
1985
  send_admin_page({
1844
1986
  res,
1845
1987
  req,
1846
1988
  active_sub: "Mobile app",
1847
1989
  headers: [
1848
1990
  {
1849
- headerTag: buildDialogScript(dockerAvailable),
1991
+ headerTag: buildDialogScript(dockerAvailable, isSbadmin2),
1992
+ },
1993
+ {
1994
+ headerTag: `<script>var pluginsReadyForMobile = ${JSON.stringify(
1995
+ pluginsReadyForMobile
1996
+ )}</script>`,
1850
1997
  },
1851
1998
  ],
1852
1999
  contents: {
@@ -2884,17 +3031,66 @@ router.get(
2884
3031
  })
2885
3032
  );
2886
3033
 
3034
+ const validateBuildDirName = (buildDirName) => {
3035
+ // ensure characters
3036
+ if (!/^[a-zA-Z0-9_-]+$/.test(buildDirName)) {
3037
+ getState().log(
3038
+ 4,
3039
+ `Invalid characters in build directory name '${buildDirName}'`
3040
+ );
3041
+ return false;
3042
+ }
3043
+ // ensure format is 'build_1234567890'
3044
+ if (!/^build_\d+$/.test(buildDirName)) {
3045
+ getState().log(4, `Invalid build directory name format '${buildDirName}'`);
3046
+ return false;
3047
+ }
3048
+ return true;
3049
+ };
3050
+
3051
+ const validateBuildDir = (buildDir, rootPath) => {
3052
+ const resolvedBuildDir = path.resolve(buildDir);
3053
+ if (!resolvedBuildDir.startsWith(path.join(rootPath, "mobile_app"))) {
3054
+ getState().log(4, `Invalid build directory path '${buildDir}'`);
3055
+ return false;
3056
+ }
3057
+ return true;
3058
+ };
3059
+
2887
3060
  router.get(
2888
3061
  "/build-mobile-app/result",
2889
3062
  isAdmin,
2890
3063
  error_catcher(async (req, res) => {
2891
3064
  const { build_dir_name } = req.query;
3065
+ if (!validateBuildDirName(build_dir_name)) {
3066
+ return res.sendWrap(req.__(`Admin`), {
3067
+ above: [
3068
+ {
3069
+ type: "card",
3070
+ title: req.__("Build Result"),
3071
+ contents: div(req.__("Invalid build directory name")),
3072
+ },
3073
+ ],
3074
+ });
3075
+ }
2892
3076
  const rootFolder = await File.rootFolder();
2893
3077
  const buildDir = path.join(
2894
3078
  rootFolder.location,
2895
3079
  "mobile_app",
2896
3080
  build_dir_name
2897
3081
  );
3082
+ if (!validateBuildDir(buildDir, rootFolder.location)) {
3083
+ return res.sendWrap(req.__(`Admin`), {
3084
+ above: [
3085
+ {
3086
+ type: "card",
3087
+ title: req.__("Build Result"),
3088
+ contents: div(req.__("Invalid build directory path")),
3089
+ },
3090
+ ],
3091
+ });
3092
+ }
3093
+
2898
3094
  const files = await Promise.all(
2899
3095
  fs
2900
3096
  .readdirSync(buildDir)
@@ -2947,6 +3143,11 @@ router.post(
2947
3143
  } = req.body;
2948
3144
  if (!includedPlugins) includedPlugins = [];
2949
3145
  if (!synchedTables) synchedTables = [];
3146
+ if (!entryPoint) {
3147
+ return res.json({
3148
+ error: req.__("Please select an entry point."),
3149
+ });
3150
+ }
2950
3151
  if (!androidPlatform && !iOSPlatform) {
2951
3152
  return res.json({
2952
3153
  error: req.__("Please select at least one platform (android or iOS)."),
@@ -3093,8 +3294,8 @@ router.post(
3093
3294
  error_catcher(async (req, res) => {
3094
3295
  const state = getState();
3095
3296
  const child = spawn(
3096
- "docker",
3097
- ["image", "pull", "saltcorn/cordova-builder:latest"],
3297
+ `${process.env.DOCKER_BIN ? `${process.env.DOCKER_BIN}/` : ""}docker`,
3298
+ ["pull", "saltcorn/cordova-builder:latest"],
3098
3299
  {
3099
3300
  stdio: ["ignore", "pipe", "pipe"],
3100
3301
  cwd: ".",
@@ -3545,6 +3746,7 @@ router.get(
3545
3746
  send_admin_page({
3546
3747
  res,
3547
3748
  req,
3749
+ page_title: req.__(`%s code page`, name),
3548
3750
  active_sub: "Development",
3549
3751
  sub2_page: req.__(`%s code page`, name),
3550
3752
  contents: {
package/routes/api.js CHANGED
@@ -27,6 +27,7 @@ const Table = require("@saltcorn/data/models/table");
27
27
  const View = require("@saltcorn/data/models/view");
28
28
  //const Field = require("@saltcorn/data/models/field");
29
29
  const Trigger = require("@saltcorn/data/models/trigger");
30
+ const File = require("@saltcorn/data/models/file");
30
31
  //const load_plugins = require("../load_plugins");
31
32
  const passport = require("passport");
32
33
 
@@ -189,6 +190,39 @@ router.post(
189
190
  )(req, res, next);
190
191
  })
191
192
  );
193
+
194
+ router.get(
195
+ "/serve-files/*",
196
+ //passport.authenticate("api-bearer", { session: false }),
197
+ error_catcher(async (req, res, next) => {
198
+ await passport.authenticate(
199
+ "api-bearer",
200
+ { session: false },
201
+ async function (err, user, info) {
202
+ const role = req?.user?.role_id || user?.role_id || 100;
203
+ const user_id = req?.user?.id || user?.id;
204
+ const serve_path = req.params[0];
205
+ const file = await File.findOne(serve_path);
206
+ if (
207
+ file &&
208
+ (role <= file.min_role_read || (user_id && user_id === file.user_id))
209
+ ) {
210
+ res.type(file.mimetype);
211
+ const cacheability =
212
+ file.min_role_read === 100 ? "public" : "private";
213
+ const maxAge = getState().getConfig("files_cache_maxage", 86400);
214
+ res.set("Cache-Control", `${cacheability}, max-age=${maxAge}`);
215
+ if (file.s3_store)
216
+ res.status(404).json({ error: req.__("Not found") });
217
+ else res.sendFile(file.location);
218
+ } else {
219
+ res.status(404).json({ error: req.__("Not found") });
220
+ }
221
+ }
222
+ )(req, res, next);
223
+ })
224
+ );
225
+
192
226
  /**
193
227
  *
194
228
  */
@@ -294,11 +328,12 @@ router.get(
294
328
  } else {
295
329
  const tbl_fields = table.getFields();
296
330
  readState(req_query, tbl_fields, req);
297
- const qstate = await stateFieldsToWhere({
331
+ const qstate = stateFieldsToWhere({
298
332
  fields: tbl_fields,
299
333
  approximate: !!approximate,
300
334
  state: req_query,
301
335
  table,
336
+ prefix: "a.",
302
337
  });
303
338
  const joinFields = {};
304
339
  const derefs = Array.isArray(dereference)
package/routes/delete.js CHANGED
@@ -38,7 +38,10 @@ router.post(
38
38
  try {
39
39
  if (role <= table.min_role_write)
40
40
  await table.deleteRows({ id }, req.user || { role_id: 100 });
41
- else if (table.ownership_field_id && req.user) {
41
+ else if (
42
+ (table.ownership_field_id || table.ownership_formula) &&
43
+ req.user
44
+ ) {
42
45
  const row = await table.getRow(
43
46
  { id },
44
47
  { forUser: req.user, forPublic: !req.user }
@@ -41,6 +41,7 @@ const {
41
41
  i,
42
42
  th,
43
43
  pre,
44
+ text,
44
45
  } = require("@saltcorn/markup/tags");
45
46
  const Table = require("@saltcorn/data/models/table");
46
47
  const { send_events_page } = require("../markup/admin.js");
@@ -442,7 +443,7 @@ router.get(
442
443
  ) +
443
444
  div(
444
445
  { class: "eventpayload" },
445
- ev.payload ? pre(JSON.stringify(ev.payload, null, 2)) : ""
446
+ ev.payload ? pre(text(JSON.stringify(ev.payload, null, 2))) : ""
446
447
  ),
447
448
  },
448
449
  });
package/routes/fields.js CHANGED
@@ -136,6 +136,9 @@ const fieldForm = async (req, fkey_opts, existing_names, id, hasData) => {
136
136
  sublabel: req.__("Calculated from other fields with a formula"),
137
137
  type: "Bool",
138
138
  disabled: !!id,
139
+ help: {
140
+ topic: "Calculated fields",
141
+ },
139
142
  }),
140
143
  new Field({
141
144
  label: req.__("Required"),
@@ -169,6 +172,9 @@ const fieldForm = async (req, fkey_opts, existing_names, id, hasData) => {
169
172
  type: "Bool",
170
173
  disabled: !!id,
171
174
  showIf: { calculated: true },
175
+ help: {
176
+ topic: "Calculated fields",
177
+ },
172
178
  }),
173
179
  new Field({
174
180
  label: req.__("Protected"),
@@ -176,6 +182,9 @@ const fieldForm = async (req, fkey_opts, existing_names, id, hasData) => {
176
182
  sublabel: req.__("Set role to access"),
177
183
  type: "Bool",
178
184
  showIf: { calculated: false },
185
+ help: {
186
+ topic: "Protected fields",
187
+ },
179
188
  }),
180
189
  {
181
190
  label: req.__("Minimum role to write"),
@@ -919,12 +928,19 @@ router.post(
919
928
  "/test-formula",
920
929
  isAdmin,
921
930
  error_catcher(async (req, res) => {
922
- const { formula, tablename, stored } = req.body;
931
+ let { formula, tablename, stored } = req.body;
932
+ if (stored === "false") stored = false;
933
+
923
934
  const table = Table.findOne({ name: tablename });
924
935
  const fields = table.getFields();
925
936
  const freeVars = freeVariables(formula);
926
937
  const joinFields = {};
927
- if (stored) add_free_variables_to_joinfields(freeVars, joinFields, fields);
938
+ add_free_variables_to_joinfields(freeVars, joinFields, fields);
939
+ if (!stored && Object.keys(joinFields).length > 0) {
940
+ return res
941
+ .status(400)
942
+ .send(`Joinfields only permitted in stored calculated fields`);
943
+ }
928
944
  const rows = await table.getJoinedRows({
929
945
  joinFields,
930
946
  orderBy: "RANDOM()",
@@ -950,9 +966,9 @@ router.post(
950
966
  );
951
967
  } catch (e) {
952
968
  console.error(e);
953
- return res.send(
954
- `Error on running on row with id=${rows[0].id}: ${e.message}`
955
- );
969
+ return res
970
+ .status(400)
971
+ .send(`Error on running on row with id=${rows[0].id}: ${e.message}`);
956
972
  }
957
973
  })
958
974
  );
package/routes/files.js CHANGED
@@ -94,11 +94,15 @@ router.get(
94
94
  const { dir, no_subdirs } = req.query;
95
95
  const noSubdirs = no_subdirs === "true";
96
96
  const safeDir = File.normalise(dir || "/");
97
- const absFolder = path.join(
97
+ const absFolder = File.normalise_in_base(
98
98
  db.connectObj.file_store,
99
99
  db.getTenantSchema(),
100
- safeDir
100
+ dir || "/"
101
101
  );
102
+ if (absFolder === null) {
103
+ res.json({ error: "Invalid path" });
104
+ return;
105
+ }
102
106
  const dirOnDisk = await File.from_file_on_disk(
103
107
  path.basename(absFolder),
104
108
  path.dirname(absFolder)
@@ -594,6 +598,9 @@ router.get(
594
598
  isAdmin,
595
599
  error_catcher(async (req, res) => {
596
600
  const form = await storage_form(req);
601
+ form.blurb = [
602
+ `<div class="alert alert-warning">S3 storage options may not work for this release. Enabling S3 storage is not recommended</div>`,
603
+ ];
597
604
  send_files_page({
598
605
  res,
599
606
  req,