@saltcorn/server 1.0.0-beta.8 → 1.0.0-rc.1

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/plugins.js CHANGED
@@ -49,6 +49,8 @@ const {
49
49
  input,
50
50
  label,
51
51
  text,
52
+ script,
53
+ domReady,
52
54
  } = require("@saltcorn/markup/tags");
53
55
  const { search_bar } = require("@saltcorn/markup/helpers");
54
56
  const fs = require("fs");
@@ -59,6 +61,10 @@ const { sleep, removeNonWordChars } = require("@saltcorn/data/utils");
59
61
  const { loadAllPlugins } = require("../load_plugins");
60
62
  const npmFetch = require("npm-registry-fetch");
61
63
  const PluginInstaller = require("@saltcorn/plugins-loader/plugin_installer");
64
+ const {
65
+ supportedVersion,
66
+ isVersionSupported,
67
+ } = require("@saltcorn/plugins-loader/stable_versioning");
62
68
 
63
69
  /**
64
70
  * @type {object}
@@ -177,6 +183,8 @@ const get_store_items = async () => {
177
183
  has_auth: plugin.has_auth,
178
184
  unsafe: plugin.unsafe,
179
185
  source: plugin.source,
186
+ ready_for_mobile:
187
+ plugin.ready_for_mobile && plugin.ready_for_mobile(plugin.name),
180
188
  }))
181
189
  .filter((p) => !p.unsafe || isRoot || tenants_unsafe_plugins);
182
190
  const local_logins = installed_plugins
@@ -190,6 +198,8 @@ const get_store_items = async () => {
190
198
  github: plugin.source === "github",
191
199
  git: plugin.source === "git",
192
200
  local: plugin.source === "local",
201
+ ready_for_mobile:
202
+ plugin.ready_for_mobile && plugin.ready_for_mobile(plugin.name),
193
203
  }));
194
204
 
195
205
  const pack_items = packs_available.map((pack) => ({
@@ -274,7 +284,8 @@ const store_item_html = (req) => (item) => ({
274
284
  item.github && badge("GitHub"),
275
285
  item.git && badge("Git"),
276
286
  item.local && badge(req.__("Local")),
277
- item.installed && badge(req.__("Installed"))
287
+ item.installed && badge(req.__("Installed")),
288
+ item.ready_for_mobile && badge(req.__("Mobile"))
278
289
  ),
279
290
  div(item.description || ""),
280
291
  item.documentation_link
@@ -584,7 +595,9 @@ router.get(
584
595
  error_catcher(async (req, res) => {
585
596
  const { name } = req.params;
586
597
  const withoutOrg = name.replace(/^@saltcorn\//, "");
587
- const plugin = await Plugin.store_by_name(decodeURIComponent(withoutOrg));
598
+ let plugin = await Plugin.store_by_name(decodeURIComponent(withoutOrg));
599
+ if (!plugin)
600
+ plugin = await Plugin.findOne({ name: decodeURIComponent(name) });
588
601
  if (!plugin) {
589
602
  getState().log(
590
603
  2,
@@ -603,12 +616,14 @@ router.get(
603
616
  res.set("Page-Title", req.__("%s versions", text(withoutOrg)));
604
617
  const versions = Object.keys(pkgInfo.versions);
605
618
  if (versions.length === 0) throw new Error(req.__("No versions found"));
619
+ const tags = pkgInfo["dist-tags"] || {};
606
620
  let selected = null;
607
621
  if (getState().plugins[plugin.name]) {
608
622
  const mod = await load_plugins.requirePlugin(plugin);
609
623
  if (mod) selected = mod.version;
610
624
  }
611
625
  if (!selected) selected = versions[versions.length - 1];
626
+ const scVersion = getState().scVersion;
612
627
  return res.send(
613
628
  form(
614
629
  {
@@ -618,6 +633,7 @@ router.get(
618
633
  input({ type: "hidden", name: "_csrf", value: req.csrfToken() }),
619
634
  div(
620
635
  { class: "form-group" },
636
+ // version
621
637
  label(
622
638
  {
623
639
  for: "version_select",
@@ -631,14 +647,49 @@ router.get(
631
647
  class: "form-control form-select",
632
648
  name: "version",
633
649
  },
634
- versions.map((version) =>
635
- option({
636
- id: `${version}_opt`,
637
- value: version,
638
- label: version,
639
- selected: version === selected,
640
- })
641
- )
650
+ versions
651
+ .filter((v) =>
652
+ isVersionSupported(v, pkgInfo.versions, scVersion)
653
+ )
654
+ .map((version) =>
655
+ option({
656
+ id: `${version}_opt`,
657
+ value: version,
658
+ label: version,
659
+ selected: version === selected,
660
+ })
661
+ )
662
+ ),
663
+ // tag
664
+ label(
665
+ {
666
+ for: "tag_select",
667
+ class: "form-label fw-bold mt-2",
668
+ },
669
+ req.__("Tags")
670
+ ),
671
+ select(
672
+ {
673
+ id: "tag_select",
674
+ class: "form-control form-select",
675
+ },
676
+ option({
677
+ id: "empty_opt",
678
+ value: "",
679
+ label: req.__("Select tag"),
680
+ selected: true,
681
+ }),
682
+ Object.keys(tags)
683
+ .filter((tag) =>
684
+ isVersionSupported(tags[tag], pkgInfo.versions, scVersion)
685
+ )
686
+ .map((tag) =>
687
+ option({
688
+ id: `${tag}_opt`,
689
+ value: tags[tag],
690
+ label: `${tag} (${tags[tag]})`,
691
+ })
692
+ )
642
693
  )
643
694
  ),
644
695
  div(
@@ -660,7 +711,19 @@ router.get(
660
711
  req.__("Install")
661
712
  )
662
713
  )
663
- )
714
+ ) +
715
+ script(
716
+ domReady(`
717
+ document.getElementById('tag_select').onchange = () => {
718
+ const version = document.getElementById('tag_select').value;
719
+ if (version) document.getElementById('version_select').value = version;
720
+ };
721
+ document.getElementById('version_select').onchange = () => {
722
+ const tagSelect = document.getElementById('tag_select');
723
+ tagSelect.value = '';
724
+ };
725
+ `)
726
+ )
664
727
  );
665
728
  } catch (error) {
666
729
  getState().log(
@@ -1168,9 +1231,22 @@ router.get(
1168
1231
  const update_permitted =
1169
1232
  db.getTenantSchema() === db.connectObj.default_schema &&
1170
1233
  plugin_db.source === "npm";
1171
- const latest =
1234
+
1235
+ let latest =
1172
1236
  update_permitted &&
1173
1237
  (await get_latest_npm_version(plugin_db.location, 1000));
1238
+ let engineInfos = await load_plugins.getEngineInfos(plugin_db); // with cache
1239
+ let forceFetch = true;
1240
+ if (latest && !engineInfos[latest]) {
1241
+ engineInfos = await load_plugins.getEngineInfos(plugin_db, forceFetch);
1242
+ forceFetch = false;
1243
+ }
1244
+ if (latest && !isVersionSupported(latest, engineInfos)) {
1245
+ latest = supportedVersion(
1246
+ latest,
1247
+ await load_plugins.getEngineInfos(plugin_db, forceFetch)
1248
+ );
1249
+ }
1174
1250
  const can_update = update_permitted && latest && mod.version !== latest;
1175
1251
  const can_select_version = update_permitted && plugin_db.source === "npm";
1176
1252
  let pkgjson;
@@ -1316,8 +1392,8 @@ router.get(
1316
1392
  error_catcher(async (req, res) => {
1317
1393
  const schema = db.getTenantSchema();
1318
1394
  if (schema === db.connectObj.default_schema) {
1319
- await upgrade_all_tenants_plugins((p, f) =>
1320
- load_plugins.loadPlugin(p, f)
1395
+ await upgrade_all_tenants_plugins((p, f, forceFetch) =>
1396
+ load_plugins.loadPlugin(p, f, forceFetch)
1321
1397
  );
1322
1398
  req.flash(
1323
1399
  "success",
@@ -1328,7 +1404,9 @@ router.get(
1328
1404
  } else {
1329
1405
  const installed_plugins = await Plugin.find({});
1330
1406
  for (const plugin of installed_plugins) {
1331
- await plugin.upgrade_version((p, f) => load_plugins.loadPlugin(p, f));
1407
+ await plugin.upgrade_version((p, f, forceFetch) =>
1408
+ load_plugins.loadPlugin(p, f, forceFetch)
1409
+ );
1332
1410
  }
1333
1411
  req.flash("success", req.__(`Modules up-to-date`));
1334
1412
  await restart_tenant(loadAllPlugins);
@@ -1354,7 +1432,12 @@ router.get(
1354
1432
  const { name } = req.params;
1355
1433
 
1356
1434
  const plugin = await Plugin.findOne({ name });
1357
- await plugin.upgrade_version((p, f) => load_plugins.loadPlugin(p, f));
1435
+ const versions = await load_plugins.getEngineInfos(plugin, true);
1436
+
1437
+ await plugin.upgrade_version(
1438
+ (p, f) => load_plugins.loadPlugin(p, f),
1439
+ supportedVersion("latest", versions, require("../package.json").version)
1440
+ );
1358
1441
  req.flash("success", req.__(`Module up-to-date`));
1359
1442
 
1360
1443
  res.redirect(`/plugins/info/${encodeURIComponent(plugin.name)}`);
@@ -1385,11 +1468,12 @@ router.post(
1385
1468
  res.redirect(`/plugins`);
1386
1469
  } else {
1387
1470
  try {
1388
- await load_plugins.loadAndSaveNewPlugin(
1471
+ const msgs = await load_plugins.loadAndSaveNewPlugin(
1389
1472
  plugin,
1390
1473
  schema === db.connectObj.default_schema || plugin.source === "github"
1391
1474
  );
1392
1475
  req.flash("success", req.__(`Module %s installed`, plugin.name));
1476
+ for (const msg of msgs) req.flash("warning", msg);
1393
1477
  res.redirect(`/plugins`);
1394
1478
  } catch (e) {
1395
1479
  req.flash("error", `${e.message}`);
@@ -1488,12 +1572,23 @@ router.post(
1488
1572
  res.redirect(`/plugins`);
1489
1573
  return;
1490
1574
  }
1491
- const msgs = await load_plugins.loadAndSaveNewPlugin(
1492
- plugin,
1493
- forceReInstall,
1494
- undefined,
1495
- req.__
1496
- );
1575
+
1576
+ let msgs = null;
1577
+ try {
1578
+ msgs = await load_plugins.loadAndSaveNewPlugin(
1579
+ plugin,
1580
+ forceReInstall,
1581
+ undefined,
1582
+ req.__
1583
+ );
1584
+ } catch (e) {
1585
+ req.flash(
1586
+ "error",
1587
+ e.message || req.__("Error installing module %s", plugin.name)
1588
+ );
1589
+ res.redirect(`/plugins`);
1590
+ return;
1591
+ }
1497
1592
  const plugin_module = getState().plugins[name];
1498
1593
  await sleep(1000); // Allow other workers to load this plugin
1499
1594
  await getState().refresh_views();
package/routes/scapi.js CHANGED
@@ -25,6 +25,7 @@ const {
25
25
  stateFieldsToWhere,
26
26
  readState,
27
27
  } = require("@saltcorn/data/plugin-helper");
28
+ const { getState } = require("@saltcorn/data/db/state");
28
29
 
29
30
  /**
30
31
  * @type {object}
@@ -307,3 +308,21 @@ router.get(
307
308
  )(req, res, next);
308
309
  })
309
310
  );
311
+
312
+ router.get(
313
+ "/reload",
314
+ error_catcher(async (req, res, next) => {
315
+ await passport.authenticate(
316
+ "api-bearer",
317
+ { session: false },
318
+ async function (err, user, info) {
319
+ if (accessAllowedRead(req, user)) {
320
+ await getState().refresh_plugins();
321
+ res.json({ success: true });
322
+ } else {
323
+ res.status(401).json({ error: req.__("Not authorized") });
324
+ }
325
+ }
326
+ )(req, res, next);
327
+ })
328
+ );
package/routes/search.js CHANGED
@@ -202,7 +202,12 @@ const runSearch = async ({ q, _page, table }, req, res) => {
202
202
  let tablesWithResults = [];
203
203
  let tablesConfigured = 0;
204
204
  for (const [tableName, viewName] of Object.entries(cfg)) {
205
- if (!viewName || viewName === "" || viewName === "search_table_description")
205
+ if (
206
+ !viewName ||
207
+ viewName === "" ||
208
+ viewName === "search_table_description" ||
209
+ tableName === "search_table_description"
210
+ )
206
211
  continue;
207
212
  tablesConfigured += 1;
208
213
  if (table && tableName !== table) continue;
package/routes/sync.js CHANGED
@@ -335,13 +335,14 @@ router.post(
335
335
  "/clean_sync_dir",
336
336
  error_catcher(async (req, res) => {
337
337
  const { dir_name } = req.body;
338
+ const safe_dir_name = File.normalise(dir_name);
338
339
  try {
339
340
  const rootFolder = await File.rootFolder();
340
341
  const syncDir = path.join(
341
342
  rootFolder.location,
342
343
  "mobile_app",
343
344
  "sync",
344
- dir_name
345
+ safe_dir_name
345
346
  );
346
347
  await fs.rm(syncDir, { recursive: true, force: true });
347
348
  res.status(200).send("");
package/routes/tables.js CHANGED
@@ -192,6 +192,9 @@ const tableForm = async (table, req) => {
192
192
  "if(!this.checked && !confirm('Are you sure? This will delete all history')) {this.checked = true; return false}",
193
193
  },
194
194
  type: "Bool",
195
+ help: {
196
+ topic: "Table history",
197
+ },
195
198
  },
196
199
  ...(table.name === "users"
197
200
  ? []
@@ -670,7 +673,10 @@ router.get(
670
673
  * @param {string} lbl
671
674
  * @returns {string}
672
675
  */
