@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 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
- const instore = await Plugin.store_plugins_available();
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.1",
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.1",
10
- "@saltcorn/builder": "0.8.8-beta.1",
11
- "@saltcorn/data": "0.8.8-beta.1",
12
- "@saltcorn/admin-models": "0.8.8-beta.1",
13
- "@saltcorn/filemanager": "0.8.8-beta.1",
14
- "@saltcorn/markup": "0.8.8-beta.1",
15
- "@saltcorn/sbadmin2": "0.8.8-beta.1",
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",
@@ -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")).show();
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
- const pickFields = (table, row) => {
11
- const result = {};
12
- const fields = table.getFields();
13
- for (const { name, type, calculated } of table.getFields()) {
14
- if (name === "id" || calculated) continue;
15
- if (type?.name === "Date") {
16
- result[name] = row[name] ? new Date(row[name]) : undefined;
17
- } else {
18
- result[name] = row[name];
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
- const allowInsert = (table, user) => {
25
- const role = user?.role_id || 100;
26
- return table.min_role_write >= role;
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 throwWithCode = (message, code) => {
30
- const err = new Error(message);
31
- err.statusCode = code;
32
- throw err;
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
- * insert the offline data uploaded by the mobile-app
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
- "/table_data",
174
+ "/deletes",
40
175
  error_catcher(async (req, res) => {
41
- // TODO sqlite
42
- getState().log(
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("BEGIN");
56
- await client.query("SET CONSTRAINTS ALL DEFERRED");
57
- for (const [tblName, offlineRows] of Object.entries(req.body.data) ||
58
- []) {
59
- const table = Table.findOne({ name: tblName });
60
- if (!table) throw new Error(`The table '${tblName}' does not exist.`);
61
- if (!allowInsert(table, req.user))
62
- throwWithCode(req.__("Not authorized"), 401);
63
- if (tblName !== "users") {
64
- for (const newRow of offlineRows.map((row) =>
65
- pickFields(table, row)
66
- )) {
67
- if (aborted) throw new Error("connection closed by client");
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({ success: true });
195
+ res.json(result);
75
196
  } catch (error) {
76
197
  await client.query("ROLLBACK");
77
- getState().log(2, `POST /sync/table_data error: '${error.message}'`);
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
- if (!db.isSQLite) await client.release(true);
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
  );