@saltcorn/server 0.8.7-beta.6 → 0.8.8-beta.0

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/tables.js CHANGED
@@ -11,6 +11,7 @@ const Table = require("@saltcorn/data/models/table");
11
11
  const File = require("@saltcorn/data/models/file");
12
12
  const View = require("@saltcorn/data/models/view");
13
13
  const User = require("@saltcorn/data/models/user");
14
+ const Model = require("@saltcorn/data/models/model");
14
15
  const Trigger = require("@saltcorn/data/models/trigger");
15
16
  const {
16
17
  mkTable,
@@ -54,7 +55,7 @@ const {
54
55
  } = require("@saltcorn/data/models/discovery");
55
56
  const { getState } = require("@saltcorn/data/db/state");
56
57
  const { cardHeaderTabs } = require("@saltcorn/markup/layout_utils");
57
- const { tablesList } = require("./common_lists");
58
+ const { tablesList, viewsList } = require("./common_lists");
58
59
  const {
59
60
  InvalidConfiguration,
60
61
  removeAllWhiteSpace,
@@ -463,7 +464,7 @@ const navigationPanel = () =>
463
464
  button(
464
465
  {
465
466
  class: "btn btn-primary er-up",
466
- onclick: "erHelper.translateY(-100)",
467
+ onclick: "erHelper.translateY(100)",
467
468
  },
468
469
  i({ class: "fas fa-chevron-up" })
469
470
  ),
@@ -477,7 +478,7 @@ const navigationPanel = () =>
477
478
  button(
478
479
  {
479
480
  class: "btn btn-primary er-left",
480
- onclick: "erHelper.translateX(-100)",
481
+ onclick: "erHelper.translateX(100)",
481
482
  },
482
483
  i({ class: "fas fa-chevron-left" })
483
484
  ),
@@ -488,14 +489,14 @@ const navigationPanel = () =>
488
489
  button(
489
490
  {
490
491
  class: "btn btn-primary er-right",
491
- onclick: "erHelper.translateX(100)",
492
+ onclick: "erHelper.translateX(-100)",
492
493
  },
493
494
  i({ class: "fas fa-chevron-right" })
494
495
  ),
495
496
  button(
496
497
  {
497
498
  class: "btn btn-primary er-down",
498
- onclick: "erHelper.translateY(100)",
499
+ onclick: "erHelper.translateY(-100)",
499
500
  },
500
501
  i({ class: "fas fa-chevron-down" })
501
502
  ),
@@ -542,11 +543,30 @@ router.get(
542
543
  {
543
544
  headerTag: `
544
545
  <script type="module">
545
- mermaid.initialize({ startOnLoad: false });
546
+ mermaid.initialize({
547
+ startOnLoad: false,
548
+ securityLevel: 'loose',
549
+ });
546
550
  await mermaid.run({
547
551
  querySelector: ".mermaid",
548
552
  postRenderCallback: (id) => {
549
553
  $("#" + id).css("height", "calc(100vh - 250px)");
554
+ $("#" + id + " > g").each(function(index) {
555
+ const jThis = $(this);
556
+ const id = jThis.attr("id");
557
+ if (id) {
558
+ const arr = /^entity-(.+)-(\\w+-\\w+-\\w+-\\w+-\\w+$)/.exec(id);
559
+ if (arr?.length === 3) {
560
+ const textEnt = $("#text-entity-" + arr[1] + "-" + arr[2]);
561
+ textEnt.css("cursor", "pointer");
562
+ textEnt.on("click", function () {
563
+ if (!erHelper.isTranslating()) {
564
+ window.open("/table/" + encodeURIComponent(this.innerHTML));
565
+ }
566
+ });
567
+ }
568
+ }
569
+ });
550
570
  }
551
571
  });
552
572
  </script>`,
@@ -573,17 +593,30 @@ router.get(
573
593
  contents: [
574
594
  div(
575
595
  {
596
+ id: "erd-wrapper",
576
597
  style: "height: calc(100vh - 250px);",
577
598
  class: "overflow-scroll position-relative",
578
599
  },
579
600
  screenshotPanel(),
580
601
  pre(
581
- { class: "mermaid", style: "height: calc(100vh - 250px);" },
602
+ {
603
+ class: "mermaid",
604
+ style: "height: calc(100vh - 250px); color: transparent;",
605
+ },
582
606
  buildMermaidMarkup(tables)
583
607
  ),
584
608
  navigationPanel()
585
609
  ),
586
610
  script({ src: "/relationship_diagram_utils.js" }),
611
+ script(
612
+ domReady(`
613
+ const erdWrapper = $("#erd-wrapper")[0];
614
+ erdWrapper.onwheel = erHelper.onWheel;
615
+ erdWrapper.onmousedown = erHelper.onMouseDown;
616
+ erdWrapper.onmouseup = erHelper.onMouseUp;
617
+ window.addEventListener("mousemove", erHelper.onMouseMove);
618
+ `)
619
+ ),
587
620
  ],
588
621
  },
589
622
  ],
@@ -622,7 +655,14 @@ const attribBadges = (f) => {
622
655
  let s = "";
623
656
  if (f.attributes) {
624
657
  Object.entries(f.attributes).forEach(([k, v]) => {
625
- if (["summary_field", "on_delete_cascade", "on_delete"].includes(k))
658
+ if (
659
+ [
660
+ "summary_field",
661
+ "on_delete_cascade",
662
+ "on_delete",
663
+ "unique_error_msg",
664
+ ].includes(k)
665
+ )
626
666
  return;
627
667
  if (v || v === 0) s += badge("secondary", k);
628
668
  });
@@ -753,37 +793,10 @@ router.get(
753
793
  );
754
794
  var viewCardContents;
755
795
  if (views.length > 0) {
756
- viewCardContents = mkTable(
757
- [
758
- {
759
- label: req.__("Name"),
760
- key: (r) => link(`/view/${encodeURIComponent(r.name)}`, r.name),
761
- },
762
- { label: req.__("Pattern"), key: "viewtemplate" },
763
- {
764
- label: req.__("Configure"),
765
- key: (r) =>
766
- link(
767
- `/viewedit/config/${encodeURIComponent(
768
- r.name
769
- )}?on_done_redirect=${encodeURIComponent(
770
- `table/${table.name}`
771
- )}`,
772
- req.__("Configure")
773
- ),
774
- },
775
- {
776
- label: req.__("Delete"),
777
- key: (r) =>
778
- post_delete_btn(
779
- `/viewedit/delete/${encodeURIComponent(r.id)}`,
780
- req
781
- ),
782
- },
783
- ],
784
- views,
785
- { hover: true }
786
- );
796
+ viewCardContents = await viewsList(views, req, {
797
+ on_done_redirect: encodeURIComponent(`table/${table.name}`),
798
+ notable: true,
799
+ });
787
800
  } else {
788
801
  viewCardContents = div(
789
802
  h4(req.__("No views defined")),
@@ -806,6 +819,33 @@ router.get(
806
819
  ),
807
820
  };
808
821
  }
822
+ const models = await Model.find({ table_id: table.id });
823
+ const modelCard = div(
824
+ mkTable(
825
+ [
826
+ {
827
+ label: req.__("Name"),
828
+ key: (r) => link(`/models/show/${r.id}`, r.name),
829
+ },
830
+ { label: req.__("Pattern"), key: "modelpattern" },
831
+ {
832
+ label: req.__("Delete"),
833
+ key: (r) =>
834
+ post_delete_btn(
835
+ `/models/delete/${encodeURIComponent(r.id)}`,
836
+ req
837
+ ),
838
+ },
839
+ ],
840
+ models
841
+ ),
842
+ a(
843
+ { href: `/models/new/${table.id}`, class: "btn btn-primary" },
844
+ i({ class: "fas fa-plus-square me-1" }),
845
+ req.__("Create model")
846
+ )
847
+ );
848
+
809
849
  // Table Data card
810
850
  const dataCard = div(
811
851
  { class: "d-flex text-center" },
@@ -965,6 +1005,15 @@ router.get(
965
1005
  titleAjaxIndicator: true,
966
1006
  contents: renderForm(tblForm, req.csrfToken()),
967
1007
  },
1008
+ ...(Model.has_templates
1009
+ ? [
1010
+ {
1011
+ type: "card",
1012
+ title: req.__("Models"),
1013
+ contents: modelCard,
1014
+ },
1015
+ ]
1016
+ : []),
968
1017
  ],
969
1018
  });
970
1019
  })
@@ -1357,11 +1406,19 @@ const constraintForm = (req, table_id, fields, type) => {
1357
1406
  blurb: req.__(
1358
1407
  "Tick the boxes for the fields that should be jointly unique"
1359
1408
  ),
1360
- fields: fields.map((f) => ({
1361
- name: f.name,
1362
- label: f.label,
1363
- type: "Bool",
1364
- })),
1409
+ fields: [
1410
+ ...fields.map((f) => ({
1411
+ name: f.name,
1412
+ label: f.label,
1413
+ type: "Bool",
1414
+ })),
1415
+ {
1416
+ name: "errormsg",
1417
+ label: "Error message",
1418
+ sublabel: "Shown the user if joint uniqueness is violated",
1419
+ type: "String",
1420
+ },
1421
+ ],
1365
1422
  });
