@saltcorn/server 0.7.4-beta.3 → 0.8.0-beta.0

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/routes/admin.js CHANGED
@@ -97,13 +97,6 @@ const { getConfigFile } = require("@saltcorn/data/db/connect");
97
97
  const os = require("os");
98
98
  const Page = require("@saltcorn/data/models/page");
99
99
 
100
- /**
101
- * @type {object}
102
- * @const
103
- * @namespace routes/adminRouter
104
- * @category server
105
- * @subcategory routes
106
- */
107
100
  const router = new Router();
108
101
  module.exports = router;
109
102
 
@@ -123,9 +116,6 @@ const site_id_form = (req) =>
123
116
  "base_url",
124
117
  "page_custom_css",
125
118
  "page_custom_html",
126
- "development_mode",
127
- "log_sql",
128
- "log_level",
129
119
  "plugins_store_endpoint",
130
120
  "packs_store_endpoint",
131
121
  ...(getConfigFile() ? ["multitenancy_enabled"] : []),
@@ -139,7 +129,7 @@ const site_id_form = (req) =>
139
129
  * @returns {Promise<Form>} form
140
130
  */
141
131
  const email_form = async (req) => {
142
- const form = await config_fields_form({
132
+ return await config_fields_form({
143
133
  req,
144
134
  field_names: [
145
135
  "smtp_host",
@@ -151,10 +141,9 @@ const email_form = async (req) => {
151
141
  ],
152
142
  action: "/admin/email",
153
143
  });
154
- return form;
155
144
  };
156
145
 
157
- const app_files_table = (files, req) =>
146
+ const app_files_table = (files, buildDirName, req) =>
158
147
  mkTable(
159
148
  [
160
149
  {
@@ -163,9 +152,21 @@ const app_files_table = (files, req) =>
163
152
  },
164
153
  { label: req.__("Size (KiB)"), key: "size_kb", align: "right" },
165
154
  { label: req.__("Media type"), key: (r) => r.mimetype },
155
+ {
156
+ label: req.__("Open"),
157
+ key: (r) =>
158
+ link(
159
+ `/files/serve/mobile_app/${buildDirName}/${r.filename}`,
160
+ req.__("Open")
161
+ ),
162
+ },
166
163
  {
167
164
  label: req.__("Download"),
168
- key: (r) => link(`/files/download/${r.id}`, req.__("Download")),
165
+ key: (r) =>
166
+ link(
167
+ `/files/download/mobile_app/${buildDirName}/${r.filename}`,
168
+ req.__("Download")
169
+ ),
169
170
  },
170
171
  ],
171
172
  files
@@ -476,10 +477,10 @@ router.get(
476
477
  li(
477
478
  a({ href: "/admin/clear-all" }, req.__("Clear this application")),
478
479
  " ",
479
- req.__("(tick all boxes)")
480
+ req.__("(tick all boxes)")
480
481
  ),
481
482
  li(
482
- req.__("When prompted to create the first user, click the link to restore a backup")
483
+ req.__("When prompted to create the first user, click the link to restore a backup")
483
484
  ),
484
485
  li(req.__("Select the downloaded backup file"))
485
486
  )
@@ -734,6 +735,7 @@ router.post(
734
735
  await auto_backup_now();
735
736
  req.flash("success", req.__("Backup successful"));
736
737
  } catch (e) {
738
+ getState().log(1, e);
737
739
  req.flash("error", e.message);
738
740
  }
739
741
  res.json({ reload_page: true });
@@ -873,12 +875,40 @@ router.get(
873
875
  ),
874
876
  tr(th(req.__("Node.js version")), td(process.version)),
875
877
  tr(
876
- th(req.__("Database")),
878
+ th(req.__("Database type")),
877
879
  td(db.isSQLite ? "SQLite " : "PostgreSQL ", dbversion)
878
880
  ),
881
+ (isRoot?
882
+ tr(
883
+ th(req.__("Database host")),
884
+ td(db.connectObj.host)
885
+ )
886
+ : ""),
887
+ (isRoot?
888
+ tr(
889
+ th(req.__("Database port")),
890
+ td(db.connectObj.port)
891
+ )
892
+ : ""),
893
+ (isRoot?
894
+ tr(
895
+ th(req.__("Database name")),
896
+ td(db.connectObj.database)
897
+ )
898
+ : ""),
899
+ (isRoot?
900
+ tr(
901
+ th(req.__("Database user")),
902
+ td(db.connectObj.user)
903
+ )
904
+ : ""),
879
905
  tr(
880
- th(req.__("Process uptime")),
881
- td(moment(get_process_init_time()).fromNow(true))
906
+ th(req.__("Database schema")),
907
+ td(db.getTenantSchema())
908
+ ),
909
+ tr(
910
+ th(req.__("Process uptime")),
911
+ td(moment(get_process_init_time()).fromNow(true))
882
912
  )
883
913
  )
884
914
  ),
@@ -980,7 +1010,7 @@ router.post(
980
1010
  res.attachment(fileName);
981
1011
  const file = fs.createReadStream(fileName);
982
1012
  file.on("end", function () {
983
- fs.unlink(fileName, function () { });
1013
+ fs.unlink(fileName, function () {});
984
1014
  });
985
1015
  file.pipe(res);
986
1016
  })
@@ -1003,7 +1033,7 @@ router.post(
1003
1033
  );
1004
1034
  if (err) req.flash("error", err);
1005
1035
  else req.flash("success", req.__("Successfully restored backup"));
1006
- fs.unlink(newPath, function () { });
1036
+ fs.unlink(newPath, function () {});
1007
1037
  res.redirect(`/admin`);
1008
1038
  })
