@saltcorn/server 0.8.6-beta.3 → 0.8.6-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/locales/en.json +3 -2
- package/package.json +8 -8
- package/public/saltcorn-common.js +51 -34
- package/public/saltcorn.css +17 -0
- package/public/saltcorn.js +1 -1
- package/routes/admin.js +3 -0
- package/routes/api.js +6 -28
- package/routes/fields.js +25 -12
- package/routes/sync.js +33 -119
- package/routes/tables.js +1 -5
- package/routes/viewedit.js +19 -4
- package/tests/sync.test.js +45 -124
package/locales/en.json
CHANGED
|
@@ -1166,5 +1166,6 @@
|
|
|
1166
1166
|
"Event logs": "Event logs",
|
|
1167
1167
|
"Migrations": "Migrations",
|
|
1168
1168
|
"Tag Entries": "Tag Entries",
|
|
1169
|
-
"Not a valid field name": "Not a valid field name"
|
|
1170
|
-
|
|
1169
|
+
"Not a valid field name": "Not a valid field name",
|
|
1170
|
+
"Set a default value for missing data": "Set a default value for missing data"
|
|
1171
|
+
}
|
package/package.json
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saltcorn/server",
|
|
3
|
-
"version": "0.8.6-beta.
|
|
3
|
+
"version": "0.8.6-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.6-beta.
|
|
10
|
-
"@saltcorn/builder": "0.8.6-beta.
|
|
11
|
-
"@saltcorn/data": "0.8.6-beta.
|
|
12
|
-
"@saltcorn/admin-models": "0.8.6-beta.
|
|
13
|
-
"@saltcorn/filemanager": "0.8.6-beta.
|
|
14
|
-
"@saltcorn/markup": "0.8.6-beta.
|
|
15
|
-
"@saltcorn/sbadmin2": "0.8.6-beta.
|
|
9
|
+
"@saltcorn/base-plugin": "0.8.6-beta.5",
|
|
10
|
+
"@saltcorn/builder": "0.8.6-beta.5",
|
|
11
|
+
"@saltcorn/data": "0.8.6-beta.5",
|
|
12
|
+
"@saltcorn/admin-models": "0.8.6-beta.5",
|
|
13
|
+
"@saltcorn/filemanager": "0.8.6-beta.5",
|
|
14
|
+
"@saltcorn/markup": "0.8.6-beta.5",
|
|
15
|
+
"@saltcorn/sbadmin2": "0.8.6-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",
|
|
@@ -435,6 +435,7 @@ function reload_on_init() {
|
|
|
435
435
|
localStorage.setItem("reload_on_init", true);
|
|
436
436
|
}
|
|
437
437
|
function initialize_page() {
|
|
438
|
+
const isNode = typeof parent?.saltcorn?.data?.state === "undefined";
|
|
438
439
|
//console.log("init page");
|
|
439
440
|
$(".blur-on-enter-keypress").bind("keyup", function (e) {
|
|
440
441
|
if (e.keyCode === 13) e.target.blur();
|
|
@@ -480,7 +481,9 @@ function initialize_page() {
|
|
|
480
481
|
if ($(this).find(".editicon").length === 0) {
|
|
481
482
|
var current = $(this).html();
|
|
482
483
|
$(this).html(
|
|
483
|
-
`<span class="current">${current}</span><i class="editicon
|
|
484
|
+
`<span class="current">${current}</span><i class="editicon ${
|
|
485
|
+
!isNode ? "visible" : ""
|
|
486
|
+
} fas fa-edit ms-1"></i>`
|
|
484
487
|
);
|
|
485
488
|
}
|
|
486
489
|
});
|
|
@@ -546,12 +549,20 @@ function initialize_page() {
|
|
|
546
549
|
} else
|
|
547
550
|
$(this).replaceWith(
|
|
548
551
|
`<form method="post" action="${url}" ${
|
|
549
|
-
ajax
|
|
552
|
+
ajax
|
|
553
|
+
? `onsubmit="inline_${
|
|
554
|
+
isNode ? "ajax" : "local"
|
|
555
|
+
}_submit(event, '${opts}')"`
|
|
556
|
+
: ""
|
|
550
557
|
}>
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
558
|
+
${
|
|
559
|
+
isNode
|
|
560
|
+
? `<input type="hidden" name="_csrf" value="${_sc_globalCsrf}"></input>`
|
|
561
|
+
: ""
|
|
562
|
+
}
|
|
563
|
+
<input type="${
|
|
564
|
+
type === "Integer" || type === "Float" ? "number" : "text"
|
|
565
|
+
}" name="${key}" value="${escapeHtml(current)}">
|
|
555
566
|
<button type="submit" class="btn btn-sm btn-primary">OK</button>
|
|
556
567
|
<button onclick="cancel_inline_edit(event, '${opts}')" type="button" class="btn btn-sm btn-danger"><i class="fas fa-times"></i></button>
|
|
557
568
|
</form>`
|
|
@@ -641,6 +652,7 @@ function initialize_page() {
|
|
|
641
652
|
$(initialize_page);
|
|
642
653
|
|
|
643
654
|
function cancel_inline_edit(e, opts1) {
|
|
655
|
+
const isNode = typeof parent?.saltcorn?.data?.state === "undefined";
|
|
644
656
|
var opts = JSON.parse(decodeURIComponent(opts1 || "") || "{}");
|
|
645
657
|
var form = $(e.target).closest("form");
|
|
646
658
|
var json_fk_opt;
|
|
@@ -668,17 +680,47 @@ function cancel_inline_edit(e, opts1) {
|
|
|
668
680
|
<span class="current">${
|
|
669
681
|
json_fk_opt || opts.current_label || opts.current
|
|
670
682
|
}</span>
|
|
671
|
-
<i class="editicon fas fa-edit ms-1"></i>
|
|
683
|
+
<i class="editicon ${!isNode ? "visible" : ""} fas fa-edit ms-1"></i>
|
|
672
684
|
</div>`);
|
|
673
685
|
initialize_page();
|
|
674
686
|
}
|
|
675
687
|
|
|
688
|
+
function inline_submit_success(e, form, opts) {
|
|
689
|
+
const isNode = typeof parent?.saltcorn?.data?.state === "undefined";
|
|
690
|
+
const formDataArray = form.serializeArray();
|
|
691
|
+
if (opts) {
|
|
692
|
+
let rawVal = formDataArray.find((f) => f.name == opts.key).value;
|
|
693
|
+
let val =
|
|
694
|
+
opts.is_key || (opts.schema && opts.schema.type.startsWith("Key to "))
|
|
695
|
+
? form.find("select").find("option:selected").text()
|
|
696
|
+
: rawVal;
|
|
697
|
+
|
|
698
|
+
$(e.target).replaceWith(`<div
|
|
699
|
+
data-inline-edit-field="${opts.key}"
|
|
700
|
+
${opts.ajax ? `data-inline-edit-ajax="true"` : ""}
|
|
701
|
+
${opts.type ? `data-inline-edit-type="${opts.type}"` : ""}
|
|
702
|
+
${opts.current ? `data-inline-edit-current="${rawVal}"` : ""}
|
|
703
|
+
${
|
|
704
|
+
opts.schema
|
|
705
|
+
? `data-inline-edit-schema="${encodeURIComponent(
|
|
706
|
+
JSON.stringify(opts.schema)
|
|
707
|
+
)}"`
|
|
708
|
+
: ""
|
|
709
|
+
}
|
|
710
|
+
${opts.current_label ? `data-inline-edit-current-label="${val}"` : ""}
|
|
711
|
+
data-inline-edit-dest-url="${opts.url}">
|
|
712
|
+
<span class="current">${val}</span>
|
|
713
|
+
<i class="editicon ${!isNode ? "visible" : ""} fas fa-edit ms-1"></i>
|
|
714
|
+
</div>`);
|
|
715
|
+
initialize_page();
|
|
716
|
+
} else location.reload();
|
|
717
|
+
}
|
|
718
|
+
|
|
676
719
|
function inline_ajax_submit(e, opts1) {
|
|
677
720
|
var opts = JSON.parse(decodeURIComponent(opts1 || "") || "{}");
|
|
678
721
|
e.preventDefault();
|
|
679
722
|
var form = $(e.target).closest("form");
|
|
680
723
|
var form_data = form.serialize();
|
|
681
|
-
var formDataArray = form.serializeArray();
|
|
682
724
|
var url = form.attr("action");
|
|
683
725
|
$.ajax(url, {
|
|
684
726
|
type: "POST",
|
|
@@ -687,32 +729,7 @@ function inline_ajax_submit(e, opts1) {
|
|
|
687
729
|
},
|
|
688
730
|
data: form_data,
|
|
689
731
|
success: function (res) {
|
|
690
|
-
|
|
691
|
-
let rawVal = formDataArray.find((f) => f.name == opts.key).value;
|
|
692
|
-
let val =
|
|
693
|
-
opts.is_key || (opts.schema && opts.schema.type.startsWith("Key to "))
|
|
694
|
-
? form.find("select").find("option:selected").text()
|
|
695
|
-
: rawVal;
|
|
696
|
-
|
|
697
|
-
$(e.target).replaceWith(`<div
|
|
698
|
-
data-inline-edit-field="${opts.key}"
|
|
699
|
-
${opts.ajax ? `data-inline-edit-ajax="true"` : ""}
|
|
700
|
-
${opts.type ? `data-inline-edit-type="${opts.type}"` : ""}
|
|
701
|
-
${opts.current ? `data-inline-edit-current="${rawVal}"` : ""}
|
|
702
|
-
${
|
|
703
|
-
opts.schema
|
|
704
|
-
? `data-inline-edit-schema="${encodeURIComponent(
|
|
705
|
-
JSON.stringify(opts.schema)
|
|
706
|
-
)}"`
|
|
707
|
-
: ""
|
|
708
|
-
}
|
|
709
|
-
${opts.current_label ? `data-inline-edit-current-label="${val}"` : ""}
|
|
710
|
-
data-inline-edit-dest-url="${opts.url}">
|
|
711
|
-
<span class="current">${val}</span>
|
|
712
|
-
<i class="editicon fas fa-edit ms-1"></i>
|
|
713
|
-
</div>`);
|
|
714
|
-
initialize_page();
|
|
715
|
-
} else location.reload();
|
|
732
|
+
inline_submit_success(e, form, opts);
|
|
716
733
|
},
|
|
717
734
|
error: function (e) {
|
|
718
735
|
ajax_done(
|
package/public/saltcorn.css
CHANGED
|
@@ -402,3 +402,20 @@ table.table-inner-grid td {
|
|
|
402
402
|
div.unread-notify {
|
|
403
403
|
border-left: 4px solid green;
|
|
404
404
|
}
|
|
405
|
+
|
|
406
|
+
.mobile-data-inline-edit {
|
|
407
|
+
position: relative;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
.mobile-data-inline-edit::after {
|
|
411
|
+
content: "";
|
|
412
|
+
position: absolute;
|
|
413
|
+
top: -25%;
|
|
414
|
+
left: 0%;
|
|
415
|
+
width: 100%;
|
|
416
|
+
height: 150%;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
.mt-6 {
|
|
420
|
+
margin-top: 5rem;
|
|
421
|
+
}
|
package/public/saltcorn.js
CHANGED
|
@@ -79,7 +79,7 @@ function set_state_field(key, value) {
|
|
|
79
79
|
function check_state_field(that) {
|
|
80
80
|
const checked = that.checked;
|
|
81
81
|
const name = that.name;
|
|
82
|
-
const value = that.value;
|
|
82
|
+
const value = encodeURIComponent(that.value);
|
|
83
83
|
var separator = window.location.href.indexOf("?") !== -1 ? "&" : "?";
|
|
84
84
|
let dest;
|
|
85
85
|
if (checked) dest = get_current_state_url() + `${separator}${name}=${value}`;
|
package/routes/admin.js
CHANGED
package/routes/api.js
CHANGED
|
@@ -19,6 +19,7 @@ const Router = require("express-promise-router");
|
|
|
19
19
|
const { error_catcher } = require("./utils.js");
|
|
20
20
|
//const { mkTable, renderForm, link, post_btn } = require("@saltcorn/markup");
|
|
21
21
|
const { getState } = require("@saltcorn/data/db/state");
|
|
22
|
+
const { prepare_update_row } = require("@saltcorn/data/web-mobile-commons");
|
|
22
23
|
const Table = require("@saltcorn/data/models/table");
|
|
23
24
|
const View = require("@saltcorn/data/models/view");
|
|
24
25
|
//const Field = require("@saltcorn/data/models/field");
|
|
@@ -410,7 +411,8 @@ router.post(
|
|
|
410
411
|
if (
|
|
411
412
|
field.required &&
|
|
412
413
|
!field.primary_key &&
|
|
413
|
-
typeof row[field.name] === "undefined"
|
|
414
|
+
typeof row[field.name] === "undefined" &&
|
|
415
|
+
!field.attributes.default
|
|
414
416
|
) {
|
|
415
417
|
hasErrors = true;
|
|
416
418
|
errors.push(`${field.name}: required`);
|
|
@@ -459,39 +461,15 @@ router.post(
|
|
|
459
461
|
return;
|
|
460
462
|
}
|
|
461
463
|
await passport.authenticate(
|
|
462
|
-
"api-bearer",
|
|
464
|
+
["api-bearer", "jwt"],
|
|
463
465
|
{ session: false },
|
|
464
466
|
async function (err, user, info) {
|
|
465
467
|
if (accessAllowedWrite(req, user, table)) {
|
|
466
468
|
const { _versions, ...row } = req.body;
|
|
467
469
|
const fields = table.getFields();
|
|
468
470
|
readState(row, fields, req);
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
for (const k of Object.keys(row)) {
|
|
472
|
-
const field = fields.find((f) => f.name === k);
|
|
473
|
-
if (!field && k.includes(".")) {
|
|
474
|
-
const [fnm, jkey] = k.split(".");
|
|
475
|
-
const jfield = fields.find((f) => f.name === fnm);
|
|
476
|
-
if (jfield?.type?.name === "JSON") {
|
|
477
|
-
if (typeof row[fnm] === "undefined") {
|
|
478
|
-
const dbrow = await table.getRow({ [table.pk_name]: id });
|
|
479
|
-
row[fnm] = dbrow[fnm] || {};
|
|
480
|
-
}
|
|
481
|
-
row[fnm][jkey] = row[k];
|
|
482
|
-
delete row[k];
|
|
483
|
-
}
|
|
484
|
-
} else if (!field || field.calculated) {
|
|
485
|
-
delete row[k];
|
|
486
|
-
} else if (field?.type && field.type.validate) {
|
|
487
|
-
const vres = field.type.validate(field.attributes || {})(row[k]);
|
|
488
|
-
if (vres.error) {
|
|
489
|
-
hasErrors = true;
|
|
490
|
-
errors.push(`${k}: ${vres.error}`);
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
if (hasErrors) {
|
|
471
|
+
const errors = await prepare_update_row(table, row, id);
|
|
472
|
+
if (errors.length > 0) {
|
|
495
473
|
getState().log(
|
|
496
474
|
2,
|
|
497
475
|
`API POST ${table.name} error: ${errors.join(", ")}`
|
package/routes/fields.js
CHANGED
|
@@ -86,7 +86,7 @@ const fieldForm = async (req, fkey_opts, existing_names, id, hasData) => {
|
|
|
86
86
|
if (Field.labelToName(s) === "row")
|
|
87
87
|
return req.__("Not a valid field name");
|
|
88
88
|
try {
|
|
89
|
-
new Function(s, "return;");
|
|
89
|
+
new Function(Field.labelToName(s), "return;");
|
|
90
90
|
} catch {
|
|
91
91
|
return req.__("Not a valid field name");
|
|
92
92
|
}
|
|
@@ -471,14 +471,11 @@ const fieldFlow = (req) =>
|
|
|
471
471
|
},
|
|
472
472
|
{
|
|
473
473
|
name: req.__("Default"),
|
|
474
|
-
onlyWhen: async (context) =>
|
|
475
|
-
|
|
476
|
-
|
|
474
|
+
onlyWhen: async (context) => context.required && !context.calculated,
|
|
475
|
+
|
|
476
|
+
form: async (context) => {
|
|
477
477
|
const table = await Table.findOne({ id: context.table_id });
|
|
478
478
|
const nrows = await table.countRows();
|
|
479
|
-
return nrows > 0;
|
|
480
|
-
},
|
|
481
|
-
form: async (context) => {
|
|
482
479
|
const formfield = new Field({
|
|
483
480
|
name: "default",
|
|
484
481
|
label: req.__("Default"),
|
|
@@ -491,12 +488,28 @@ const fieldFlow = (req) =>
|
|
|
491
488
|
},
|
|
492
489
|
});
|
|
493
490
|
await formfield.fill_fkey_options();
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
491
|
+
const defaultOptional = nrows === 0 || context.id;
|
|
492
|
+
if (defaultOptional) formfield.showIf = { set_default: true };
|
|
493
|
+
|
|
494
|
+
const form = new Form({
|
|
495
|
+
blurb: defaultOptional
|
|
496
|
+
? req.__("Set a default value for missing data")
|
|
497
|
+
: req.__(
|
|
498
|
+
"A default value is required when adding required fields to nonempty tables"
|
|
499
|
+
),
|
|
500
|
+
fields: [
|
|
501
|
+
...(defaultOptional
|
|
502
|
+
? [{ name: "set_default", label: "Set Default", type: "Bool" }]
|
|
503
|
+
: []),
|
|
504
|
+
formfield,
|
|
505
|
+
],
|
|
499
506
|
});
|
|
507
|
+
if (
|
|
508
|
+
typeof context.default !== "undefined" &&
|
|
509
|
+
context.default !== null
|
|
510
|
+
)
|
|
511
|
+
form.values.set_default = true;
|
|
512
|
+
return form;
|
|
500
513
|
},
|
|
501
514
|
},
|
|
502
515
|
],
|
package/routes/sync.js
CHANGED
|
@@ -1,53 +1,16 @@
|
|
|
1
1
|
const { error_catcher } = require("./utils.js");
|
|
2
|
-
const Table = require("@saltcorn/data/models/table");
|
|
3
2
|
const Router = require("express-promise-router");
|
|
4
3
|
const db = require("@saltcorn/data/db");
|
|
5
4
|
const { getState } = require("@saltcorn/data/db/state");
|
|
5
|
+
const Table = require("@saltcorn/data/models/table");
|
|
6
6
|
|
|
7
7
|
const router = new Router();
|
|
8
8
|
module.exports = router;
|
|
9
9
|
|
|
10
|
-
/**
|
|
11
|
-
* Send all rows from a user, so that they can be used in an offline session with the mobile app
|
|
12
|
-
*/
|
|
13
|
-
router.get(
|
|
14
|
-
"/table_data",
|
|
15
|
-
error_catcher(async (req, res) => {
|
|
16
|
-
// TODO optimsie: hash over all rows or dynamic user specific
|
|
17
|
-
// TODO public user
|
|
18
|
-
// TODO split large data 10 000 rows?
|
|
19
|
-
getState().log(
|
|
20
|
-
4,
|
|
21
|
-
`GET /sync/table_data user: '${req.user ? req.user.id : "public"}'`
|
|
22
|
-
);
|
|
23
|
-
const allTables = await Table.find();
|
|
24
|
-
const result = {};
|
|
25
|
-
const selectOpts = req.user ? { forUser: req.user } : { forPublic: true };
|
|
26
|
-
for (const table of allTables) {
|
|
27
|
-
const rows = await table.getRows({}, selectOpts);
|
|
28
|
-
if (
|
|
29
|
-
req.user &&
|
|
30
|
-
table.name === "users" &&
|
|
31
|
-
!rows.find((row) => row.id === req.user.id)
|
|
32
|
-
) {
|
|
33
|
-
rows.push(await table.getRow({ id: req.user.id }));
|
|
34
|
-
}
|
|
35
|
-
result[table.name] = {
|
|
36
|
-
rows:
|
|
37
|
-
table.name !== "users"
|
|
38
|
-
? rows
|
|
39
|
-
: rows.map(({ id, email, role_id, language, disabled }) => {
|
|
40
|
-
return { id, email, role_id, language, disabled };
|
|
41
|
-
}),
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
res.json(result);
|
|
45
|
-
})
|
|
46
|
-
);
|
|
47
|
-
|
|
48
10
|
const pickFields = (table, row) => {
|
|
49
11
|
const result = {};
|
|
50
12
|
for (const { name, type } of table.getFields()) {
|
|
13
|
+
if (name === "id") continue;
|
|
51
14
|
if (type?.name === "Date") {
|
|
52
15
|
result[name] = row[name] ? new Date(row[name]) : undefined;
|
|
53
16
|
} else {
|
|
@@ -57,114 +20,65 @@ const pickFields = (table, row) => {
|
|
|
57
20
|
return result;
|
|
58
21
|
};
|
|
59
22
|
|
|
60
|
-
const
|
|
61
|
-
const changes = {};
|
|
62
|
-
for (const { name, type } of table.getFields()) {
|
|
63
|
-
if (name !== "id") {
|
|
64
|
-
const dbVal = dbRow[name];
|
|
65
|
-
const appVal = appRow[name];
|
|
66
|
-
let valHasChanged = false;
|
|
67
|
-
if (type?.name === "Date") {
|
|
68
|
-
valHasChanged = dbVal?.valueOf() !== appVal?.valueOf();
|
|
69
|
-
} else {
|
|
70
|
-
valHasChanged = dbVal !== appVal;
|
|
71
|
-
}
|
|
72
|
-
// TODO Float with decimal_places
|
|
73
|
-
if (valHasChanged) {
|
|
74
|
-
changes[name] = appRow[name];
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
return changes;
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
const allowUpdate = (table, row, user) => {
|
|
82
|
-
const role = user?.role_id || 100;
|
|
83
|
-
return table.min_role_write >= role || table.is_owner(user, row);
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
const allowInsert = (table, row, user) => {
|
|
23
|
+
const allowInsert = (table, user) => {
|
|
87
24
|
const role = user?.role_id || 100;
|
|
88
25
|
return table.min_role_write >= role;
|
|
89
26
|
};
|
|
90
27
|
|
|
91
|
-
const
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
const translatedIds = [];
|
|
97
|
-
for (const appRow of appRows.map((row) => pickFields(table, row))) {
|
|
98
|
-
if (!appRow.id) continue;
|
|
99
|
-
const dbRow = dbRowsLookup[appRow.id];
|
|
100
|
-
if (dbRow) {
|
|
101
|
-
const changes = getChanges(table, dbRow, appRow);
|
|
102
|
-
if (Object.keys(changes).length > 0 && allowUpdate(table, dbRow, user)) {
|
|
103
|
-
await db.update(table.name, changes, dbRow.id, { client: dbClient });
|
|
104
|
-
}
|
|
105
|
-
} else if (allowInsert(table, appRow, user)) {
|
|
106
|
-
const idFromApp = appRow.id;
|
|
107
|
-
delete appRow.id;
|
|
108
|
-
const newId = await db.insert(table.name, appRow, { client: dbClient });
|
|
109
|
-
if (newId !== idFromApp)
|
|
110
|
-
translatedIds.push({ from: idFromApp, to: newId });
|
|
111
|
-
} else {
|
|
112
|
-
getState().log(
|
|
113
|
-
3,
|
|
114
|
-
`Skipping id: '${appRow.id}' from app of table '${table.name}'`
|
|
115
|
-
);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
return translatedIds;
|
|
28
|
+
const throwWithCode = (message, code) => {
|
|
29
|
+
const err = new Error(message);
|
|
30
|
+
err.statusCode = code;
|
|
31
|
+
throw err;
|
|
119
32
|
};
|
|
120
33
|
|
|
121
34
|
/**
|
|
122
|
-
*
|
|
35
|
+
* insert the offline data uploaded by the mobile-app
|
|
123
36
|
*/
|
|
124
37
|
router.post(
|
|
125
38
|
"/table_data",
|
|
126
39
|
error_catcher(async (req, res) => {
|
|
127
|
-
// TODO public user
|
|
128
40
|
// TODO sqlite
|
|
129
41
|
getState().log(
|
|
130
42
|
4,
|
|
131
43
|
`POST /sync/table_data user: '${req.user ? req.user.id : "public"}'`
|
|
132
44
|
);
|
|
133
|
-
|
|
45
|
+
let aborted = false;
|
|
46
|
+
req.socket.on("close", () => {
|
|
47
|
+
aborted = true;
|
|
48
|
+
});
|
|
49
|
+
req.socket.on("timeout", () => {
|
|
50
|
+
aborted = true;
|
|
51
|
+
});
|
|
134
52
|
const client = db.isSQLite ? db : await db.getClient();
|
|
135
|
-
const selectOpts = req.user ? { forUser: req.user } : { forPublic: true };
|
|
136
53
|
try {
|
|
137
54
|
await client.query("BEGIN");
|
|
138
55
|
await client.query("SET CONSTRAINTS ALL DEFERRED");
|
|
139
|
-
const
|
|
140
|
-
|
|
56
|
+
for (const [tblName, offlineRows] of Object.entries(req.body.data) ||
|
|
57
|
+
[]) {
|
|
58
|
+
const table = Table.findOne({ name: tblName });
|
|
59
|
+
if (!table) throw new Error(`The table '${tblName}' does not exist.`);
|
|
60
|
+
if (!allowInsert(table, req.user))
|
|
61
|
+
throwWithCode(req.__("Not authorized"), 401);
|
|
141
62
|
if (tblName !== "users") {
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
: (await table.getRows({}, selectOpts)).filter((row) =>
|
|
148
|
-
table.is_owner(req.user, row)
|
|
149
|
-
);
|
|
150
|
-
const translated = await syncRows(
|
|
151
|
-
table,
|
|
152
|
-
dbRows,
|
|
153
|
-
appRows,
|
|
154
|
-
req.user,
|
|
155
|
-
client
|
|
156
|
-
);
|
|
157
|
-
if (translated.length > 0) translateIds[tblName] = translated;
|
|
63
|
+
for (const newRow of offlineRows.map((row) =>
|
|
64
|
+
pickFields(table, row)
|
|
65
|
+
)) {
|
|
66
|
+
if (aborted) throw new Error("connection closed by client");
|
|
67
|
+
await db.insert(table.name, newRow, { client: client });
|
|
158
68
|
}
|
|
159
69
|
}
|
|
160
70
|
}
|
|
71
|
+
if (aborted) throw new Error("connection closed by client");
|
|
161
72
|
await client.query("COMMIT");
|
|
162
|
-
|
|
163
|
-
res.json({ translateIds });
|
|
73
|
+
res.json({ success: true });
|
|
164
74
|
} catch (error) {
|
|
165
75
|
await client.query("ROLLBACK");
|
|
166
76
|
getState().log(2, `POST /sync/table_data error: '${error.message}'`);
|
|
167
|
-
res
|
|
77
|
+
res
|
|
78
|
+
.status(error.statusCode || 400)
|
|
79
|
+
.json({ error: error.message || error });
|
|
80
|
+
} finally {
|
|
81
|
+
if (!db.isSQLite) await client.release(true);
|
|
168
82
|
}
|
|
169
83
|
})
|
|
170
84
|
);
|
package/routes/tables.js
CHANGED
|
@@ -541,11 +541,7 @@ const attribBadges = (f) => {
|
|
|
541
541
|
let s = "";
|
|
542
542
|
if (f.attributes) {
|
|
543
543
|
Object.entries(f.attributes).forEach(([k, v]) => {
|
|
544
|
-
if (
|
|
545
|
-
["summary_field", "default", "on_delete_cascade", "on_delete"].includes(
|
|
546
|
-
k
|
|
547
|
-
)
|
|
548
|
-
)
|
|
544
|
+
if (["summary_field", "on_delete_cascade", "on_delete"].includes(k))
|
|
549
545
|
return;
|
|
550
546
|
if (v || v === 0) s += badge("secondary", k);
|
|
551
547
|
});
|
package/routes/viewedit.js
CHANGED
|
@@ -493,14 +493,22 @@ router.post(
|
|
|
493
493
|
* @param {object} res
|
|
494
494
|
* @returns {void}
|
|
495
495
|
*/
|
|
496
|
-
const respondWorkflow = (view, wf, wfres, req, res) => {
|
|
496
|
+
const respondWorkflow = (view, wf, wfres, req, res, table) => {
|
|
497
497
|
const wrap = (contents, noCard, previewURL) => ({
|
|
498
498
|
above: [
|
|
499
499
|
{
|
|
500
500
|
type: "breadcrumbs",
|
|
501
501
|
crumbs: [
|
|
502
502
|
{ text: req.__("Views"), href: "/viewedit" },
|
|
503
|
-
{
|
|
503
|
+
{
|
|
504
|
+
href: `/view/${view.name}`,
|
|
505
|
+
text: view.name,
|
|
506
|
+
postLinkText: `[${view.viewtemplate}${
|
|
507
|
+
table
|
|
508
|
+
? ` on ${a({ href: `/table/` + table.name }, table.name)}`
|
|
509
|
+
: ""
|
|
510
|
+
}]`,
|
|
511
|
+
},
|
|
504
512
|
{ workflow: wf, step: wfres },
|
|
505
513
|
],
|
|
506
514
|
},
|
|
@@ -584,6 +592,9 @@ router.get(
|
|
|
584
592
|
(view.configuration?.columns || []).forEach((c) => {
|
|
585
593
|
c._columndef = JSON.stringify(c);
|
|
586
594
|
});
|
|
595
|
+
let table;
|
|
596
|
+
if (view.table_id) table = Table.findOne({ id: view.table_id });
|
|
597
|
+
if (view.exttable_name) table = Table.findOne({ name: view.exttable_name });
|
|
587
598
|
const configFlow = await view.get_config_flow(req);
|
|
588
599
|
const hasConfig =
|
|
589
600
|
view.configuration && Object.keys(view.configuration).length > 0;
|
|
@@ -598,7 +609,7 @@ router.get(
|
|
|
598
609
|
},
|
|
599
610
|
req
|
|
600
611
|
);
|
|
601
|
-
respondWorkflow(view, configFlow, wfres, req, res);
|
|
612
|
+
respondWorkflow(view, configFlow, wfres, req, res, table);
|
|
602
613
|
})
|
|
603
614
|
);
|
|
604
615
|
|
|
@@ -617,7 +628,11 @@ router.post(
|
|
|
617
628
|
const view = await View.findOne({ name });
|
|
618
629
|
const configFlow = await view.get_config_flow(req);
|
|
619
630
|
const wfres = await configFlow.run(req.body, req);
|
|
620
|
-
|
|
631
|
+
|
|
632
|
+
let table;
|
|
633
|
+
if (view.table_id) table = Table.findOne({ id: view.table_id });
|
|
634
|
+
if (view.exttable_name) table = Table.findOne({ name: view.exttable_name });
|
|
635
|
+
respondWorkflow(view, configFlow, wfres, req, res, table);
|
|
621
636
|
})
|
|
622
637
|
);
|
|
623
638
|
|
package/tests/sync.test.js
CHANGED
|
@@ -2,9 +2,10 @@ const request = require("supertest");
|
|
|
2
2
|
const getApp = require("../app");
|
|
3
3
|
const {
|
|
4
4
|
getUserLoginCookie,
|
|
5
|
-
getStaffLoginCookie,
|
|
6
5
|
getAdminLoginCookie,
|
|
7
6
|
resetToFixtures,
|
|
7
|
+
notAuthorized,
|
|
8
|
+
respondJsonWith,
|
|
8
9
|
} = require("../auth/testhelp");
|
|
9
10
|
const db = require("@saltcorn/data/db");
|
|
10
11
|
|
|
@@ -15,42 +16,16 @@ beforeAll(async () => {
|
|
|
15
16
|
});
|
|
16
17
|
afterAll(db.close);
|
|
17
18
|
|
|
18
|
-
describe("Load offline data", () => {
|
|
19
|
-
it("public request", async () => {
|
|
20
|
-
const app = await getApp({ disableCsrf: true });
|
|
21
|
-
const resp = await request(app).get("/sync/table_data");
|
|
22
|
-
for (const [k, v] of Object.entries(resp._body)) {
|
|
23
|
-
expect(v.rows.length).toBe(k === "books" ? 2 : 0);
|
|
24
|
-
}
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it("user request", async () => {
|
|
28
|
-
const app = await getApp({ disableCsrf: true });
|
|
29
|
-
const loginCookie = await getUserLoginCookie();
|
|
30
|
-
const resp = await request(app)
|
|
31
|
-
.get("/sync/table_data")
|
|
32
|
-
.set("Cookie", loginCookie);
|
|
33
|
-
const data = resp._body;
|
|
34
|
-
expect(data.patients.rows.length).toBe(0);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it("admin request", async () => {
|
|
38
|
-
const app = await getApp({ disableCsrf: true });
|
|
39
|
-
const loginCookie = await getAdminLoginCookie();
|
|
40
|
-
const resp = await request(app)
|
|
41
|
-
.get("/sync/table_data")
|
|
42
|
-
.set("Cookie", loginCookie);
|
|
43
|
-
const data = resp._body;
|
|
44
|
-
expect(data.patients.rows.length).toBe(2);
|
|
45
|
-
});
|
|
46
|
-
});
|
|
47
|
-
|
|
48
19
|
describe("Synchronise with mobile offline data", () => {
|
|
49
|
-
|
|
50
|
-
|
|
20
|
+
it("not permitted", async () => {
|
|
21
|
+
if (!db.isSQLite) {
|
|
22
|
+
const patients = Table.findOne({ name: "patients" });
|
|
23
|
+
const books = Table.findOne({ name: "books" });
|
|
24
|
+
const patientsBefore = await patients.countRows();
|
|
25
|
+
const booksBefore = await books.countRows();
|
|
51
26
|
const app = await getApp({ disableCsrf: true });
|
|
52
27
|
const loginCookie = await getUserLoginCookie();
|
|
53
|
-
|
|
28
|
+
await request(app)
|
|
54
29
|
.post("/sync/table_data")
|
|
55
30
|
.set("Cookie", loginCookie)
|
|
56
31
|
.send({
|
|
@@ -68,26 +43,35 @@ describe("Synchronise with mobile offline data", () => {
|
|
|
68
43
|
parent: 1,
|
|
69
44
|
},
|
|
70
45
|
],
|
|
46
|
+
books: [
|
|
47
|
+
{
|
|
48
|
+
id: 3,
|
|
49
|
+
author: "foo",
|
|
50
|
+
pages: 20,
|
|
51
|
+
publisher: 1,
|
|
52
|
+
},
|
|
53
|
+
],
|
|
71
54
|
},
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
.set("Cookie", adminCookie);
|
|
81
|
-
const data = downloadResp._body;
|
|
82
|
-
expect(data.patients.rows.length).toBe(2);
|
|
83
|
-
});
|
|
55
|
+
})
|
|
56
|
+
.expect(notAuthorized);
|
|
57
|
+
const patientsAfter = await patients.countRows();
|
|
58
|
+
const booksAfter = await books.countRows();
|
|
59
|
+
expect(patientsAfter).toBe(patientsBefore);
|
|
60
|
+
expect(booksAfter).toBe(booksBefore);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
84
63
|
|
|
85
|
-
|
|
64
|
+
it("upload patients and books", async () => {
|
|
65
|
+
if (!db.isSQLite) {
|
|
66
|
+
const patients = Table.findOne({ name: "patients" });
|
|
67
|
+
const books = Table.findOne({ name: "books" });
|
|
68
|
+
const patientsBefore = await patients.countRows();
|
|
69
|
+
const booksBefore = await books.countRows();
|
|
86
70
|
const app = await getApp({ disableCsrf: true });
|
|
87
|
-
const
|
|
88
|
-
|
|
71
|
+
const loginCookie = await getAdminLoginCookie();
|
|
72
|
+
await request(app)
|
|
89
73
|
.post("/sync/table_data")
|
|
90
|
-
.set("Cookie",
|
|
74
|
+
.set("Cookie", loginCookie)
|
|
91
75
|
.send({
|
|
92
76
|
data: {
|
|
93
77
|
patients: [
|
|
@@ -97,7 +81,7 @@ describe("Synchronise with mobile offline data", () => {
|
|
|
97
81
|
parent: 1,
|
|
98
82
|
},
|
|
99
83
|
{
|
|
100
|
-
id: 84,
|
|
84
|
+
id: 84,
|
|
101
85
|
name: "Pitt Brad",
|
|
102
86
|
favbook: 2,
|
|
103
87
|
parent: 1,
|
|
@@ -105,83 +89,20 @@ describe("Synchronise with mobile offline data", () => {
|
|
|
105
89
|
],
|
|
106
90
|
books: [
|
|
107
91
|
{
|
|
108
|
-
id: 3,
|
|
92
|
+
id: 3,
|
|
109
93
|
author: "foo",
|
|
110
94
|
pages: 20,
|
|
111
95
|
publisher: 1,
|
|
112
96
|
},
|
|
113
97
|
],
|
|
114
98
|
},
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
expect(
|
|
120
|
-
expect(
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
.get("/sync/table_data")
|
|
125
|
-
.set("Cookie", staffCookie);
|
|
126
|
-
const data = downloadResp._body;
|
|
127
|
-
expect(data.patients.rows.length).toBe(3);
|
|
128
|
-
expect(data.books.rows.length).toBe(3);
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it("upload with ownership_field", async () => {
|
|
132
|
-
const messagesTbl = Table.findOne({ name: "messages" });
|
|
133
|
-
const userField = messagesTbl
|
|
134
|
-
.getFields()
|
|
135
|
-
.find((field) => field.name === "user");
|
|
136
|
-
await messagesTbl.update({
|
|
137
|
-
min_role_read: 1,
|
|
138
|
-
min_role_write: 1,
|
|
139
|
-
ownership_field_id: userField.id,
|
|
140
|
-
});
|
|
141
|
-
const staffMsgId = await db.insert("messages", {
|
|
142
|
-
content: "message from staff",
|
|
143
|
-
user: 2,
|
|
144
|
-
room: 1,
|
|
145
|
-
});
|
|
146
|
-
const userMsgId = await db.insert("messages", {
|
|
147
|
-
content: "message from user",
|
|
148
|
-
user: 3,
|
|
149
|
-
room: 1,
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
const app = await getApp({ disableCsrf: true });
|
|
153
|
-
const userCookie = await getUserLoginCookie();
|
|
154
|
-
const uploadResp = await request(app)
|
|
155
|
-
.post("/sync/table_data")
|
|
156
|
-
.set("Cookie", userCookie)
|
|
157
|
-
.send({
|
|
158
|
-
data: {
|
|
159
|
-
messages: [
|
|
160
|
-
{
|
|
161
|
-
id: staffMsgId, // will be skipped
|
|
162
|
-
user: 3,
|
|
163
|
-
room: 1,
|
|
164
|
-
content: "offline change",
|
|
165
|
-
},
|
|
166
|
-
{
|
|
167
|
-
id: userMsgId, // will be updated because user is the owner
|
|
168
|
-
user: 2,
|
|
169
|
-
room: 1,
|
|
170
|
-
content: "offline change",
|
|
171
|
-
},
|
|
172
|
-
],
|
|
173
|
-
},
|
|
174
|
-
});
|
|
175
|
-
const translateIds = uploadResp._body.translateIds;
|
|
176
|
-
expect(translateIds).toBeDefined();
|
|
177
|
-
expect(Object.keys(translateIds).length).toBe(0);
|
|
178
|
-
// load the admin data
|
|
179
|
-
const adminCookie = await getAdminLoginCookie();
|
|
180
|
-
const resp = await request(app)
|
|
181
|
-
.get("/sync/table_data")
|
|
182
|
-
.set("Cookie", adminCookie);
|
|
183
|
-
const data = resp._body;
|
|
184
|
-
expect(data.messages.rows.length).toBe(4);
|
|
185
|
-
});
|
|
186
|
-
}
|
|
99
|
+
})
|
|
100
|
+
.expect(respondJsonWith(200, ({ success }) => success));
|
|
101
|
+
const patientsAfter = await patients.countRows();
|
|
102
|
+
const booksAfter = await books.countRows();
|
|
103
|
+
expect(patientsAfter).toBe(patientsBefore + 2);
|
|
104
|
+
expect(booksAfter).toBe(booksBefore + 1);
|
|
105
|
+
expect((await patients.getRows({ id: 84 })).length).toBe(0);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
187
108
|
});
|