1366
1423
  case "Index":
1367
1424
  return new Form({
@@ -1453,11 +1510,12 @@ router.post(
1453
1510
  if (form.hasErrors) req.flash("error", req.__("An error occurred"));
1454
1511
  else {
1455
1512
  let configuration = {};
1456
- if (type === "Unique")
1513
+ if (type === "Unique") {
1457
1514
  configuration.fields = fields
1458
1515
  .map((f) => f.name)
1459
1516
  .filter((f) => form.values[f]);
1460
- else configuration = form.values;
1517
+ configuration.errormsg = form.values.errormsg;
1518
+ } else configuration = form.values;
1461
1519
  await TableConstraint.create({
1462
1520
  table_id: table.id,
1463
1521
  type,
@@ -1841,7 +1899,7 @@ const get_provider_workflow = (table, req) => {
1841
1899
 
1842
1900
  return {
1843
1901
  redirect: `/table/${table.id}`,
1844
- flash: ["success", `Table ${this.name || ""} saved`],
1902
+ flash: ["success", `Table ${table.name || ""} saved`],
1845
1903
  };
1846
1904
  };
1847
1905
  return workflow;
package/routes/view.js CHANGED
@@ -8,6 +8,7 @@ const Router = require("express-promise-router");
8
8
 
9
9
  const View = require("@saltcorn/data/models/view");
10
10
  const Table = require("@saltcorn/data/models/table");
11
+ const Trigger = require("@saltcorn/data/models/trigger");
11
12
 
12
13
  const { text, style } = require("@saltcorn/markup/tags");
13
14
  const {
@@ -51,7 +52,7 @@ router.get(
51
52
  res.redirect("/");
52
53
  return;
53
54
  }
54
- const tic = state.logLevel >= 5 ? new Date() : null;
55
+ const tic = new Date();
55
56
 
56
57
  view.rewrite_query_from_slug(query, req.params);
57
58
  if (
@@ -85,11 +86,14 @@ router.get(
85
86
  res.set("SaltcornModalSaveIndicator", `true`);
86
87
  if (isModal && view.attributes?.popup_link_out)
87
88
  res.set("SaltcornModalLinkOut", `true`);
88
- if (tic) {
89
- const tock = new Date();
90
- const ms = tock.getTime() - tic.getTime();
91
- state.log(5, `View ${viewname} rendered in ${ms} ms`);
92
- }
89
+ const tock = new Date();
90
+ const ms = tock.getTime() - tic.getTime();
91
+ Trigger.emitEvent("PageLoad", null, req.user, {
92
+ text: req.__("View '%s' was loaded", viewname),
93
+ type: "view",
94
+ name: viewname,
95
+ render_time: ms,
96
+ });
93
97
  if (typeof contents === "object" && contents.goto)
94
98
  res.redirect(contents.goto);
95
99
  else
@@ -13,7 +13,12 @@ const { p, a, div, script, text, domReady, code, pre, tbody, tr, th, td } =
13
13
  tags;
14
14
 
15
15
  const { getState } = require("@saltcorn/data/db/state");
16
- const { isAdmin, error_catcher, addOnDoneRedirect } = require("./utils.js");
16
+ const {
17
+ isAdmin,
18
+ error_catcher,
19
+ addOnDoneRedirect,
20
+ is_relative_url,
21
+ } = require("./utils.js");
17
22
  const { setTableRefs, viewsList } = require("./common_lists");
18
23
  const Form = require("@saltcorn/data/models/form");
19
24
  const Field = require("@saltcorn/data/models/field");
@@ -675,7 +680,11 @@ router.post(
675
680
  view.name
676
681
  )
677
682
  );
678
- res.redirect(`/viewedit`);
683
+ let redirectTarget =
684
+ req.query.on_done_redirect && is_relative_url(req.query.on_done_redirect)
685
+ ? `/${req.query.on_done_redirect}`
686
+ : "/viewedit";
687
+ res.redirect(redirectTarget);
679
688
  })
680
689
  );
681
690
 
@@ -696,7 +705,11 @@ router.post(
696
705
  "success",
697
706
  req.__("View %s duplicated as %s", view.name, newview.name)
698
707
  );
699
- res.redirect(`/viewedit`);
708
+ let redirectTarget =
709
+ req.query.on_done_redirect && is_relative_url(req.query.on_done_redirect)
710
+ ? `/${req.query.on_done_redirect}`
711
+ : "/viewedit";
712
+ res.redirect(redirectTarget);
700
713
  })
701
714
  );
702
715
 
@@ -713,7 +726,11 @@ router.post(
713
726
  const { id } = req.params;
714
727
  await View.delete({ id });
715
728
  req.flash("success", req.__("View deleted"));
716
- res.redirect(`/viewedit`);
729
+ let redirectTarget =
730
+ req.query.on_done_redirect && is_relative_url(req.query.on_done_redirect)
731
+ ? `/${req.query.on_done_redirect}`
732
+ : "/viewedit";
733
+ res.redirect(redirectTarget);
717
734
  })
718
735
  );
