@saltcorn/server 0.8.8-beta.3 → 0.8.8-beta.5

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/auth/routes.js CHANGED
@@ -254,7 +254,9 @@ const loginWithJwt = async (email, password, saltcornApp, res, req) => {
254
254
  res.json(token);
255
255
  } else {
256
256
  res.json({
257
- alerts: [{ type: "danger", msg: req.__("Incorrect user or password") }],
257
+ alerts: [
258
+ { type: "danger", msg: req.__("Incorrect user or password") },
259
+ ],
258
260
  });
259
261
  }
260
262
  } else if (publicUserLink) {
@@ -276,7 +278,9 @@ const loginWithJwt = async (email, password, saltcornApp, res, req) => {
276
278
  res.json(token);
277
279
  } else {
278
280
  res.json({
279
- alerts: [{ type: "danger", msg: req.__("The public login is deactivated") }],
281
+ alerts: [
282
+ { type: "danger", msg: req.__("The public login is deactivated") },
283
+ ],
280
284
  });
281
285
  }
282
286
  };
@@ -628,9 +632,11 @@ router.post(
628
632
  * @throws {InvalidConfiguration}
629
633
  */
630
634
  const getNewUserForm = async (new_user_view_name, req, askEmail) => {
635
+ if (!new_user_view_name) return;
631
636
  const view = await View.findOne({ name: new_user_view_name });
632
637
  if (!view)
633
638
  throw new InvalidConfiguration("New user form view does not exist");
639
+ if (view.viewtemplate !== "Edit") return;
634
640
  const table = Table.findOne({ name: "users" });
635
641
  const fields = table.getFields();
636
642
  const { columns, layout } = view.configuration;
@@ -704,14 +710,14 @@ const getNewUserForm = async (new_user_view_name, req, askEmail) => {
704
710
  * @param {object} res
705
711
  * @returns {void}
706
712
  */
707
- const signup_login_with_user = (u, req, res) =>
713
+ const signup_login_with_user = (u, req, res, redirUrl) =>
708
714
  req.login(u.session_object, function (err) {
709
715
  if (!err) {
710
716
  Trigger.emitEvent("Login", null, u);
711
717
  if (getState().verifier) res.redirect("/auth/verification-flow");
712
718
  else if (getState().get2FApolicy(u) === "Mandatory")
713
719
  res.redirect("/auth/twofa/setup/totp");
714
- else res.redirect("/");
720
+ else res.redirect(redirUrl || "/");
715
721
  } else {
716
722
  req.flash("danger", err);
717
723
  res.redirect("/auth/signup");
@@ -869,7 +875,8 @@ router.post(
869
875
  return;
870
876
  }
871
877
 
872
- const unsuitableEmailPassword = async (email, password, passwordRepeat) => {
878
+ const unsuitableEmailPassword = async (urecord) => {
879
+ const { email, password, passwordRepeat } = urecord;
873
880
  if (!email || !password) {
874
881
  req.flash("danger", req.__("E-mail and password required"));
875
882
  res.redirect("/auth/signup");
@@ -911,6 +918,12 @@ router.post(
911
918
  res.redirect("/auth/signup");
912
919
  return true;
913
920
  }
921
+ let constraint_check_error = User.table.check_table_constraints(urecord);
922
+ if (constraint_check_error) {
923
+ req.flash("danger", constraint_check_error);
924
+ res.redirect("/auth/signup");
925
+ return true;
926
+ }
914
927
  };
915
928
  const new_user_form = getState().getConfig("new_user_form");
916
929
 
@@ -943,21 +956,32 @@ router.post(
943
956
  signup_form.values[f.name] = signup_form.values[f.name] || "";
944
957
  });
945
958
  const userObject = signup_form.values;
946
- const { email, password, passwordRepeat } = userObject;
947
- if (await unsuitableEmailPassword(email, password, passwordRepeat))
948
- return;
949
- if (new_user_form) {
950
- const form = await getNewUserForm(new_user_form, req);
959
+ //const { email, password, passwordRepeat } = userObject;
960
+ if (await unsuitableEmailPassword(userObject)) return;
961
+ const new_user_form_form = await getNewUserForm(new_user_form, req);
962
+ if (new_user_form_form) {
951
963
  Object.entries(userObject).forEach(([k, v]) => {
952
- form.values[k] = v;
953
- if (!form.fields.find((f) => f.name === k)) form.hidden(k);
964
+ new_user_form_form.values[k] = v;
965
+ if (!new_user_form_form.fields.find((f) => f.name === k))
966
+ new_user_form_form.hidden(k);
954
967
  });
955
- res.sendAuthWrap(new_user_form, form, getAuthLinks("signup", true));
968
+ res.sendAuthWrap(
969
+ new_user_form,
970
+ new_user_form_form,
971
+ getAuthLinks("signup", true)
972
+ );
956
973
  } else {
957
974
  const u = await User.create(userObject);
958
975
  await send_verification_email(u, req);
959
976
 
960
- signup_login_with_user(u, req, res);
977
+ signup_login_with_user(
978
+ u,
979
+ req,
980
+ res,
981
+ new_user_form && !new_user_form_form
982
+ ? `/view/${new_user_form}?id=${u.id}`
983
+ : undefined
984
+ );
961
985
  }
962
986
  return;
963
987
  }
@@ -972,7 +996,7 @@ router.post(
972
996
  res.sendAuthWrap(req.__(`Sign up`), form, getAuthLinks("signup"));
973
997
  } else {
974
998
  const { email, password } = form.values;
975
- if (await unsuitableEmailPassword(email, password)) return;
999
+ if (await unsuitableEmailPassword({ email, password })) return;
976
1000
  if (new_user_form) {
977
1001
  const form = await getNewUserForm(new_user_form, req);
978
1002
  form.values.email = email;
@@ -1100,7 +1124,13 @@ router.get(
1100
1124
  const { method } = req.params;
1101
1125
  if (method === "jwt") {
1102
1126
  const { email, password } = req.query;
1103
- await loginWithJwt(email, password, req.headers["x-saltcorn-app"], res, req);
1127
+ await loginWithJwt(
1128
+ email,
1129
+ password,
1130
+ req.headers["x-saltcorn-app"],
1131
+ res,
1132
+ req
1133
+ );
1104
1134
  } else {
1105
1135
  const auth = getState().auth_methods[method];
1106
1136
  if (auth) {
package/locales/en.json CHANGED
@@ -1242,5 +1242,11 @@
1242
1242
  "unsynched": "unsynched",
1243
1243
  "synched": "synched",
1244
1244
  "Sync information": "Sync information",
1245
- "Sync information tracks the last modification or deletion timestamp so that the table data can be synchronized with the mobile app": "Sync information tracks the last modification or deletion timestamp so that the table data can be synchronized with the mobile app"
1245
+ "Sync information tracks the last modification or deletion timestamp so that the table data can be synchronized with the mobile app": "Sync information tracks the last modification or deletion timestamp so that the table data can be synchronized with the mobile app",
1246
+ "Included Plugins": "Included Plugins",
1247
+ "exclude": "exclude",
1248
+ "include": "include",
1249
+ "Auto public login": "Auto public login",
1250
+ "New user view": "New user view",
1251
+ "A view to show to new users, to finalise registration (if Edit) or as a welcome view": "A view to show to new users, to finalise registration (if Edit) or as a welcome view"
1246
1252
  }
package/locales/it.json CHANGED
@@ -503,5 +503,20 @@
503
503
  "Table columns": "Table columns",
504
504
  "Configuration items": "Configuration items",
505
505
  "Crashlogs": "Crashlogs",
506
- "Logged out user %s": "Logged out user %s"
507
- }
506
+ "Logged out user %s": "Logged out user %s",
507
+ "CSV upload": "CSV upload",
508
+ "Pages are the web pages of your application built with a drag-and-drop builder. They have static content, and by embedding views, dynamic content.": "Pages are the web pages of your application built with a drag-and-drop builder. They have static content, and by embedding views, dynamic content.",
509
+ "Create page": "Create page",
510
+ "Triggers run actions in response to events.": "Triggers run actions in response to events.",
511
+ "No triggers": "No triggers",
512
+ "Upload file(s)": "Upload file(s)",
513
+ "Pattern": "Pattern",
514
+ "You have views with a role to access lower than the table role to read, with no table ownership. This may cause a denial of access. Users need to have table read access to any data displayed.": "You have views with a role to access lower than the table role to read, with no table ownership. This may cause a denial of access. Users need to have table read access to any data displayed.",
515
+ "Views potentially affected": "Views potentially affected",
516
+ "Fixed and blocked fields": "Fixed and blocked fields",
517
+ "URL after delete": "URL after delete",
518
+ "Save before going back": "Save before going back",
519
+ "Reload after going back": "Reload after going back",
520
+ "Steps to go back": "Steps to go back",
521
+ "%s configuration": "%s configuration"
522
+ }
package/package.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "@saltcorn/server",
3
- "version": "0.8.8-beta.3",
3
+ "version": "0.8.8-beta.5",
4
4
  "description": "Server app for Saltcorn, open-source no-code platform",
5
5
  "homepage": "https://saltcorn.com",
6
6
  "main": "index.js",
7
7
  "license": "MIT",
8
8
  "dependencies": {
9
- "@saltcorn/base-plugin": "0.8.8-beta.3",
10
- "@saltcorn/builder": "0.8.8-beta.3",
11
- "@saltcorn/data": "0.8.8-beta.3",
12
- "@saltcorn/admin-models": "0.8.8-beta.3",
13
- "@saltcorn/filemanager": "0.8.8-beta.3",
14
- "@saltcorn/markup": "0.8.8-beta.3",
15
- "@saltcorn/sbadmin2": "0.8.8-beta.3",
9
+ "@saltcorn/base-plugin": "0.8.8-beta.5",
10
+ "@saltcorn/builder": "0.8.8-beta.5",
11
+ "@saltcorn/data": "0.8.8-beta.5",
12
+ "@saltcorn/admin-models": "0.8.8-beta.5",
13
+ "@saltcorn/filemanager": "0.8.8-beta.5",
14
+ "@saltcorn/markup": "0.8.8-beta.5",
15
+ "@saltcorn/sbadmin2": "0.8.8-beta.5",
16
16
  "@socket.io/cluster-adapter": "^0.2.1",
17
17
  "@socket.io/sticky": "^1.0.1",
18
18
  "adm-zip": "0.5.10",
@@ -41,6 +41,15 @@ const _apply_showif_plugins = [];
41
41
  const add_apply_showif_plugin = (p) => {
42
42
  _apply_showif_plugins.push(p);
43
43
  };
44
+
45
+ const nubBy = (prop, xs) => {
46
+ const vs = new Set();
47
+ return xs.filter((x) => {
48
+ if (vs.has(x[prop])) return false;
49
+ vs.add(x[prop]);
50
+ return true;
51
+ });
52
+ };
44
53
  function apply_showif() {
45
54
  $("[data-show-if]").each(function (ix, element) {
46
55
  var e = $(element);
@@ -109,10 +118,16 @@ function apply_showif() {
109
118
  const dynwhere = JSON.parse(
110
119
  decodeURIComponent(e.attr("data-fetch-options"))
111
120
  );
112
- //console.log(dynwhere);
113
- const qs = Object.entries(dynwhere.whereParsed)
114
- .map(([k, v]) => `${k}=${v[0] === "$" ? rec[v.substring(1)] : v}`)
115
- .join("&");
121
+ //console.log("dynwhere", dynwhere);
122
+ const qss = Object.entries(dynwhere.whereParsed).map(
123
+ ([k, v]) => `${k}=${v[0] === "$" ? rec[v.substring(1)] : v}`
124
+ );
125
+ if (dynwhere.dereference) {
126
+ if (Array.isArray(dynwhere.dereference))
127
+ qss.push(...dynwhere.dereference.map((d) => `dereference=${d}`));
128
+ else qss.push(`dereference=${dynwhere.dereference}`);
129
+ }
130
+ const qs = qss.join("&");
116
131
  var current = e.attr("data-selected");
117
132
  e.change(function (ec) {
118
133
  e.attr("data-selected", ec.target.value);
@@ -129,7 +144,11 @@ function apply_showif() {
129
144
  if (!dynwhere.required) toAppend.push(`<option></option>`);
130
145
  let currentDataOption = undefined;
131
146
  const dataOptions = [];
132
- success.forEach((r) => {
147
+ //console.log(success);
148
+ const success1 = dynwhere.nubBy
149
+ ? nubBy(dynwhere.nubBy, success)
150
+ : success;
151
+ success1.forEach((r) => {
133
152
  const label = dynwhere.label_formula
134
153
  ? new Function(
135
154
  `{${Object.keys(r).join(",")}}`,
@@ -137,6 +156,7 @@ function apply_showif() {
137
156
  )(r)
138
157
  : r[dynwhere.summary_field];
139
158
  const value = r[dynwhere.refname];
159
+ //console.log("lv", label, value, r, dynwhere.summary_field);
140
160
  const selected = `${current}` === `${r[dynwhere.refname]}`;
141
161
  dataOptions.push({ text: label, value });
142
162
  if (selected) currentDataOption = value;
@@ -510,6 +530,14 @@ function initialize_page() {
510
530
  if (schema) {
511
531
  schema = JSON.parse(decodeURIComponent(schema));
512
532
  }
533
+ if (type === "Date") {
534
+ console.log("timeelsems", $(this).find("span.current time"));
535
+ current =
536
+ $(this).attr("data-inline-edit-current") ||
537
+ $(this).find("span.current time").attr("datetime"); // ||
538
+ //$(this).children("span.current").html();
539
+ }
540
+ console.log({ type, current });
513
541
  var is_key = type?.startsWith("Key:");
514
542
  const opts = encodeURIComponent(
515
543
  JSON.stringify({
@@ -875,7 +903,7 @@ function common_done(res, isWeb = true) {
875
903
  if (res.eval_js) handle(res.eval_js, eval);
876
904
 
877
905
  if (res.reload_page) {
878
- (isWeb ? location : parent.location).reload(); //TODO notify to cookie if reload or goto
906
+ (isWeb ? location : parent).reload(); //TODO notify to cookie if reload or goto
879
907
  }
880
908
  if (res.download) {
881
909
  handle(res.download, (download) => {
@@ -389,6 +389,20 @@ function saveAndContinue(e, k) {
389
389
  return false;
390
390
  }
391
391
 
392
+ function updateMatchingRows(e, viewname) {
393
+ const form = $(e).closest("form");
394
+ try {
395
+ const sp = `${new URL(get_current_state_url()).searchParams.toString()}`;
396
+ form.attr(
397
+ "action",
398
+ `/view/${viewname}/update_matching_rows${sp ? `?${sp}` : ""}`
399
+ );
400
+ form[0].submit();
401
+ } finally {
402
+ form.attr("action", `/view/${viewname}`);
403
+ }
404
+ }
405
+
392
406
  function applyViewConfig(e, url, k) {
393
407
  var form = $(e).closest("form");
394
408
  var form_data = form.serializeArray();
@@ -678,6 +692,10 @@ function build_mobile_app(button) {
678
692
  params.synchedTables = Array.from($("#synched-tbls-select-id")[0].options)
679
693
  .filter((option) => !option.hidden)
680
694
  .map((option) => option.value);
695
+ const pluginsSelect = $("#included-plugins-select-id")[0];
696
+ params.includedPlugins = Array.from(pluginsSelect.options)
697
+ .filter((option) => !option.hidden)
698
+ .map((option) => option.value);
681
699
  ajax_post("/admin/build-mobile-app", {
682
700
  data: params,
683
701
  success: (data) => {
@@ -717,6 +735,32 @@ function move_to_unsynched() {
717
735
  }
718
736
  }
719
737
 
738
+ function move_plugin_to_included() {
739
+ const opts = $("#excluded-plugins-select-id");
740
+ $("#included-plugins-select-id").removeAttr("selected");
741
+ for (const selected of opts.val()) {
742
+ const jExclOpt = $(`[id='${selected}_excluded_opt']`);
743
+ jExclOpt.attr("hidden", "true");
744
+ jExclOpt.removeAttr("selected");
745
+ const jInclOpt = $(`[id='${selected}_included_opt']`);
746
+ jInclOpt.removeAttr("hidden");
747
+ jInclOpt.removeAttr("selected");
748
+ }
749
+ }
750
+
751
+ function move_plugin_to_excluded() {
752
+ const opts = $("#included-plugins-select-id");
753
+ $("#excluded-plugins-select-id").removeAttr("selected");
754
+ for (const selected of opts.val()) {
755
+ const jInclOpt = $(`[id='${selected}_included_opt']`);
756
+ jInclOpt.attr("hidden", "true");
757
+ jInclOpt.removeAttr("selected");
758
+ const jExclOpt = $(`[id='${selected}_excluded_opt']`);
759
+ jExclOpt.removeAttr("hidden");
760
+ jExclOpt.removeAttr("selected");
761
+ }
762
+ }
763
+
720
764
  function toggle_tbl_sync() {
721
765
  if ($("#offlineModeBoxId")[0].checked === true) {
722
766
  $("#tblSyncSelectorId").attr("hidden", false);
@@ -725,6 +769,18 @@ function toggle_tbl_sync() {
725
769
  }
726
770
  }
727
771
 
772
+ function toggle_android_platform() {
773
+ if ($("#androidCheckboxId")[0].checked === true) {
774
+ $("#dockerCheckboxId").attr("hidden", false);
775
+ $("#dockerCheckboxId").attr("checked", true);
776
+ $("#dockerLabelId").removeClass("d-none");
777
+ } else {
778
+ $("#dockerCheckboxId").attr("hidden", true);
779
+ $("#dockerCheckboxId").attr("checked", false);
780
+ $("#dockerLabelId").addClass("d-none");
781
+ }
782
+ }
783
+
728
784
  function join_field_clicked(e, fieldPath) {
729
785
  $("#inputjoin_field").val(fieldPath);
730
786
  apply_showif();
package/routes/admin.js CHANGED
@@ -1492,6 +1492,9 @@ router.get(
1492
1492
  image.filename?.endsWith(".png")
1493
1493
  );
1494
1494
  const withSyncInfo = await Table.find({ has_sync_info: true });
1495
+ const plugins = (await Plugin.find()).filter(
1496
+ (plugin) => ["base", "sbadmin2"].indexOf(plugin.name) < 0
1497
+ );
1495
1498
  send_admin_page({
1496
1499
  res,
1497
1500
  req,
@@ -1532,7 +1535,9 @@ router.get(
1532
1535
  div({ class: "col-sm-4 fw-bold" }, req.__("Platform")),
1533
1536
  div(
1534
1537
  {
1535
- class: "col-sm-1 fw-bold d-flex justify-content-center",
1538
+ class:
1539
+ "col-sm-1 fw-bold d-flex justify-content-center d-none",
1540
+ id: "dockerLabelId",
1536
1541
  },
1537
1542
  req.__("docker")
1538
1543
  )
@@ -1593,7 +1598,7 @@ router.get(
1593
1598
  ),
1594
1599
  div(
1595
1600
  { class: "col-sm-4" },
1596
-
1601
+ // android
1597
1602
  div(
1598
1603
  { class: "container ps-0" },
1599
1604
  div(
@@ -1606,9 +1611,11 @@ router.get(
1606
1611
  class: "form-check-input",
1607
1612
  name: "androidPlatform",
1608
1613
  id: "androidCheckboxId",
1614
+ onClick: "toggle_android_platform()",
1609
1615
  })
1610
1616
  )
1611
1617
  ),
1618
+ // iOS
1612
1619
  div(
1613
1620
  { class: "row" },
1614
1621
  div({ class: "col-sm-8" }, req.__("iOS")),
@@ -1624,6 +1631,7 @@ router.get(
1624
1631
  )
1625
1632
  )
1626
1633
  ),
1634
+ // android with docker
1627
1635
  div(
1628
1636
  { class: "col-sm-1 d-flex justify-content-center" },
1629
1637
  input({
@@ -1631,6 +1639,7 @@ router.get(
1631
1639
  class: "form-check-input",
1632
1640
  name: "useDocker",
1633
1641
  id: "dockerCheckboxId",
1642
+ hidden: true,
1634
1643
  })
1635
1644
  )
1636
1645
  ),
@@ -1750,7 +1759,29 @@ router.get(
1750
1759
  )
1751
1760
  )
1752
1761
  ),
1753
-
1762
+ // auto public login box
1763
+ div(
1764
+ { class: "row pb-2" },
1765
+ div(
1766
+ { class: "col-sm-4" },
1767
+ input({
1768
+ type: "checkbox",
1769
+ id: "autoPublLoginId",
1770
+ class: "form-check-input me-2",
1771
+ name: "autoPublicLogin",
1772
+ value: "autoPublicLogin",
1773
+ checked: false,
1774
+ }),
1775
+ label(
1776
+ {
1777
+ for: "autoPublLoginId",
1778
+ class: "form-label",
1779
+ },
1780
+ req.__("Auto public login")
1781
+ )
1782
+ )
1783
+ ),
1784
+ // allow offline mode box
1754
1785
  div(
1755
1786
  { class: "row pb-2" },
1756
1787
  div(
@@ -1773,10 +1804,11 @@ router.get(
1773
1804
  )
1774
1805
  )
1775
1806
  ),
1807
+ // synched/unsynched tables
1776
1808
  div(
1777
1809
  {
1778
1810
  id: "tblSyncSelectorId",
1779
- class: "row pb-2",
1811
+ class: "row pb-3",
1780
1812
  },
1781
1813
  div(
1782
1814
  label(
@@ -1864,6 +1896,97 @@ router.get(
1864
1896
  )
1865
1897
  )
1866
1898
  )
1899
+ ),
1900
+ // included/excluded plugins
1901
+ div(
1902
+ {
1903
+ id: "pluginsSelectorId",
1904
+ class: "row pb-2",
1905
+ },
1906
+ div(
1907
+ label({ class: "form-label fw-bold" }, req.__("Plugins"))
1908
+ ),
1909
+ div(
1910
+ { class: "container" },
1911
+ div(
1912
+ { class: "row" },
1913
+ div(
1914
+ { class: "col-sm-4 text-center" },
1915
+ req.__("exclude")
1916
+ ),
1917
+ div({ class: "col-sm-1" }),
1918
+ div(
1919
+ { class: "col-sm-4 text-center" },
1920
+ req.__("include")
1921
+ )
1922
+ ),
1923
+ div(
1924
+ { class: "row" },
1925
+ div(
1926
+ { class: "col-sm-4" },
1927
+ select(
1928
+ {
1929
+ id: "excluded-plugins-select-id",
1930
+ class: "form-control form-select",
1931
+ multiple: true,
1932
+ },
1933
+ plugins.map((plugin) =>
1934
+ option({
1935
+ id: `${plugin.name}_excluded_opt`,
1936
+ value: plugin.name,
1937
+ label: plugin.name,
1938
+ hidden: "true",
1939
+ })
1940
+ )
1941
+ )
1942
+ ),
1943
+ div(
1944
+ { class: "col-sm-1 d-flex justify-content-center" },
1945
+ div(
1946
+ div(
1947
+ button(
1948
+ {
1949
+ id: "move-plugin-right-btn-id",
1950
+ type: "button",
1951
+ onClick: `move_plugin_to_included()`,
1952
+ class: "btn btn-light pt-1 mb-1",
1953
+ },
1954
+ i({ class: "fas fa-arrow-right" })
1955
+ )
1956
+ ),
1957
+ div(
1958
+ button(
1959
+ {
1960
+ id: "move-plugin-left-btn-id",
1961
+ type: "button",
1962
+ onClick: `move_plugin_to_excluded()`,
1963
+ class: "btn btn-light pt-1",
1964
+ },
1965
+ i({ class: "fas fa-arrow-left" })
1966
+ )
1967
+ )
1968
+ )
1969
+ ),
1970
+ div(
1971
+ { class: "col-sm-4" },
1972
+ select(
1973
+ {
1974
+ id: "included-plugins-select-id",
1975
+ class: "form-control form-select",
1976
+ multiple: true,
1977
+ },
1978
+ plugins.map((plugin) =>
1979
+ option({
1980
+ id: `${plugin.name}_included_opt`,
1981
+ value: plugin.name,
1982
+ label: plugin.name,
1983
+ // hidden: "true",
1984
+ })
1985
+ )
1986
+ )
1987
+ )
1988
+ )
1989
+ )
1867
1990
  )
1868
1991
  ),
1869
1992
  button(
@@ -1969,8 +2092,10 @@ router.post(
1969
2092
  appIcon,
1970
2093
  serverURL,
1971
2094
  splashPage,
2095
+ autoPublicLogin,
1972
2096
  allowOfflineMode,
1973
2097
  synchedTables,
2098
+ includedPlugins,
1974
2099
  } = req.body;
1975
2100
  if (!androidPlatform && !iOSPlatform) {
1976
2101
  return res.json({
@@ -2022,8 +2147,14 @@ router.post(
2022
2147
  if (serverURL) spawnParams.push("-s", serverURL);
2023
2148
  if (splashPage) spawnParams.push("--splashPage", splashPage);
2024
2149
  if (allowOfflineMode) spawnParams.push("--allowOfflineMode");
2150
+ if (autoPublicLogin) spawnParams.push("--autoPublicLogin");
2025
2151
  if (synchedTables?.length > 0)
2026
2152
  spawnParams.push("--synchedTables", ...synchedTables.map((tbl) => tbl));
2153
+ if (includedPlugins?.length > 0)
2154
+ spawnParams.push(
2155
+ "--includedPlugins",
2156
+ ...includedPlugins.map((pluginName) => pluginName)
2157
+ );
2027
2158
  if (
2028
2159
  db.is_it_multi_tenant() &&
2029
2160
  db.getTenantSchema() !== db.connectObj.default_schema
package/routes/api.js CHANGED
@@ -251,7 +251,8 @@ router.get(
251
251
  //passport.authenticate("api-bearer", { session: false }),
252
252
  error_catcher(async (req, res, next) => {
253
253
  let { tableName } = req.params;
254
- const { fields, versioncount, approximate, ...req_query } = req.query;
254
+ const { fields, versioncount, approximate, dereference, ...req_query } =
255
+ req.query;
255
256
  const table = Table.findOne(
256
257
  strictParseInt(tableName)
257
258
  ? { id: strictParseInt(tableName) }
@@ -284,7 +285,7 @@ router.get(
284
285
  },
285
286
  };
286
287
  rows = await table.getJoinedRows(joinOpts);
287
- } else if (req_query && req_query !== {}) {
288
+ } else {
288
289
  const tbl_fields = table.getFields();
289
290
  readState(req_query, tbl_fields, req);
290
291
  const qstate = await stateFieldsToWhere({
@@ -293,18 +294,26 @@ router.get(
293
294
  state: req_query,
294
295
  table,
295
296
  });
296
- rows = await table.getRows(qstate, {
297
+ const joinFields = {};
298
+ const derefs = Array.isArray(dereference)
299
+ ? dereference
300
+ : !dereference
301
+ ? []
302
+ : [dereference];
303
+ derefs.forEach((f) => {
304
+ const field = table.getField(f);
305
+ if (field?.attributes?.summary_field)
306
+ joinFields[`${f}_${field?.attributes?.summary_field}`] = {
307
+ ref: f,
308
+ target: field?.attributes?.summary_field,
309
+ };
310
+ });
311
+ rows = await table.getJoinedRows({
312
+ where: qstate,
313
+ joinFields,
297
314
  forPublic: !(req.user || user),
298
315
  forUser: req.user || user,
299
316
  });
300
- } else {
301
- rows = await table.getRows(
302
- {},
303
- {
304
- forPublic: !(req.user || user),
305
- forUser: req.user || user,
306
- }
307
- );
308
317
  }
309
318
  res.json({ success: rows.map(limitFields(fields)) });
310
319
  } else {
@@ -180,6 +180,7 @@ const customEventForm = async (req) => {
180
180
  name: "name",
181
181
  label: req.__("Event Name"),
182
182
  type: "String",
183
+ required: true,
183
184
  },
184
185
  {
185
186
  name: "hasChannel",
@@ -256,11 +257,11 @@ router.post(
256
257
  * @function
257
258
  */
258
259
  router.post(
259
- "/custom/delete/:name",
260
+ "/custom/delete/:name?",
260
261
  isAdmin,
261
262
  error_catcher(async (req, res) => {
262
- const { name } = req.params;
263
-
263
+ let { name } = req.params;
264
+ if (!name) name = "";
264
265
  const cevs = getState().getConfig("custom_events", []);
265
266
 
266
267
  await getState().setConfig(
package/routes/sync.js CHANGED
@@ -24,9 +24,30 @@ router.get(
24
24
  })
25
25
  );
26
26
 
27
- const getSyncRows = async (syncInfo, table, syncUntil, client) => {
27
+ const getSyncRows = async (syncInfo, table, syncUntil, client, user) => {
28
28
  const tblName = table.name;
29
29
  const pkName = table.pk_name;
30
+ const minRole = table.min_role_read;
31
+ const role = user?.role_id || 100;
32
+ let ownerFieldName = null;
33
+ if (
34
+ role > minRole &&
35
+ ((!table.ownership_field_id && !table.ownership_formula) || role === 100)
36
+ )
37
+ return null;
38
+ if (user?.id && role < 100 && role > minRole && table.ownership_field_id) {
39
+ const ownerField = table
40
+ .getFields()
41
+ .find((f) => f.id === table.ownership_field_id);
42
+ if (!ownerField) {
43
+ getState().log(
44
+ 5,
45
+ `GET /load_changes: The ownership field of '${table.name}' does not exist.`
46
+ );
47
+ return null;
48
+ }
49
+ ownerFieldName = ownerField.name;
50
+ }
30
51
  const schema = db.getTenantSchemaPrefix();
31
52
  if (!syncInfo.syncFrom) {
32
53
  const { rows } = await client.query(
@@ -43,9 +64,9 @@ const getSyncRows = async (syncInfo, table, syncUntil, client) => {
43
64
  on info_tbl.ref = data_tbl."${db.sqlsanitize(
44
65
  pkName
45
66
  )}" and info_tbl.deleted = false
46
- where data_tbl."${db.sqlsanitize(pkName)}" > ${
47
- syncInfo.maxLoadedId
48
- } order by data_tbl."${db.sqlsanitize(pkName)}"`
67
+ where data_tbl."${db.sqlsanitize(pkName)}" > ${syncInfo.maxLoadedId}
68
+ ${ownerFieldName ? `and data_tbl."${ownerFieldName}" = ${user.id}` : ""}
69
+ order by data_tbl."${db.sqlsanitize(pkName)}"`
49
70
  );
50
71
  for (const row of rows) {
51
72
  if (row._sync_info_tbl_last_modified_)
@@ -76,6 +97,7 @@ const getSyncRows = async (syncInfo, table, syncUntil, client) => {
76
97
  })
77
98
  and info_tbl.deleted = false
78
99
  and info_tbl.ref > ${syncInfo.maxLoadedId}
100
+ ${ownerFieldName ? `and data_tbl."${ownerFieldName}" = ${user.id}` : ""}
79
101
  order by info_tbl.ref`
80
102
  );
81
103
  for (const row of rows) {
@@ -114,7 +136,14 @@ router.post(
114
136
  const table = Table.findOne({ name: tblName });
115
137
  if (!table) throw new Error(`The table '${tblName}' does not exists`);
116
138
  const pkName = table.pk_name;
117
- let rows = await getSyncRows(syncInfo, table, loadUntil, client);
139
+ let rows = await getSyncRows(
140
+ syncInfo,
141
+ table,
142
+ loadUntil,
143
+ client,
144
+ req.user
145
+ );
146
+ if (!rows) continue;
118
147
  if (role > table.min_role_read) {
119
148
  if (
120
149
  role === 100 ||
package/routes/tables.js CHANGED
@@ -90,7 +90,7 @@ const tableForm = async (table, req) => {
90
90
  noSubmitButton: true,
91
91
  onChange: "saveAndContinue(this)",
92
92
  fields: [
93
- ...(!table.external
93
+ ...(!table.external && !table.provider_name
94
94
  ? [
95
95
  {
96
96
  label: req.__("Ownership field"),
@@ -146,9 +146,9 @@ const tableForm = async (table, req) => {
146
146
  name: "min_role_read",
147
147
  input_type: "select",
148
148
  options: roleOptions,
149
- attributes: { asideNext: !table.external },
149
+ attributes: { asideNext: !table.external && !table.provider_name },
150
150
  },
151
- ...(table.external
151
+ ...(table.external || table.provider_name
152
152
  ? []
153
153
  : [
154
154
  {
@@ -790,6 +790,7 @@ router.get(
790
790
  "<br>"
791
791
  : "",
792
792
  !table.external &&
793
+ !table.provider_name &&
793
794
  a(
794
795
  {
795
796
  href: `/field/new/${table.id}`,
@@ -903,6 +904,7 @@ router.get(
903
904
  )
904
905
  ),
905
906
  !table.external &&
907
+ !table.provider_name &&
906
908
  div(
907
909
  { class: "mx-auto" },
908
910
  form(
@@ -929,6 +931,7 @@ router.get(
929
931
  )
930
932
  ),
931
933
  !table.external &&
934
+ !table.provider_name &&
932
935
  div(
933
936
  { class: "mx-auto" },
934
937
  a(
@@ -944,6 +947,7 @@ router.get(
944
947
 
945
948
  // only if table is not external
946
949
  !table.external &&
950
+ !table.provider_name &&
947
951
  div(
948
952
  { class: "mx-auto" },
949
953
  settingsDropdown(`dataMenuButton`, [
package/tests/api.test.js CHANGED
@@ -127,6 +127,22 @@ describe("API read", () => {
127
127
  .set("Cookie", loginCookie)
128
128
  .expect(succeedJsonWith((rows) => rows.length == 2));
129
129
  });
130
+ it("should dereference", async () => {
131
+ const loginCookie = await getStaffLoginCookie();
132
+
133
+ const app = await getApp({ disableCsrf: true });
134
+ await request(app)
135
+ .get("/api/patients/?dereference=favbook")
136
+ .set("Cookie", loginCookie)
137
+ .expect(
138
+ succeedJsonWith(
139
+ (rows) =>
140
+ rows.length == 2 &&
141
+ rows.find((r) => r.favbook === 1).favbook_author ==
142
+ "Herman Melville"
143
+ )
144
+ );
145
+ });
130
146
  it("should add version counts", async () => {
131
147
  const patients = Table.findOne({ name: "patients" });
132
148
  await patients.update({ versioned: true });
@@ -10,6 +10,8 @@ const db = require("@saltcorn/data/db");
10
10
  const { sleep } = require("@saltcorn/data/tests/mocks");
11
11
 
12
12
  const Table = require("@saltcorn/data/models/table");
13
+ const Field = require("@saltcorn/data/models/field");
14
+ const User = require("@saltcorn/data/models/user");
13
15
 
14
16
  beforeAll(async () => {
15
17
  await resetToFixtures();
@@ -32,7 +34,7 @@ const initSyncInfo = async (tbls) => {
32
34
  describe("load remote insert/updates", () => {
33
35
  if (!db.isSQLite) {
34
36
  beforeAll(async () => {
35
- await initSyncInfo(["books", "publisher"]);
37
+ await initSyncInfo(["books", "publisher", "patients"]);
36
38
  });
37
39
  it("check params", async () => {
38
40
  const app = await getApp({ disableCsrf: true });
@@ -178,6 +180,99 @@ describe("load remote insert/updates", () => {
178
180
  expect(data.books.rows[1].author).toBe("Leo Tolstoy");
179
181
  }
180
182
  });
183
+
184
+ it("load sync not authorized", async () => {
185
+ const app = await getApp({ disableCsrf: true });
186
+ const loginCookie = await getUserLoginCookie();
187
+ const loadUntil = new Date();
188
+ const resp = await request(app)
189
+ .post("/sync/load_changes")
190
+ .set("Cookie", loginCookie)
191
+ .send({
192
+ loadUntil: loadUntil.valueOf(),
193
+ syncInfos: {
194
+ patients: {
195
+ maxLoadedId: 0,
196
+ syncFrom: 1000,
197
+ },
198
+ },
199
+ });
200
+ expect(resp.status).toBe(200);
201
+ const data = resp._body;
202
+ expect(Object.keys(data).length).toBe(0);
203
+ });
204
+
205
+ const addOwnerField = async () => {
206
+ const patients = Table.findOne({ name: "patients" });
207
+ const users = Table.findOne({ name: "users" });
208
+ const ownerField = await Field.create({
209
+ table: patients,
210
+ name: "owner",
211
+ label: "Pages",
212
+ type: "Key",
213
+ reftable: users,
214
+ attributes: { summary_field: "id" },
215
+ });
216
+ patients.ownership_field_id = ownerField.id;
217
+ await patients.update(patients);
218
+ const user = await User.findOne({ email: "user@foo.com" });
219
+ await patients.updateRow({ owner: user.id }, 1);
220
+ };
221
+
222
+ it("load sync authorized with ownership", async () => {
223
+ await addOwnerField();
224
+ const app = await getApp({ disableCsrf: true });
225
+ const loginCookie = await getUserLoginCookie();
226
+ const loadUntil = new Date();
227
+ const resp = await request(app)
228
+ .post("/sync/load_changes")
229
+ .set("Cookie", loginCookie)
230
+ .send({
231
+ loadUntil: loadUntil.valueOf(),
232
+ syncInfos: {
233
+ patients: {
234
+ maxLoadedId: 0,
235
+ },
236
+ },
237
+ });
238
+ expect(resp.status).toBe(200);
239
+ const data = resp._body;
240
+ expect(Object.keys(data).length).toBe(1);
241
+ expect(data.patients).toBeDefined();
242
+ expect(data.patients.rows.length).toBe(1);
243
+ expect(data.patients.rows[0].id).toBe(1);
244
+ });
245
+
246
+ it("load sync authorized with ownership and syncFrom", async () => {
247
+ const patients = Table.findOne({ name: "patients" });
248
+ if (!patients.ownership_field_id) await addOwnerField();
249
+ const rows = await patients.getRows();
250
+ for (const row of rows) {
251
+ await patients.updateRow(row, row.id);
252
+ }
253
+
254
+ const app = await getApp({ disableCsrf: true });
255
+ const loginCookie = await getUserLoginCookie();
256
+ const loadUntil = new Date();
257
+ const resp = await request(app)
258
+ .post("/sync/load_changes")
259
+ .set("Cookie", loginCookie)
260
+ .send({
261
+ loadUntil: loadUntil.valueOf(),
262
+ syncInfos: {
263
+ patients: {
264
+ maxLoadedId: 0,
265
+ syncFrom: 1000,
266
+ },
267
+ },
268
+ });
269
+ expect(resp.status).toBe(200);
270
+ const data = resp._body;
271
+ expect(Object.keys(data).length).toBe(1);
272
+ expect(data.patients).toBeDefined();
273
+ expect(data.patients.rows.length).toBe(1);
274
+ expect(data.patients.rows[0].id).toBe(1);
275
+ });
181
276
  } else
182
277
  it("only pq support", () => {
183
278
  expect(true).toBe(true);
@@ -9,13 +9,12 @@ const {
9
9
  toNotInclude,
10
10
  resetToFixtures,
11
11
  respondJsonWith,
12
+ toSucceed,
12
13
  } = require("../auth/testhelp");
13
14
  const db = require("@saltcorn/data/db");
14
15
  const { getState } = require("@saltcorn/data/db/state");
15
16
  const View = require("@saltcorn/data/models/view");
16
17
  const Table = require("@saltcorn/data/models/table");
17
- const Trigger = require("@saltcorn/data/models/trigger");
18
- const Page = require("@saltcorn/data/models/page");
19
18
 
20
19
  const { plugin_with_routes } = require("@saltcorn/data/tests/mocks");
21
20
 
@@ -397,6 +396,70 @@ describe("action row_variable", () => {
397
396
  });
398
397
  });
399
398
 
399
+ describe("update matching rows", () => {
400
+ const updateMatchingRows = async ({ query, body }) => {
401
+ const app = await getApp({ disableCsrf: true });
402
+ const loginCookie = await getAdminLoginCookie();
403
+ await request(app)
404
+ .post(
405
+ `/view/author_multi_edit/update_matching_rows${
406
+ query ? `?${query}` : ""
407
+ }`
408
+ )
409
+ .set("Cookie", loginCookie)
410
+ .send(body)
411
+ .set("Content-Type", "application/json")
412
+ .set("Accept", "application/json")
413
+ .expect(toSucceed(302));
414
+ };
415
+
416
+ beforeAll(async () => {
417
+ const table = Table.findOne({ name: "books" });
418
+ const field = table.getFields().find((f) => f.name === "author");
419
+ await field.update({ is_unique: false });
420
+ });
421
+
422
+ it("update matching books normal", async () => {
423
+ const table = Table.findOne({ name: "books" });
424
+ await updateMatchingRows({
425
+ query: "author=leo&publisher=1",
426
+ body: { author: "new_author" },
427
+ });
428
+ let actualRows = await table.getRows({ author: "new_author" });
429
+ expect(actualRows.length).toBe(1);
430
+ await updateMatchingRows({
431
+ query: "_gte_pages=600",
432
+ body: { author: "more_than" },
433
+ });
434
+ actualRows = await table.getRows({ author: "more_than" });
435
+ expect(actualRows.length >= 2).toBe(true);
436
+ const expected = (await table.getRows()).map((row) => {
437
+ return { id: row.id, author: "agi", pages: 100, publisher: null };
438
+ });
439
+ await updateMatchingRows({
440
+ body: { author: "agi", pages: 100, publisher: null },
441
+ });
442
+ actualRows = await table.getRows({});
443
+ expect(actualRows).toEqual(expected);
444
+ });
445
+
446
+ it("update matching books with edit-in-edit", async () => {
447
+ const disBooks = Table.findOne({ name: "discusses_books" });
448
+ await updateMatchingRows({
449
+ query: "id=2",
450
+ body: { author: "Leo Tolstoy" },
451
+ });
452
+ await updateMatchingRows({
453
+ query: "author=leo",
454
+ body: { author: "agi", discussant_0: "1", discussant_1: "2" },
455
+ });
456
+ const discBooksRows = (await disBooks.getRows({ book: 2 })).filter(
457
+ ({ discussant }) => discussant === 1 || discussant === 2
458
+ );
459
+ expect(discBooksRows.length).toBe(2);
460
+ });
461
+ });
462
+
400
463
  describe("inbound relations", () => {
401
464
  it("view with inbound relation", async () => {
402
465
  const app = await getApp({ disableCsrf: true });