673
- const badge = (col, lbl) => `<span class="badge bg-${col}">${lbl}</span>&nbsp;`;
676
+ const badge = (col, lbl, title) =>
677
+ `<span ${
678
+ title ? `title="${title}" ` : ""
679
+ }class="badge bg-${col}">${lbl}</span>&nbsp;`;
674
680
 
675
681
  /**
676
682
  * @param {object} f
@@ -708,7 +714,11 @@ const attribBadges = (f) => {
708
714
  ].includes(k)
709
715
  )
710
716
  return;
711
- if (v || v === 0) s += badge("secondary", k);
717
+ if (Array.isArray(v) && !v.length) return;
718
+ const title = ["string", "number", "boolean"].includes(typeof v)
719
+ ? `${v}`
720
+ : null;
721
+ if (v || v === 0) s += badge("secondary", k, title);
712
722
  });
713
723
  }
714
724
  return s;
@@ -791,7 +801,7 @@ router.get(
791
801
  key: (r) => attribBadges(r),
792
802
  },
793
803
  { label: req.__("Variable name"), key: (t) => code(t.name) },
794
- ...(table.external || db.isSQLite
804
+ ...(table.external
795
805
  ? []
796
806
  : [
797
807
  {
@@ -1911,7 +1921,7 @@ router.post(
1911
1921
  const table = Table.findOne({ name });
1912
1922
 
1913
1923
  try {
1914
- await table.deleteRows({});
1924
+ await table.deleteRows({}, req.user);
1915
1925
  req.flash("success", req.__("Deleted all rows"));
1916
1926
  } catch (e) {
1917
1927
  req.flash("error", e.message);
package/serve.js CHANGED
@@ -4,7 +4,7 @@
4
4
  * @category server
5
5
  * @module serve
6
6
  */