719
736
 
@@ -805,7 +822,12 @@ router.post(
805
822
  : req.__(`Minimum role updated`);
806
823
  if (!req.xhr) {
807
824
  req.flash("success", message);
808
- res.redirect("/viewedit");
825
+ let redirectTarget =
826
+ req.query.on_done_redirect &&
827
+ is_relative_url(req.query.on_done_redirect)
828
+ ? `/${req.query.on_done_redirect}`
829
+ : "/viewedit";
830
+ res.redirect(redirectTarget);
809
831
  } else res.json({ okay: true, responseText: message });
810
832
  })
811
833
  );
@@ -2,8 +2,13 @@ const request = require("supertest");
2
2
  const getApp = require("../app");
3
3
  const Table = require("@saltcorn/data/models/table");
4
4
  const Plugin = require("@saltcorn/data/models/plugin");
5
- const { getState } = require("@saltcorn/data/db/state");
6
-
5
+ const { getState, add_tenant } = require("@saltcorn/data/db/state");
6
+ const { install_pack } = require("@saltcorn/admin-models/models/pack");
7
+ const {
8
+ switchToTenant,
9
+ insertTenant,
10
+ create_tenant,
11
+ } = require("@saltcorn/admin-models/models/tenant");
7
12
  const {
8
13
  getAdminLoginCookie,
9
14
  itShouldRedirectUnauthToLogin,
@@ -16,6 +21,7 @@ const db = require("@saltcorn/data/db");
16
21
  const load_plugins = require("../load_plugins");
17
22
 
18
23
  beforeAll(async () => {
24
+ if (!db.isSQLite) await db.query(`drop schema if exists test101 CASCADE `);
19
25
  await resetToFixtures();
20
26
  });
21
27
  afterAll(db.close);
@@ -325,3 +331,98 @@ describe("config endpoints", () => {
325
331
  .expect(toInclude(">FooSiteName<"));
326
332
  });
327
333
  });
