@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 +46 -16
- package/locales/en.json +7 -1
- package/locales/it.json +17 -2
- package/package.json +8 -8
- package/public/saltcorn-common.js +34 -6
- package/public/saltcorn.js +56 -0
- package/routes/admin.js +135 -4
- package/routes/api.js +20 -11
- package/routes/eventlog.js +4 -3
- package/routes/sync.js +34 -5
- package/routes/tables.js +7 -3
- package/tests/api.test.js +16 -0
- package/tests/sync.test.js +96 -1
- package/tests/view.test.js +65 -2
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: [
|
|
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: [
|
|
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 (
|
|
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(
|
|
948
|
-
|
|
949
|
-
if (
|
|
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
|
-
|
|
953
|
-
if (!
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
+
"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.
|
|
10
|
-
"@saltcorn/builder": "0.8.8-beta.
|
|
11
|
-
"@saltcorn/data": "0.8.8-beta.
|
|
12
|
-
"@saltcorn/admin-models": "0.8.8-beta.
|
|
13
|
-
"@saltcorn/filemanager": "0.8.8-beta.
|
|
14
|
-
"@saltcorn/markup": "0.8.8-beta.
|
|
15
|
-
"@saltcorn/sbadmin2": "0.8.8-beta.
|
|
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
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
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
|
|
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) => {
|
package/public/saltcorn.js
CHANGED
|
@@ -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:
|
|
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-
|
|
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 } =
|
|
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
|
|
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
|
-
|
|
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 {
|
package/routes/eventlog.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
48
|
-
|
|
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(
|
|
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 });
|
package/tests/sync.test.js
CHANGED
|
@@ -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);
|
package/tests/view.test.js
CHANGED
|
@@ -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 });
|