7
- const runScheduler = require("@saltcorn/data/models/scheduler");
7
+ const { runScheduler } = require("@saltcorn/data/models/scheduler");
8
8
  const User = require("@saltcorn/data/models/user");
9
9
  const Plugin = require("@saltcorn/data/models/plugin");
10
10
  const db = require("@saltcorn/data/db");
@@ -27,7 +27,7 @@ const {
27
27
  loadAndSaveNewPlugin,
28
28
  loadPlugin,
29
29
  } = require("./load_plugins");
30
- const { getConfig } = require("@saltcorn/data/models/config");
30
+ const { getConfig, setConfig } = require("@saltcorn/data/models/config");
31
31
  const { migrate } = require("@saltcorn/data/migrate");
32
32
  const socketio = require("socket.io");
33
33
  const { createAdapter, setupPrimary } = require("@socket.io/cluster-adapter");
@@ -77,6 +77,17 @@ const ensureJwtSecret = () => {
77
77
  }
78
78
  };
79
79
 
80
+ /**
81
+ * Ensure the engines cache is up to date with the current sc version
82
+ */
83
+ const ensureEnginesCache = async () => {
84
+ const cacheScVersion = await getConfig("engines_cache_sc_version", "");
85
+ if (!cacheScVersion || cacheScVersion !== getState().scVersion) {
86
+ await setConfig("engines_cache", {});
87
+ await setConfig("engines_cache_sc_version", getState().scVersion);
88
+ }
89
+ };
90
+
80
91
  // helpful https://gist.github.com/jpoehls/2232358
