@saltcorn/server 0.8.8-beta.1 → 0.8.8-beta.3
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/load_plugins.js +8 -1
- package/locales/en.json +6 -1
- package/locales/ru.json +4 -1
- package/package.json +8 -8
- package/public/saltcorn.js +44 -1
- package/routes/admin.js +98 -2
- package/routes/fields.js +0 -8
- package/routes/scapi.js +1 -1
- package/routes/sync.js +293 -57
- package/routes/tables.js +19 -1
- package/tests/plugin_install.test.js +114 -0
- package/tests/plugins.test.js +2 -102
- package/tests/sync.test.js +451 -75
- package/tests/view.test.js +2 -0
- package/tests/viewedit.test.js +2 -0
- package/wrapper.js +6 -4
package/load_plugins.js
CHANGED
|
@@ -187,7 +187,14 @@ const loadAndSaveNewPlugin = async (plugin, force, noSignalOrDB) => {
|
|
|
187
187
|
if (!isRoot && !tenants_unsafe_plugins) {
|
|
188
188
|
if (plugin.source !== "npm") return;
|
|
189
189
|
//get allowed plugins
|
|
190
|
-
|
|
190
|
+
|
|
191
|
+
//refresh root store
|
|
192
|
+
await db.runWithTenant(
|
|
193
|
+
db.connectObj.default_schema,
|
|
194
|
+
async () => await Plugin.store_plugins_available()
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
const instore = getRootState().getConfig("available_plugins", []);
|
|
191
198
|
const safes = instore.filter((p) => !p.unsafe).map((p) => p.location);
|
|
192
199
|
if (!safes.includes(plugin.location)) return;
|
|
193
200
|
}
|
package/locales/en.json
CHANGED
|
@@ -1237,5 +1237,10 @@
|
|
|
1237
1237
|
"Formula for JavaScript object that will be added to state parameters": "Formula for JavaScript object that will be added to state parameters",
|
|
1238
1238
|
"Alignment": "Alignment",
|
|
1239
1239
|
"Click to edit?": "Click to edit?",
|
|
1240
|
-
"Format": "Format"
|
|
1240
|
+
"Format": "Format",
|
|
1241
|
+
"Table Synchronization": "Table Synchronization",
|
|
1242
|
+
"unsynched": "unsynched",
|
|
1243
|
+
"synched": "synched",
|
|
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"
|
|
1241
1246
|
}
|
package/locales/ru.json
CHANGED
|
@@ -1095,5 +1095,8 @@
|
|
|
1095
1095
|
"Backup settings": "Настройки резервного копироваия",
|
|
1096
1096
|
"Click to edit?": "Редактировать по клику?",
|
|
1097
1097
|
"Format": "Формат",
|
|
1098
|
-
"Page '%s' was loaded": "Страница '%s' была загружена"
|
|
1098
|
+
"Page '%s' was loaded": "Страница '%s' была загружена",
|
|
1099
|
+
"Table provider": "Провайдер данных",
|
|
1100
|
+
"Database table": "Таблица БД",
|
|
1101
|
+
"Disable on mobile": "Disable on mobile"
|
|
1099
1102
|
}
|
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.3",
|
|
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.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",
|
|
16
16
|
"@socket.io/cluster-adapter": "^0.2.1",
|
|
17
17
|
"@socket.io/sticky": "^1.0.1",
|
|
18
18
|
"adm-zip": "0.5.10",
|
package/public/saltcorn.js
CHANGED
|
@@ -282,6 +282,10 @@ function ensure_modal_exists_and_closed() {
|
|
|
282
282
|
</div>
|
|
283
283
|
</div>`);
|
|
284
284
|
} else if ($("#scmodal").hasClass("show")) {
|
|
285
|
+
// remove reload handler added by edit, for when we have popup link
|
|
286
|
+
// in autosave edit in popup
|
|
287
|
+
$("#scmodal").off("hidden.bs.modal");
|
|
288
|
+
|
|
285
289
|
close_saltcorn_modal();
|
|
286
290
|
}
|
|
287
291
|
}
|
|
@@ -319,7 +323,9 @@ function ajax_modal(url, opts = {}) {
|
|
|
319
323
|
if (title) $("#scmodal .modal-title").html(decodeURIComponent(title));
|
|
320
324
|
$("#scmodal .modal-body").html(res);
|
|
321
325
|
$("#scmodal").prop("data-modal-state", url);
|
|
322
|
-
new bootstrap.Modal($("#scmodal")
|
|
326
|
+
new bootstrap.Modal($("#scmodal"), {
|
|
327
|
+
focus: false,
|
|
328
|
+
}).show();
|
|
323
329
|
initialize_page();
|
|
324
330
|
(opts.onOpen || function () {})(res);
|
|
325
331
|
$("#scmodal").on("hidden.bs.modal", function (e) {
|
|
@@ -669,6 +675,9 @@ function build_mobile_app(button) {
|
|
|
669
675
|
form.serializeArray().forEach((item) => {
|
|
670
676
|
params[item.name] = item.value;
|
|
671
677
|
});
|
|
678
|
+
params.synchedTables = Array.from($("#synched-tbls-select-id")[0].options)
|
|
679
|
+
.filter((option) => !option.hidden)
|
|
680
|
+
.map((option) => option.value);
|
|
672
681
|
ajax_post("/admin/build-mobile-app", {
|
|
673
682
|
data: params,
|
|
674
683
|
success: (data) => {
|
|
@@ -682,6 +691,40 @@ function build_mobile_app(button) {
|
|
|
682
691
|
});
|
|
683
692
|
}
|
|
684
693
|
|
|
694
|
+
function move_to_synched() {
|
|
695
|
+
const opts = $("#unsynched-tbls-select-id");
|
|
696
|
+
$("#synched-tbls-select-id").removeAttr("selected");
|
|
697
|
+
for (const selected of opts.val()) {
|
|
698
|
+
const jUnsOpt = $(`[id='${selected}_unsynched_opt']`);
|
|
699
|
+
jUnsOpt.attr("hidden", "true");
|
|
700
|
+
jUnsOpt.removeAttr("selected");
|
|
701
|
+
const jSynOpt = $(`[id='${selected}_synched_opt']`);
|
|
702
|
+
jSynOpt.removeAttr("hidden");
|
|
703
|
+
jSynOpt.removeAttr("selected");
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function move_to_unsynched() {
|
|
708
|
+
const opts = $("#synched-tbls-select-id");
|
|
709
|
+
$("#unsynched-tbls-select-id").removeAttr("selected");
|
|
710
|
+
for (const selected of opts.val()) {
|
|
711
|
+
const jSynOpt = $(`[id='${selected}_synched_opt']`);
|
|
712
|
+
jSynOpt.attr("hidden", "true");
|
|
713
|
+
jSynOpt.removeAttr("selected");
|
|
714
|
+
const jUnsOpt = $(`[id='${selected}_unsynched_opt']`);
|
|
715
|
+
jUnsOpt.removeAttr("hidden");
|
|
716
|
+
jUnsOpt.removeAttr("selected");
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function toggle_tbl_sync() {
|
|
721
|
+
if ($("#offlineModeBoxId")[0].checked === true) {
|
|
722
|
+
$("#tblSyncSelectorId").attr("hidden", false);
|
|
723
|
+
} else {
|
|
724
|
+
$("#tblSyncSelectorId").attr("hidden", true);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
685
728
|
function join_field_clicked(e, fieldPath) {
|
|
686
729
|
$("#inputjoin_field").val(fieldPath);
|
|
687
730
|
apply_showif();
|
package/routes/admin.js
CHANGED
|
@@ -1491,7 +1491,7 @@ router.get(
|
|
|
1491
1491
|
const images = (await File.find({ mime_super: "image" })).filter((image) =>
|
|
1492
1492
|
image.filename?.endsWith(".png")
|
|
1493
1493
|
);
|
|
1494
|
-
|
|
1494
|
+
const withSyncInfo = await Table.find({ has_sync_info: true });
|
|
1495
1495
|
send_admin_page({
|
|
1496
1496
|
res,
|
|
1497
1497
|
req,
|
|
@@ -1752,7 +1752,6 @@ router.get(
|
|
|
1752
1752
|
),
|
|
1753
1753
|
|
|
1754
1754
|
div(
|
|
1755
|
-
// TODO only for some tables?
|
|
1756
1755
|
{ class: "row pb-2" },
|
|
1757
1756
|
div(
|
|
1758
1757
|
{ class: "col-sm-4" },
|
|
@@ -1761,6 +1760,8 @@ router.get(
|
|
|
1761
1760
|
id: "offlineModeBoxId",
|
|
1762
1761
|
class: "form-check-input me-2",
|
|
1763
1762
|
name: "allowOfflineMode",
|
|
1763
|
+
value: "allowOfflineMode",
|
|
1764
|
+
onClick: "toggle_tbl_sync()",
|
|
1764
1765
|
checked: true,
|
|
1765
1766
|
}),
|
|
1766
1767
|
label(
|
|
@@ -1771,6 +1772,98 @@ router.get(
|
|
|
1771
1772
|
req.__("Allow offline mode")
|
|
1772
1773
|
)
|
|
1773
1774
|
)
|
|
1775
|
+
),
|
|
1776
|
+
div(
|
|
1777
|
+
{
|
|
1778
|
+
id: "tblSyncSelectorId",
|
|
1779
|
+
class: "row pb-2",
|
|
1780
|
+
},
|
|
1781
|
+
div(
|
|
1782
|
+
label(
|
|
1783
|
+
{ class: "form-label fw-bold" },
|
|
1784
|
+
req.__("Table Synchronization")
|
|
1785
|
+
)
|
|
1786
|
+
),
|
|
1787
|
+
div(
|
|
1788
|
+
{ class: "container" },
|
|
1789
|
+
div(
|
|
1790
|
+
{ class: "row" },
|
|
1791
|
+
div(
|
|
1792
|
+
{ class: "col-sm-4 text-center" },
|
|
1793
|
+
req.__("unsynched")
|
|
1794
|
+
),
|
|
1795
|
+
div({ class: "col-sm-1" }),
|
|
1796
|
+
div(
|
|
1797
|
+
{ class: "col-sm-4 text-center" },
|
|
1798
|
+
req.__("synched")
|
|
1799
|
+
)
|
|
1800
|
+
),
|
|
1801
|
+
div(
|
|
1802
|
+
{ class: "row" },
|
|
1803
|
+
div(
|
|
1804
|
+
{ class: "col-sm-4" },
|
|
1805
|
+
select(
|
|
1806
|
+
{
|
|
1807
|
+
id: "unsynched-tbls-select-id",
|
|
1808
|
+
class: "form-control form-select",
|
|
1809
|
+
multiple: true,
|
|
1810
|
+
},
|
|
1811
|
+
withSyncInfo.map((table) =>
|
|
1812
|
+
option({
|
|
1813
|
+
id: `${table.name}_unsynched_opt`,
|
|
1814
|
+
value: table.name,
|
|
1815
|
+
label: table.name,
|
|
1816
|
+
})
|
|
1817
|
+
)
|
|
1818
|
+
)
|
|
1819
|
+
),
|
|
1820
|
+
div(
|
|
1821
|
+
{ class: "col-sm-1 d-flex justify-content-center" },
|
|
1822
|
+
div(
|
|
1823
|
+
div(
|
|
1824
|
+
button(
|
|
1825
|
+
{
|
|
1826
|
+
id: "move-right-btn-id",
|
|
1827
|
+
type: "button",
|
|
1828
|
+
onClick: `move_to_synched()`,
|
|
1829
|
+
class: "btn btn-light pt-1 mb-1",
|
|
1830
|
+
},
|
|
1831
|
+
i({ class: "fas fa-arrow-right" })
|
|
1832
|
+
)
|
|
1833
|
+
),
|
|
1834
|
+
div(
|
|
1835
|
+
button(
|
|
1836
|
+
{
|
|
1837
|
+
id: "move-left-btn-id",
|
|
1838
|
+
type: "button",
|
|
1839
|
+
onClick: `move_to_unsynched()`,
|
|
1840
|
+
class: "btn btn-light pt-1",
|
|
1841
|
+
},
|
|
1842
|
+
i({ class: "fas fa-arrow-left" })
|
|
1843
|
+
)
|
|
1844
|
+
)
|
|
1845
|
+
)
|
|
1846
|
+
),
|
|
1847
|
+
div(
|
|
1848
|
+
{ class: "col-sm-4" },
|
|
1849
|
+
select(
|
|
1850
|
+
{
|
|
1851
|
+
id: "synched-tbls-select-id",
|
|
1852
|
+
class: "form-control form-select",
|
|
1853
|
+
multiple: true,
|
|
1854
|
+
},
|
|
1855
|
+
withSyncInfo.map((table) =>
|
|
1856
|
+
option({
|
|
1857
|
+
id: `${table.name}_synched_opt`,
|
|
1858
|
+
value: table.name,
|
|
1859
|
+
label: table.name,
|
|
1860
|
+
hidden: "true",
|
|
1861
|
+
})
|
|
1862
|
+
)
|
|
1863
|
+
)
|
|
1864
|
+
)
|
|
1865
|
+
)
|
|
1866
|
+
)
|
|
1774
1867
|
)
|
|
1775
1868
|
),
|
|
1776
1869
|
button(
|
|
@@ -1877,6 +1970,7 @@ router.post(
|
|
|
1877
1970
|
serverURL,
|
|
1878
1971
|
splashPage,
|
|
1879
1972
|
allowOfflineMode,
|
|
1973
|
+
synchedTables,
|
|
1880
1974
|
} = req.body;
|
|
1881
1975
|
if (!androidPlatform && !iOSPlatform) {
|
|
1882
1976
|
return res.json({
|
|
@@ -1928,6 +2022,8 @@ router.post(
|
|
|
1928
2022
|
if (serverURL) spawnParams.push("-s", serverURL);
|
|
1929
2023
|
if (splashPage) spawnParams.push("--splashPage", splashPage);
|
|
1930
2024
|
if (allowOfflineMode) spawnParams.push("--allowOfflineMode");
|
|
2025
|
+
if (synchedTables?.length > 0)
|
|
2026
|
+
spawnParams.push("--synchedTables", ...synchedTables.map((tbl) => tbl));
|
|
1931
2027
|
if (
|
|
1932
2028
|
db.is_it_multi_tenant() &&
|
|
1933
2029
|
db.getTenantSchema() !== db.connectObj.default_schema
|
package/routes/fields.js
CHANGED
|
@@ -542,14 +542,6 @@ const fieldFlow = (req) =>
|
|
|
542
542
|
type: "Bool",
|
|
543
543
|
showIf: { summary_field: textfields },
|
|
544
544
|
}),
|
|
545
|
-
/*new Field({
|
|
546
|
-
name: "on_delete_cascade",
|
|
547
|
-
label: req.__("On delete cascade"),
|
|
548
|
-
type: "Bool",
|
|
549
|
-
sublabel: req.__(
|
|
550
|
-
"If the parent row is deleted, automatically delete the child rows."
|
|
551
|
-
),
|
|
552
|
-
}),*/
|
|
553
545
|
new Field({
|
|
554
546
|
name: "on_delete",
|
|
555
547
|
label: req.__("On delete"),
|
package/routes/scapi.js
CHANGED
|
@@ -101,7 +101,7 @@ router.get(
|
|
|
101
101
|
{ session: false },
|
|
102
102
|
async function (err, user, info) {
|
|
103
103
|
if (accessAllowedRead(req, user)) {
|
|
104
|
-
const views = await View.find({});
|
|
104
|
+
const views = await View.find({}, { cached: true });
|
|
105
105
|
|
|
106
106
|
res.json({ success: views });
|
|
107
107
|
} else {
|
package/routes/sync.js
CHANGED
|
@@ -3,83 +3,319 @@ const Router = require("express-promise-router");
|
|
|
3
3
|
const db = require("@saltcorn/data/db");
|
|
4
4
|
const { getState } = require("@saltcorn/data/db/state");
|
|
5
5
|
const Table = require("@saltcorn/data/models/table");
|
|
6
|
+
const File = require("@saltcorn/data/models/file");
|
|
7
|
+
const { getSafeSaltcornCmd } = require("@saltcorn/data/utils");
|
|
8
|
+
const { spawn, spawnSync } = require("child_process");
|
|
9
|
+
const path = require("path");
|
|
10
|
+
const fs = require("fs").promises;
|
|
6
11
|
|
|
7
12
|
const router = new Router();
|
|
8
13
|
module.exports = router;
|
|
9
14
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
router.get(
|
|
16
|
+
"/sync_timestamp",
|
|
17
|
+
error_catcher(async (req, res) => {
|
|
18
|
+
try {
|
|
19
|
+
res.json({ syncTimestamp: (await db.time()).valueOf() });
|
|
20
|
+
} catch (error) {
|
|
21
|
+
getState().log(2, `GET /sync_timestamp: '${error.message}'`);
|
|
22
|
+
res.status(400).json({ error: error.message || error });
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
const getSyncRows = async (syncInfo, table, syncUntil, client) => {
|
|
28
|
+
const tblName = table.name;
|
|
29
|
+
const pkName = table.pk_name;
|
|
30
|
+
const schema = db.getTenantSchemaPrefix();
|
|
31
|
+
if (!syncInfo.syncFrom) {
|
|
32
|
+
const { rows } = await client.query(
|
|
33
|
+
`select
|
|
34
|
+
info_tbl.ref "_sync_info_tbl_ref_",
|
|
35
|
+
info_tbl.last_modified "_sync_info_tbl_last_modified_",
|
|
36
|
+
info_tbl.deleted "_sync_info_tbl_deleted_",
|
|
37
|
+
data_tbl.*
|
|
38
|
+
from ${schema}"${db.sqlsanitize(
|
|
39
|
+
tblName
|
|
40
|
+
)}_sync_info" "info_tbl" right join "${db.sqlsanitize(
|
|
41
|
+
tblName
|
|
42
|
+
)}" "data_tbl"
|
|
43
|
+
on info_tbl.ref = data_tbl."${db.sqlsanitize(
|
|
44
|
+
pkName
|
|
45
|
+
)}" and info_tbl.deleted = false
|
|
46
|
+
where data_tbl."${db.sqlsanitize(pkName)}" > ${
|
|
47
|
+
syncInfo.maxLoadedId
|
|
48
|
+
} order by data_tbl."${db.sqlsanitize(pkName)}"`
|
|
49
|
+
);
|
|
50
|
+
for (const row of rows) {
|
|
51
|
+
if (row._sync_info_tbl_last_modified_)
|
|
52
|
+
row._sync_info_tbl_last_modified_ =
|
|
53
|
+
row._sync_info_tbl_last_modified_.valueOf();
|
|
54
|
+
else row._sync_info_tbl_last_modified_ = new Date(syncUntil).valueOf();
|
|
55
|
+
row._sync_info_tbl_ref_ = row[pkName];
|
|
19
56
|
}
|
|
57
|
+
return rows;
|
|
58
|
+
} else {
|
|
59
|
+
const { rows } = await client.query(
|
|
60
|
+
`select
|
|
61
|
+
info_tbl.ref "_sync_info_tbl_ref_",
|
|
62
|
+
info_tbl.last_modified "_sync_info_tbl_last_modified_",
|
|
63
|
+
info_tbl.deleted "_sync_info_tbl_deleted_",
|
|
64
|
+
data_tbl.*
|
|
65
|
+
from ${schema}"${db.sqlsanitize(
|
|
66
|
+
tblName
|
|
67
|
+
)}_sync_info" "info_tbl" join ${schema}"${db.sqlsanitize(
|
|
68
|
+
tblName
|
|
69
|
+
)}" "data_tbl"
|
|
70
|
+
on info_tbl.ref = data_tbl."${db.sqlsanitize(pkName)}"
|
|
71
|
+
where date_trunc('milliseconds', info_tbl.last_modified) > to_timestamp(${
|
|
72
|
+
new Date(syncInfo.syncFrom).valueOf() / 1000.0
|
|
73
|
+
})
|
|
74
|
+
and date_trunc('milliseconds', info_tbl.last_modified) < to_timestamp(${
|
|
75
|
+
new Date(syncUntil).valueOf() / 1000.0
|
|
76
|
+
})
|
|
77
|
+
and info_tbl.deleted = false
|
|
78
|
+
and info_tbl.ref > ${syncInfo.maxLoadedId}
|
|
79
|
+
order by info_tbl.ref`
|
|
80
|
+
);
|
|
81
|
+
for (const row of rows) {
|
|
82
|
+
if (row._sync_info_tbl_last_modified_)
|
|
83
|
+
row._sync_info_tbl_last_modified_ =
|
|
84
|
+
row._sync_info_tbl_last_modified_.valueOf();
|
|
85
|
+
else row._sync_info_tbl_last_modified_ = syncUntil.valueOf();
|
|
86
|
+
}
|
|
87
|
+
return rows;
|
|
20
88
|
}
|
|
21
|
-
return result;
|
|
22
89
|
};
|
|
23
90
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
91
|
+
/*
|
|
92
|
+
load inserts/updates after syncFrom
|
|
93
|
+
If a table has no syncFrom then it's the first sync and we have to send everything
|
|
94
|
+
*/
|
|
95
|
+
router.post(
|
|
96
|
+
"/load_changes",
|
|
97
|
+
error_catcher(async (req, res) => {
|
|
98
|
+
const result = {};
|
|
99
|
+
const { syncInfos, loadUntil } = req.body;
|
|
100
|
+
if (!loadUntil) {
|
|
101
|
+
getState().log(2, `POST /load_changes: loadUntil is missing`);
|
|
102
|
+
return res.status(400).json({ error: "loadUntil is missing" });
|
|
103
|
+
}
|
|
104
|
+
if (!syncInfos) {
|
|
105
|
+
getState().log(2, `POST /load_changes: syncInfos is missing`);
|
|
106
|
+
return res.status(400).json({ error: "syncInfos is missing" });
|
|
107
|
+
}
|
|
108
|
+
const role = req.user ? req.user.role_id : 100;
|
|
109
|
+
const client = await db.getClient();
|
|
110
|
+
let rowLimit = 1000;
|
|
111
|
+
try {
|
|
112
|
+
await client.query(`BEGIN`);
|
|
113
|
+
for (const [tblName, syncInfo] of Object.entries(syncInfos)) {
|
|
114
|
+
const table = Table.findOne({ name: tblName });
|
|
115
|
+
if (!table) throw new Error(`The table '${tblName}' does not exists`);
|
|
116
|
+
const pkName = table.pk_name;
|
|
117
|
+
let rows = await getSyncRows(syncInfo, table, loadUntil, client);
|
|
118
|
+
if (role > table.min_role_read) {
|
|
119
|
+
if (
|
|
120
|
+
role === 100 ||
|
|
121
|
+
(!table.ownership_field_id && !table.ownership_formula)
|
|
122
|
+
)
|
|
123
|
+
continue;
|
|
124
|
+
else if (table.ownership_field_id) {
|
|
125
|
+
} else if (table.ownership_formula) {
|
|
126
|
+
rows = rows.filter((row) => table.is_owner(req.user, row));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (rows.length > rowLimit) {
|
|
130
|
+
rows.splice(rowLimit);
|
|
131
|
+
}
|
|
132
|
+
rowLimit -= rows.length;
|
|
133
|
+
result[tblName] = {
|
|
134
|
+
rows,
|
|
135
|
+
maxLoadedId: rows.length > 0 ? rows[rows.length - 1][pkName] : 0,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
await client.query("COMMIT");
|
|
139
|
+
res.json(result);
|
|
140
|
+
} catch (error) {
|
|
141
|
+
await client.query("ROLLBACK");
|
|
142
|
+
getState().log(2, `POST /load_changes: '${error.message}'`);
|
|
143
|
+
res.status(400).json({ error: error.message || error });
|
|
144
|
+
} finally {
|
|
145
|
+
client.release(true);
|
|
146
|
+
}
|
|
147
|
+
})
|
|
148
|
+
);
|
|
28
149
|
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
150
|
+
const getDelRows = async (tblName, syncFrom, syncUntil, client) => {
|
|
151
|
+
const schema = db.getTenantSchemaPrefix();
|
|
152
|
+
const dbRes = await client.query(
|
|
153
|
+
`select *
|
|
154
|
+
from (
|
|
155
|
+
select ref, max(last_modified) from ${schema}"${db.sqlsanitize(
|
|
156
|
+
tblName
|
|
157
|
+
)}_sync_info"
|
|
158
|
+
group by ref, deleted having deleted = true) as alias
|
|
159
|
+
where alias.max < to_timestamp(${syncUntil.valueOf() / 1000.0})
|
|
160
|
+
and alias.max > to_timestamp(${syncFrom.valueOf() / 1000.0})`
|
|
161
|
+
);
|
|
162
|
+
for (const row of dbRes.rows) {
|
|
163
|
+
if (row.last_modified) row.last_modified = row.last_modified.valueOf();
|
|
164
|
+
if (row.max) row.max = row.max.valueOf();
|
|
165
|
+
}
|
|
166
|
+
return dbRes.rows;
|
|
33
167
|
};
|
|
34
168
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
169
|
+
/*
|
|
170
|
+
load deletes after syncFrom
|
|
171
|
+
If a table has no syncFrom then it's the first sync and there is nothing to delete
|
|
172
|
+
*/
|
|
38
173
|
router.post(
|
|
39
|
-
"/
|
|
174
|
+
"/deletes",
|
|
40
175
|
error_catcher(async (req, res) => {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
4,
|
|
44
|
-
`POST /sync/table_data user: '${req.user ? req.user.id : "public"}'`
|
|
45
|
-
);
|
|
46
|
-
let aborted = false;
|
|
47
|
-
req.socket.on("close", () => {
|
|
48
|
-
aborted = true;
|
|
49
|
-
});
|
|
50
|
-
req.socket.on("timeout", () => {
|
|
51
|
-
aborted = true;
|
|
52
|
-
});
|
|
53
|
-
const client = db.isSQLite ? db : await db.getClient();
|
|
176
|
+
const { syncInfos, syncTimestamp } = req.body;
|
|
177
|
+
const client = await db.getClient();
|
|
54
178
|
try {
|
|
55
|
-
await client.query(
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
await db.insert(table.name, newRow, { client: client });
|
|
69
|
-
}
|
|
179
|
+
await client.query(`BEGIN`);
|
|
180
|
+
const syncUntil = new Date(syncTimestamp);
|
|
181
|
+
const result = {
|
|
182
|
+
deletes: {},
|
|
183
|
+
};
|
|
184
|
+
for (const [tblName, syncInfo] of Object.entries(syncInfos)) {
|
|
185
|
+
if (syncInfo.syncFrom) {
|
|
186
|
+
result.deletes[tblName] = await getDelRows(
|
|
187
|
+
tblName,
|
|
188
|
+
new Date(syncInfo.syncFrom),
|
|
189
|
+
syncUntil,
|
|
190
|
+
client
|
|
191
|
+
);
|
|
70
192
|
}
|
|
71
193
|
}
|
|
72
|
-
if (aborted) throw new Error("connection closed by client");
|
|
73
194
|
await client.query("COMMIT");
|
|
74
|
-
res.json(
|
|
195
|
+
res.json(result);
|
|
75
196
|
} catch (error) {
|
|
76
197
|
await client.query("ROLLBACK");
|
|
77
|
-
getState().log(2, `POST /sync/
|
|
78
|
-
res
|
|
79
|
-
.status(error.statusCode || 400)
|
|
80
|
-
.json({ error: error.message || error });
|
|
198
|
+
getState().log(2, `POST /sync/deletes: '${error.message}'`);
|
|
199
|
+
res.status(400).json({ error: error.message || error });
|
|
81
200
|
} finally {
|
|
82
|
-
|
|
201
|
+
client.release(true);
|
|
202
|
+
}
|
|
203
|
+
})
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
/*
|
|
207
|
+
insert the app offline data
|
|
208
|
+
*/
|
|
209
|
+
router.post(
|
|
210
|
+
"/offline_changes",
|
|
211
|
+
error_catcher(async (req, res) => {
|
|
212
|
+
const { changes, syncTimestamp } = req.body;
|
|
213
|
+
const rootFolder = await File.rootFolder();
|
|
214
|
+
try {
|
|
215
|
+
const syncDirName = `${syncTimestamp}_${req.user?.email || "public"}`;
|
|
216
|
+
const syncDir = path.join(
|
|
217
|
+
rootFolder.location,
|
|
218
|
+
"mobile_app",
|
|
219
|
+
"sync",
|
|
220
|
+
syncDirName
|
|
221
|
+
);
|
|
222
|
+
await fs.mkdir(syncDir, { recursive: true });
|
|
223
|
+
await fs.writeFile(
|
|
224
|
+
path.join(syncDir, "changes.json"),
|
|
225
|
+
JSON.stringify(changes)
|
|
226
|
+
);
|
|
227
|
+
const spawnParams = ["sync-upload-data"];
|
|
228
|
+
if (req.user?.email) spawnParams.push("--userEmail", req.user.email);
|
|
229
|
+
spawnParams.push("--directory", syncDir);
|
|
230
|
+
if (
|
|
231
|
+
db.is_it_multi_tenant() &&
|
|
232
|
+
db.getTenantSchema() !== db.connectObj.default_schema
|
|
233
|
+
) {
|
|
234
|
+
spawnParams.push("--tenantAppName", db.getTenantSchema());
|
|
235
|
+
}
|
|
236
|
+
spawnParams.push("--syncTimestamp", syncTimestamp);
|
|
237
|
+
|
|
238
|
+
res.json({ syncDir: syncDirName });
|
|
239
|
+
const child = spawn(getSafeSaltcornCmd(), spawnParams, {
|
|
240
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
241
|
+
cwd: ".",
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
child.on("exit", async (exitCode, signal) => {
|
|
245
|
+
getState().log(
|
|
246
|
+
5,
|
|
247
|
+
`POST /sync/offline_changes: upload offline data finished with code: ${exitCode}`
|
|
248
|
+
);
|
|
249
|
+
});
|
|
250
|
+
child.on("error", (msg) => {
|
|
251
|
+
const message = msg.message ? msg.message : msg.code;
|
|
252
|
+
getState().log(
|
|
253
|
+
5,
|
|
254
|
+
`POST /sync/offline_changes: upload offline data failed: ${message}`
|
|
255
|
+
);
|
|
256
|
+
});
|
|
257
|
+
} catch (error) {
|
|
258
|
+
getState().log(2, `POST /sync/offline_changes: '${error.message}'`);
|
|
259
|
+
res.status(400).json({ error: error.message || error });
|
|
260
|
+
}
|
|
261
|
+
})
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
router.get(
|
|
265
|
+
"/upload_finished",
|
|
266
|
+
error_catcher(async (req, res) => {
|
|
267
|
+
const { dir_name } = req.query;
|
|
268
|
+
try {
|
|
269
|
+
const rootFolder = await File.rootFolder();
|
|
270
|
+
const syncDir = path.join(
|
|
271
|
+
rootFolder.location,
|
|
272
|
+
"mobile_app",
|
|
273
|
+
"sync",
|
|
274
|
+
dir_name
|
|
275
|
+
);
|
|
276
|
+
let entries = null;
|
|
277
|
+
try {
|
|
278
|
+
entries = await fs.readdir(syncDir);
|
|
279
|
+
} catch (error) {
|
|
280
|
+
return res.json({ finished: false });
|
|
281
|
+
}
|
|
282
|
+
if (entries.indexOf("translated-ids.json") >= 0) {
|
|
283
|
+
const translatedIds = JSON.parse(
|
|
284
|
+
await fs.readFile(path.join(syncDir, "translated-ids.json"))
|
|
285
|
+
);
|
|
286
|
+
res.json({ finished: true, translatedIds });
|
|
287
|
+
} else if (entries.indexOf("error.json") >= 0) {
|
|
288
|
+
const error = JSON.parse(
|
|
289
|
+
await fs.readFile(path.join(syncDir, "error.json"))
|
|
290
|
+
);
|
|
291
|
+
res.json({ finished: true, error });
|
|
292
|
+
} else {
|
|
293
|
+
res.json({ finished: false });
|
|
294
|
+
}
|
|
295
|
+
} catch (error) {
|
|
296
|
+
getState().log(2, `GET /sync/upload_finished: '${error.message}'`);
|
|
297
|
+
res.status(400).json({ error: error.message || error });
|
|
298
|
+
}
|
|
299
|
+
})
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
router.post(
|
|
303
|
+
"/clean_sync_dir",
|
|
304
|
+
error_catcher(async (req, res) => {
|
|
305
|
+
const { dir_name } = req.body;
|
|
306
|
+
try {
|
|
307
|
+
const rootFolder = await File.rootFolder();
|
|
308
|
+
const syncDir = path.join(
|
|
309
|
+
rootFolder.location,
|
|
310
|
+
"mobile_app",
|
|
311
|
+
"sync",
|
|
312
|
+
dir_name
|
|
313
|
+
);
|
|
314
|
+
await fs.rm(syncDir, { recursive: true, force: true });
|
|
315
|
+
res.status(200).send("");
|
|
316
|
+
} catch (error) {
|
|
317
|
+
getState().log(2, `POST /sync/clean_sync_dir: '${error.message}'`);
|
|
318
|
+
res.status(400).json({ error: error.message || error });
|
|
83
319
|
}
|
|
84
320
|
})
|
|
85
321
|
);
|