1009
1039
  );
@@ -1198,12 +1228,17 @@ router.get(
1198
1228
  "/configuration-check",
1199
1229
  isAdmin,
1200
1230
  error_catcher(async (req, res) => {
1201
- const { passes, errors, pass } = await runConfigurationCheck(req);
1231
+ const { passes, errors, pass, warnings } = await runConfigurationCheck(req);
1202
1232
  const mkError = (err) =>
1203
1233
  div(
1204
1234
  { class: "alert alert-danger", role: "alert" },
1205
1235
  pre({ class: "mb-0" }, code(err))
1206
1236
  );
1237
+ const mkWarning = (err) =>
1238
+ div(
1239
+ { class: "alert alert-warning", role: "alert" },
1240
+ pre({ class: "mb-0" }, code(err))
1241
+ );
1207
1242
  res.sendWrap(req.__(`Admin`), {
1208
1243
  above: [
1209
1244
  {
@@ -1227,7 +1262,8 @@ router.get(
1227
1262
  req.__("No errors detected during configuration check")
1228
1263
  )
1229
1264
  )
1230
- : errors.map(mkError)
1265
+ : errors.map(mkError),
1266
+ (warnings || []).map(mkWarning)
1231
1267
  ),
1232
1268
  },
1233
1269
  {
@@ -1283,7 +1319,9 @@ const buildDialogScript = () => {
1283
1319
  }
1284
1320
  </script>`;
1285
1321
  };
1286
-
1322
+ /**
1323
+ * Build mobile app
1324
+ */
1287
1325
  router.get(
1288
1326
  "/build-mobile-app",
1289
1327
  isAdmin,
@@ -1476,8 +1514,9 @@ router.get(
1476
1514
  ),
1477
1515
  button(
1478
1516
  {
1479
- type: "submit",
1480
- onClick: `handleMessages(); press_store_button(this);`,
1517
+ id: "buildMobileAppBtnId",
1518
+ type: "button",
1519
+ onClick: `build_mobile_app(this);`,
1481
1520
  class: "btn btn-warning",
1482
1521
  },
1483
1522
  i({ class: "fas fa-hammer pe-2" }),
@@ -1493,6 +1532,57 @@ router.get(
1493
1532
  })
1494
1533
  );
1495
1534
 
1535
+ const checkFiles = async (outDir, fileNames) => {
1536
+ const rootFolder = await File.rootFolder();
1537
+ const mobile_app_dir = path.join(rootFolder.location, "mobile_app", outDir);
1538
+ const entries = fs.readdirSync(mobile_app_dir);
1539
+ return fileNames.some((fileName) => entries.indexOf(fileName) >= 0);
1540
+ };
1541
+
1542
+ // check if a build has finished (poll service)
1543
+ router.get(
1544
+ "/build-mobile-app/finished",
1545
+ isAdmin,
1546
+ error_catcher(async (req, res) => {
1547
+ const { build_dir } = req.query;
1548
+ res.json({
1549
+ finished: await checkFiles(build_dir, ["logs.txt", "error_logs.txt"]),
1550
+ });
1551
+ })
1552
+ );
1553
+
1554
+ router.get(
1555
+ "/build-mobile-app/result",
1556
+ isAdmin,
1557
+ error_catcher(async (req, res) => {
1558
+ const { build_dir_name } = req.query;
1559
+ const rootFolder = await File.rootFolder();
1560
+ const buildDir = path.join(
1561
+ rootFolder.location,
1562
+ "mobile_app",
1563
+ build_dir_name
1564
+ );
1565
+ const files = await Promise.all(
1566
+ fs
1567
+ .readdirSync(buildDir)
1568
+ .map(async (outFile) => await File.from_file_on_disk(outFile, buildDir))
1569
+ );
1570
+ const resultMsg = files.find((file) => file.filename === "logs.txt")
1571
+ ? req.__("The build was successfully")
1572
+ : req.__("Unable to build the app");
1573
+ res.sendWrap(req.__(`Admin`), {
1574
+ above: [
1575
+ {
1576
+ type: "card",
1577
+ title: req.__("Build Result"),
1578
+ contents: div(resultMsg),
1579
+ },
1580
+ files.length > 0 ? app_files_table(files, build_dir_name, req) : "",
1581
+ ],
1582
+ });
1583
+ })
1584
+ );
1585
+
1496
1586
  router.post(
1497
1587
  "/build-mobile-app",
1498
1588
  isAdmin,
@@ -1507,24 +1597,27 @@ router.post(
1507
1597
  serverURL,
1508
1598
  } = req.body;
1509
1599
  if (!androidPlatform && !iOSPlatform) {
1510
- req.flash(
1511
- "error",
1512
- req.__("Please select at least one platform (android or iOS).")
1513
- );
1514
- return res.redirect("/admin/build-mobile-app");
1600
+ return res.json({
1601
+ error: req.__("Please select at least one platform (android or iOS)."),
1602
+ });
1515
1603
  }
1516
1604
  if (!androidPlatform && useDocker) {
1517
- req.flash("error", req.__("Only the android build supports docker."));
1518
- return res.redirect("/admin/build-mobile-app");
1605
+ return res.json({
1606
+ error: req.__("Only the android build supports docker."),
1607
+ });
1519
1608
  }
1520
1609
  if (!serverURL || serverURL.length == 0) {
1521
1610
  serverURL = getState().getConfig("base_url") || "";
1522
1611
  }
1523
1612
  if (!serverURL.startsWith("http")) {
1524
- req.flash("error", req.__("Please enter a valid server URL."));
1525
- return res.redirect("/admin/build-mobile-app");
1613
+ return res.json({
1614
+ error: req.__("Please enter a valid server URL."),
1615
+ });
1526
1616
  }
1527
- const appOut = path.join(__dirname, "..", "mobile-app-out");
1617
+ const outDirName = `build_${new Date().valueOf()}`;
1618
+ const rootFolder = await File.rootFolder();
1619
+ const buildDir = path.join(rootFolder.location, "mobile_app", outDirName);
1620
+ await File.new_folder(outDirName, "/mobile_app");
1528
1621
  const spawnParams = [
1529
1622
  "build-app",
1530
1623
  "-e",
@@ -1532,7 +1625,7 @@ router.post(
1532
1625
  "-t",
1533
1626
  entryPointType,
1534
1627
  "-c",
1535
- appOut,
1628
+ buildDir,
1536
1629
  "-b",
1537
1630
  `${os.userInfo().homedir}/mobile_app_build`,
1538
1631
  ];
@@ -1547,6 +1640,15 @@ router.post(
1547
1640
  }
1548
1641
  if (appFile) spawnParams.push("-a", appFile);
1549
1642
  if (serverURL) spawnParams.push("-s", serverURL);
1643
+ if (
1644
+ db.is_it_multi_tenant() &&
1645
+ db.getTenantSchema() !== db.connectObj.default_schema
1646
+ ) {
1647
+ spawnParams.push("--tenantAppName", db.getTenantSchema());
1648
+ }
1649
+ // end http call, return the out directory name
1650
+ // the gui polls for results
1651
+ res.json({ build_dir_name: outDirName });
1550
1652
  const child = spawn("saltcorn", spawnParams, {
1551
1653
  stdio: ["ignore", "pipe", "pipe"],
1552
1654
  cwd: ".",
@@ -1561,60 +1663,37 @@ router.post(
1561
1663
  childOutputs.push(data.toString());
1562
1664
  });
1563
1665
  child.on("exit", async function (exitCode, signal) {
1564
- if (exitCode === 0) {
1565
- const files = await Promise.all(
1566
- fs
1567
- .readdirSync(appOut)
1568
- .map(
1569
- async (outFile) =>
1570
- await File.from_existing_file(appOut, outFile, req.user.id)
1571
- )
1572
- );
1573
- res.sendWrap(req.__(`Admin`), {
1574
- above: [
1575
- {
1576
- type: "card",
1577
- title: req.__("Build Result"),
1578
- contents: div(req.__("The build was successfully")),
1579
- },
1580
- files.length > 0 ? app_files_table(files, req) : "",
1581
- ],
1582
- });
1583
- } else
1584
- res.sendWrap(req.__(`Admin`), {
1585
- above: [
1586
- {
1587
- type: "card",
1588
- title: req.__("Build Result"),
1589
- contents: div(
1590
- req.__("Unable to build the app:"),
1591
- pre(code(childOutputs.join("<br/>")))
1592
- ),
1593
- },
1594
- ],
1595
- });
1666
+ const logFile = exitCode === 0 ? "logs.txt" : "error_logs.txt";
1667
+ fs.writeFile(
1668
+ path.join(buildDir, logFile),
1669
+ childOutputs.join("\n"),
1670
+ (error) => {
1671
+ if (error) {
1672
+ console.log(`unable to write '${logFile}' to '${buildDir}'`);
1673
+ console.log(error);
1674
+ }
1675
+ }
1676
+ );
1596
1677
  });
1597
1678
  child.on("error", function (msg) {
1598
1679
  const message = msg.message ? msg.message : msg.code;
1599
1680
  const stack = msg.stack ? msg.stack : "";
1600
- res.sendWrap(req.__(`Admin`), {
1601
- above: [
1602
- {
1603
- type: "card",
1604
- title: req.__("Build Result"),
1605
- contents: div(
1606
- p(req.__("Unable to build the app:")),
1607
- pre(code(message)),
1608
- pre(code(stack))
1609
- ),
1610
- },
1611
- ],
1612
- });
1681
+ fs.writeFile(
1682
+ path.join(buildDir, "error_logs.txt"),
1683
+ [message, stack].join("\n"),
1684
+ (error) => {
1685
+ if (error) {
1686
+ console.log(`unable to write '${logFile}' to '${buildDir}'`);
1687
+ console.log(error);
1688
+ }
1689
+ }
1690
+ );
1613
1691
  });
1614
1692
  })
1615
1693
  );
1616
1694
 
1617
1695
  /**
1696
+ * Clear all
1618
1697
  * @name post/clear-all
1619
1698
  * @function
1620
1699
  * @memberof module:routes/admin~routes/adminRouter
@@ -1733,3 +1812,85 @@ router.post(
1733
1812
  }
1734
1813
  })
1735
1814
  );
1815
+
1816
+ /**
1817
+ * Developer settings form
1818
+ * @param {object} req request
1819
+ * @returns {Promise<Form>} form
1820
+ */
1821
+ const dev_form = async (req) => {
1822
+ return await config_fields_form({
1823
+ req,
1824
+ field_names: [
1825
+ "development_mode",
1826
+ "log_sql",
1827
+ "log_level",
1828
+ ],
1829
+ action: "/admin/dev",
1830
+ });
1831
+ };
1832
+ /**
1833
+ * Developer Mode page
1834
+ * @name get/dev
1835
+ * @function
1836
+ * @memberof module:routes/admin~routes/adminRouter
1837
+ */
1838
+ router.get(
1839
+ "/dev",
1840
+ isAdmin,
1841
+ error_catcher(async (req, res) => {
1842
+ const form = await dev_form(req);
1843
+ send_admin_page({
1844
+ res,
1845
+ req,
1846
+ active_sub: "Development",
1847
+ contents: {
1848
+ type: "card",
1849
+ title: req.__("Development settings"),
1850
+ contents: [
1851
+ renderForm(form, req.csrfToken())/*,
1852
+ a(
1853
+ {
1854
+ id: "testemail",
1855
+ href: "/admin/send-test-email",
1856
+ class: "btn btn-primary",
1857
+ },
1858
+ req.__("Send test email")
1859
+ ),*/
1860
+ ],
1861
+ },
1862
+ });
1863
+ })
1864
+ );
1865
+
1866
+ /**
1867
+ * Development mode
1868
+ * @name post/email
1869
+ * @function
1870
+ * @memberof module:routes/admin~routes/adminRouter
1871
+ */
1872
+ router.post(
1873
+ "/dev",
1874
+ isAdmin,
1875
+ error_catcher(async (req, res) => {
1876
+ const form = await dev_form(req);
1877
+ form.validate(req.body);
1878
+ if (form.hasErrors) {
1879
+ send_admin_page({
1880
+ res,
1881
+ req,
1882
+ active_sub: "Development",
1883
+ contents: {
1884
+ type: "card",
1885
+ title: req.__("Development settings"),
1886
+ contents: [renderForm(form, req.csrfToken())],
1887
+ },
1888
+ });
1889
+ } else {
1890
+ await save_config_from_form(form);
1891
+ req.flash("success", req.__("Development mode settings updated"));
1892
+ if (!req.xhr) res.redirect("/admin/dev");
1893
+ else res.json({ success: "ok" });
1894
+ }
1895
+ })
1896
+ );
package/routes/api.js CHANGED
@@ -124,8 +124,17 @@ router.post(
124
124
  error_catcher(async (req, res, next) => {
125
125
  let { viewName, queryName } = req.params;
126
126
  const view = await View.findOne({ name: viewName });
127
+ const db = require("@saltcorn/data/db");
127
128
  if (!view) {
128
- res.status(404).json({ error: req.__("Not found") });
129
+ res.status(404).json({
130
+ error: req.__("View %s not found", viewName),
131
+ view: viewName,
132
+ queryName: queryName,
133
+ smr: req.smr,
134
+ smrHeader: req.headers["x-saltcorn-client"],
135
+ schema: db.getTenantSchema(),
136
+ userTenant: req.user?.tenant,
137
+ });
129
138
  return;
130
139
  }
131
140
  await passport.authenticate(
@@ -143,7 +152,15 @@ router.post(
143
152
  const resp = await queries[queryName](...args, true);
144
153
  res.json({ success: resp, alerts: getFlashes(req) });
145
154
  } else {
146
- res.status(404).json({ error: req.__("Not found") });
155
+ res.status(404).json({
156
+ error: req.__("Query %s not found", queryName),
157
+ view: viewName,
158
+ queryName: queryName,
159
+ smr: req.smr,
160
+ smrHeader: req.headers["x-saltcorn-client"],
161
+ schema: db.getTenantSchema(),
162
+ userTenant: req.user?.tenant,
163
+ });
147
164
  }
148
165
  } else {
149
166
  res.status(401).json({ error: req.__("Not authorized") });