81
92
  /**
82
93
  * @param {object} opts
@@ -222,7 +233,10 @@ module.exports =
222
233
  dev,
223
234
  ...appargs
224
235
  } = {}) => {
225
- ensureJwtSecret();
236
+ if (cluster.isMaster) {
237
+ ensureJwtSecret();
238
+ await ensureEnginesCache();
239
+ }
226
240
  process.on("unhandledRejection", (reason, p) => {
227
241
  console.error(reason, "Unhandled Rejection at Promise");
228
242
  });
@@ -350,6 +350,38 @@ describe("Field Endpoints", () => {
350
350
  .set("Cookie", loginCookie)
351
351
  .expect(toInclude(" is: <pre>2</pre>"));
352
352
  });
353
+ it("should show on stored expression with joinfield", async () => {
354
+ const loginCookie = await getAdminLoginCookie();
355
+ const table = Table.findOne({ name: "books" });
356
+
357
+ const ctx = encodeURIComponent(JSON.stringify({ table_id: table.id }));
358
+ const app = await getApp({ disableCsrf: true });
359
+ await request(app)
360
+ .post("/field/test-formula")
361
+ .send({
362
+ formula: "publisher.name",
363
+ tablename: "books",
364
+ stored: true,
365
+ })
366
+ .set("Cookie", loginCookie)
367
+ .expect(toInclude(" is: <pre>"));
368
+ });
369
+ it("should fail on non-stored expression with joinfield", async () => {
370
+ const loginCookie = await getAdminLoginCookie();
371
+ const table = Table.findOne({ name: "books" });
372
+
373
+ const ctx = encodeURIComponent(JSON.stringify({ table_id: table.id }));
374
+ const app = await getApp({ disableCsrf: true });
375
+ await request(app)
376
+ .post("/field/test-formula")
377
+ .send({
378
+ formula: "publisher.name",
379
+ tablename: "books",
380
+ stored: false,
381
+ })
382
+ .set("Cookie", loginCookie)
383
+ .expect(400);
384
+ });
353
385
  it("should show calculated", async () => {
354
386
  const loginCookie = await getAdminLoginCookie();
355
387
  const table = Table.findOne({ name: "books" });