334
+
335
+ const plugin_pack = (plugin) => ({
336
+ tables: [],
337
+ views: [],
338
+ plugins: [
339
+ {
340
+ ...plugin,
341
+ configuration: null,
342
+ },
343
+ ],
344
+ pages: [],
345
+ roles: [],
346
+ library: [],
347
+ triggers: [],
348
+ });
349
+
350
+ describe("Tenant cannot install unsafe plugins", () => {
351
+ if (!db.isSQLite) {
352
+ it("creates a new tenant", async () => {
353
+ db.enable_multi_tenant();
354
+ const loadAndSaveNewPlugin = load_plugins.loadAndSaveNewPlugin;
355
+
356
+ await getState().setConfig("base_url", "http://example.com/");
357
+
358
+ add_tenant("test101");
359
+
360
+ await switchToTenant(
361
+ await insertTenant("test101", "foo@foo.com", ""),
362
+ "http://test101.example.com/"
363
+ );
364
+
365
+ await create_tenant({
366
+ t: "test101",
367
+ loadAndSaveNewPlugin,
368
+ plugin_loader() {},
369
+ });
370
+ });
371
+ it("can install safe plugins on tenant", async () => {
372
+ await db.runWithTenant("test101", async () => {
373
+ const loadAndSaveNewPlugin = load_plugins.loadAndSaveNewPlugin;
374
+
375
+ await install_pack(
376
+ plugin_pack({
377
+ name: "html",
378
+ source: "npm",
379
+ location: "@saltcorn/html",
380
+ }),
381
+ "Todo list",
382
+ loadAndSaveNewPlugin
383
+ );
384
+ const dbPlugin = await Plugin.findOne({ name: "html" });
385
+ expect(dbPlugin).not.toBe(null);
386
+ });
387
+ });
388
+ it("cannot install unsafe plugins on tenant", async () => {
389
+ await db.runWithTenant("test101", async () => {
390
+ const loadAndSaveNewPlugin = load_plugins.loadAndSaveNewPlugin;
391
+
392
+ await install_pack(
393
+ plugin_pack({
394
+ name: "sql-list",
395
+ source: "npm",
396
+ location: "@saltcorn/sql-list",
397
+ }),
398
+ "Todo list",
399
+ loadAndSaveNewPlugin
400
+ );
401
+ const dbPlugin = await Plugin.findOne({ name: "sql-list" });
402
+ expect(dbPlugin).toBe(null);
403
+ });
404
+ });
405
+ it("can install unsafe plugins on tenant when permitted", async () => {
406
+ await getState().setConfig("tenants_unsafe_plugins", true);
407
+ await db.runWithTenant("test101", async () => {
408
+ const loadAndSaveNewPlugin = load_plugins.loadAndSaveNewPlugin;
409
+
410
+ await install_pack(
411
+ plugin_pack({
412
+ name: "sql-list",
413
+ source: "npm",
414
+ location: "@saltcorn/sql-list",
415
+ }),
416
+ "Todo list",
417
+ loadAndSaveNewPlugin
418
+ );
419
+ const dbPlugin = await Plugin.findOne({ name: "sql-list" });
420
+ expect(dbPlugin).not.toBe(null);
421
+ });
422
+ });
423
+ } else {
424
+ it("does not support tenants on SQLite", async () => {
425
+ expect(db.isSQLite).toBe(true);
426
+ });
427
+ }
428
+ });