@saltcorn/server 0.8.5-beta.2 → 0.8.5-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/package.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "@saltcorn/server",
3
- "version": "0.8.5-beta.2",
3
+ "version": "0.8.5-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.5-beta.2",
10
- "@saltcorn/builder": "0.8.5-beta.2",
11
- "@saltcorn/data": "0.8.5-beta.2",
12
- "@saltcorn/admin-models": "0.8.5-beta.2",
13
- "@saltcorn/filemanager": "0.8.5-beta.2",
14
- "@saltcorn/markup": "0.8.5-beta.2",
15
- "@saltcorn/sbadmin2": "0.8.5-beta.2",
9
+ "@saltcorn/base-plugin": "0.8.5-beta.3",
10
+ "@saltcorn/builder": "0.8.5-beta.3",
11
+ "@saltcorn/data": "0.8.5-beta.3",
12
+ "@saltcorn/admin-models": "0.8.5-beta.3",
13
+ "@saltcorn/filemanager": "0.8.5-beta.3",
14
+ "@saltcorn/markup": "0.8.5-beta.3",
15
+ "@saltcorn/sbadmin2": "0.8.5-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",
@@ -52,6 +52,7 @@
52
52
  "qrcode": "1.5.1",
53
53
  "resize-with-sharp-or-jimp": "0.1.6",
54
54
  "socket.io": "4.6.0",
55
+ "systeminformation": "^5.11.12",
55
56
  "thirty-two": "1.0.2",
56
57
  "tmp-promise": "^3.0.2",
57
58
  "uuid": "^8.2.0",
@@ -145,7 +145,7 @@ function apply_showif() {
145
145
  });
146
146
  element.dispatchEvent(new Event("RefreshSelectOptions"));
147
147
  if (e.hasClass("selectized") && $().selectize) {
148
- e.selectize()[0].selectize.clearOptions();
148
+ e.selectize()[0].selectize.clearOptions(true);
149
149
  e.selectize()[0].selectize.addOption(dataOptions);
150
150
  if (typeof currentDataOption !== "undefined")
151
151
  e.selectize()[0].selectize.setValue(currentDataOption);
@@ -428,6 +428,10 @@ function initialize_page() {
428
428
  var key = $(this).attr("data-inline-edit-field") || "value";
429
429
  var ajax = !!$(this).attr("data-inline-edit-ajax");
430
430
  var type = $(this).attr("data-inline-edit-type");
431
+ var schema = $(this).attr("data-inline-edit-schema");
432
+ if (schema) {
433
+ schema = JSON.parse(decodeURIComponent(schema));
434
+ }
431
435
  var is_key = type?.startsWith("Key:");
432
436
  const opts = encodeURIComponent(
433
437
  JSON.stringify({
@@ -438,12 +442,16 @@ function initialize_page() {
438
442
  current_label: $(this).attr("data-inline-edit-current-label"),
439
443
  type,
440
444
  is_key,
445
+ schema,
441
446
  })
442
447
  );
443
- if (is_key) {
444
- const [tblName, target] = type.replace("Key:", "").split(".");
448
+ const doAjaxOptionsFetch = (tblName, target) => {
445
449
  $.ajax(`/api/${tblName}`).then((resp) => {
446
450
  if (resp.success) {
451
+ resp.success.sort((a, b) =>
452
+ a[target]?.toLowerCase?.() > b[target]?.toLowerCase?.() ? 1 : -1
453
+ );
454
+
447
455
  const selopts = resp.success.map(
448
456
  (r) =>
449
457
  `<option ${current == r.id ? `selected ` : ``}value="${
@@ -463,6 +471,14 @@ function initialize_page() {
463
471
  );
464
472
  }
465
473
  });
474
+ };
475
+ if (type === "JSON" && schema && schema.type.startsWith("Key to ")) {
476
+ const tblName = schema.type.replace("Key to ", "");
477
+ const target = schema.summary_field || "id";
478
+ doAjaxOptionsFetch(tblName, target);
479
+ } else if (is_key) {
480
+ const [tblName, target] = type.replace("Key:", "").split(".");
481
+ doAjaxOptionsFetch(tblName, target);
466
482
  } else
467
483
  $(this).replaceWith(
468
484
  `<form method="post" action="${url}" ${
@@ -590,7 +606,10 @@ $(initialize_page);
590
606
  function cancel_inline_edit(e, opts1) {
591
607
  var opts = JSON.parse(decodeURIComponent(opts1 || "") || "{}");
592
608
  var form = $(e.target).closest("form");
593
-
609
+ var json_fk_opt;
610
+ if (opts.schema) {
611
+ json_fk_opt = form.find(`option[value="${opts.current}"]`).text();
612
+ }
594
613
  form.replaceWith(`<div
595
614
  data-inline-edit-field="${opts.key}"
596
615
  ${opts.ajax ? `data-inline-edit-ajax="true"` : ""}
@@ -601,8 +620,17 @@ function cancel_inline_edit(e, opts1) {
601
620
  ? `data-inline-edit-current-label="${opts.current_label}"`
602
621
  : ""
603
622
  }
623
+ ${
624
+ opts.schema
625
+ ? `data-inline-edit-schema="${encodeURIComponent(
626
+ JSON.stringify(opts.schema)
627
+ )}"`
628
+ : ""
629
+ }
604
630
  data-inline-edit-dest-url="${opts.url}">
605
- <span class="current">${opts.current_label || opts.current}</span>
631
+ <span class="current">${
632
+ json_fk_opt || opts.current_label || opts.current
633
+ }</span>
606
634
  <i class="editicon fas fa-edit ms-1"></i>
607
635
  </div>`);
608
636
  initialize_page();
@@ -624,15 +652,23 @@ function inline_ajax_submit(e, opts1) {
624
652
  success: function (res) {
625
653
  if (opts) {
626
654
  let rawVal = formDataArray.find((f) => f.name == opts.key).value;
627
- let val = opts.is_key
628
- ? form.find("select").find("option:selected").text()
629
- : rawVal;
655
+ let val =
656
+ opts.is_key || (opts.schema && opts.schema.type.startsWith("Key to "))
657
+ ? form.find("select").find("option:selected").text()
658
+ : rawVal;
630
659
 
631
660
  $(e.target).replaceWith(`<div
632
661
  data-inline-edit-field="${opts.key}"
633
662
  ${opts.ajax ? `data-inline-edit-ajax="true"` : ""}
634
663
  ${opts.type ? `data-inline-edit-type="${opts.type}"` : ""}
635
664
  ${opts.current ? `data-inline-edit-current="${rawVal}"` : ""}
665
+ ${
666
+ opts.schema
667
+ ? `data-inline-edit-schema="${encodeURIComponent(
668
+ JSON.stringify(opts.schema)
669
+ )}"`
670
+ : ""
671
+ }
636
672
  ${opts.current_label ? `data-inline-edit-current-label="${val}"` : ""}
637
673
  data-inline-edit-dest-url="${opts.url}">
638
674
  <span class="current">${val}</span>
package/routes/admin.js CHANGED
@@ -10,6 +10,7 @@ const {
10
10
  error_catcher,
11
11
  getGitRevision,
12
12
  setTenant,
13
+ get_sys_info,
13
14
  } = require("./utils.js");
14
15
  const Table = require("@saltcorn/data/models/table");
15
16
  const Plugin = require("@saltcorn/data/models/plugin");
@@ -864,7 +865,7 @@ router.get(
864
865
  const can_update =
865
866
  !is_latest && !process.env.SALTCORN_DISABLE_UPGRADE && !git_commit;
866
867
  const dbversion = await db.getVersion(true);
867
-
868
+ const { memUsage, diskUsage, cpuUsage } = await get_sys_info();
868
869
  send_admin_page({
869
870
  res,
870
871
  req,
@@ -986,7 +987,20 @@ router.get(
986
987
  tr(
987
988
  th(req.__("Process uptime")),
988
989
  td(moment(get_process_init_time()).fromNow(true))
989
- )
990
+ ),
991
+ tr(
992
+ th(req.__("Disk usage")),
993
+ diskUsage > 95
994
+ ? td(
995
+ { class: "text-danger fw-bold" },
996
+ diskUsage,
997
+ "%",
998
+ i({ class: "fas fa-exclamation-triangle ms-1" })
999
+ )
1000
+ : td(diskUsage, "%")
1001
+ ),
1002
+ tr(th(req.__("CPU usage")), td(cpuUsage, "%")),
1003
+ tr(th(req.__("Mem usage")), td(memUsage, "%"))
990
1004
  )
991
1005
  ),
992
1006
  p(
@@ -1307,7 +1321,7 @@ router.get(
1307
1321
  "/configuration-check",
1308
1322
  isAdmin,
1309
1323
  error_catcher(async (req, res) => {
1310
- const filename = `${moment().format("YYYMMDDHHmm")}.html`;
1324
+ const filename = `${moment().format("YYYYMMDDHHmm")}.html`;
1311
1325
  await File.new_folder("configuration_checks");
1312
1326
  const go = async () => {
1313
1327
  const { passes, errors, pass, warnings } = await runConfigurationCheck(
package/routes/api.js CHANGED
@@ -421,29 +421,29 @@ router.post(
421
421
  readState(row, fields);
422
422
  let errors = [];
423
423
  let hasErrors = false;
424
- Object.keys(row).forEach((k) => {
424
+ for (const k of Object.keys(row)) {
425
425
  const field = fields.find((f) => f.name === k);
426
426
  if (!field && k.includes(".")) {
427
427
  const [fnm, jkey] = k.split(".");
428
428
  const jfield = fields.find((f) => f.name === fnm);
429
429
  if (jfield?.type?.name === "JSON") {
430
- if (!row[fnm]) row[fnm] = { [jkey]: row[k] };
431
- else row[fnm][jkey] = row[k];
430
+ if (typeof row[fnm] === "undefined") {
431
+ const dbrow = await table.getRow({ [table.pk_name]: id });
432
+ row[fnm] = dbrow[fnm] || {};
433
+ }
434
+ row[fnm][jkey] = row[k];
432
435
  delete row[k];
433
436
  }
434
437
  } else if (!field || field.calculated) {
435
438
  delete row[k];
436
- return;
437
- }
438
-
439
- if (field?.type && field.type.validate) {
439
+ } else if (field?.type && field.type.validate) {
440
440
  const vres = field.type.validate(field.attributes || {})(row[k]);
441
441
  if (vres.error) {
442
442
  hasErrors = true;
443
443
  errors.push(`${k}: ${vres.error}`);
444
444
  }
445
445
  }
446
- });
446
+ }
447
447
  if (hasErrors) {
448
448
  res.status(400).json({ error: errors.join(", ") });
449
449
  return;
package/routes/tables.js CHANGED
@@ -762,18 +762,22 @@ router.get(
762
762
  })
763
763
  )
764
764
  ),
765
+ !table.external &&
766
+ div(
767
+ { class: "mx-auto" },
768
+ a(
769
+ { href: `/table/constraints/${table.id}` },
770
+ i({ class: "fas fa-2x fa-tasks" }),
771
+ "<br/>",
772
+ req.__("Constraints")
773
+ )
774
+ ),
775
+
765
776
  // only if table is not external
766
777
  !table.external &&
767
778
  div(
768
779
  { class: "mx-auto" },
769
780
  settingsDropdown(`dataMenuButton`, [
770
- a(
771
- {
772
- class: "dropdown-item",
773
- href: `/table/constraints/${table.id}`,
774
- },
775
- '<i class="fas fa-ban"></i>&nbsp;' + req.__("Constraints")
776
- ),
777
781
  // rename table doesnt supported for sqlite
778
782
  !db.isSQLite &&
779
783
  table.name !== "users" &&
@@ -1154,8 +1158,15 @@ router.get(
1154
1158
  [
1155
1159
  { label: req.__("Type"), key: "type" },
1156
1160
  {
1157
- label: req.__("Fields"),
1158
- key: (r) => r.configuration.fields.join(", "),
1161
+ label: req.__("What"),
1162
+ key: (r) =>
1163
+ r.type === "Unique"
1164
+ ? r.configuration.fields.join(", ")
1165
+ : r.type === "Index"
1166
+ ? r.configuration.field
1167
+ : r.type === "Formula"
1168
+ ? r.configuration.formula
1169
+ : "",
1159
1170
  },
1160
1171
  {
1161
1172
  label: req.__("Delete"),
@@ -1166,7 +1177,12 @@ router.get(
1166
1177
  cons,
1167
1178
  { hover: true }
1168
1179
  ),
1169
- link(`/table/add-constraint/${id}`, req.__("Add constraint")),
1180
+ req.__("Add constraint: "),
1181
+ link(`/table/add-constraint/${id}/Unique`, req.__("Unique")),
1182
+ " | ",
1183
+ link(`/table/add-constraint/${id}/Formula`, req.__("Formula")),
1184
+ " | ",
1185
+ link(`/table/add-constraint/${id}/Index`, req.__("Index")),
1170
1186
  ],
1171
1187
  },
1172
1188
  ],
@@ -1181,18 +1197,68 @@ router.get(
1181
1197
  * @param {object[]} fields
1182
1198
  * @returns {Form}
1183
1199
  */
1184
- const constraintForm = (req, table_id, fields) =>
1185
- new Form({
1186
- action: `/table/add-constraint/${table_id}`,
1187
- blurb: req.__(
1188
- "Tick the boxes for the fields that should be jointly unique"
1189
- ),
1190
- fields: fields.map((f) => ({
1191
- name: f.name,
1192
- label: f.label,
1193
- type: "Bool",
1194
- })),
1195
- });
1200
+ const constraintForm = (req, table_id, fields, type) => {
1201
+ switch (type) {
1202
+ case "Formula":
1203
+ return new Form({
1204
+ action: `/table/add-constraint/${table_id}/${type}`,
1205
+
1206
+ fields: [
1207
+ {
1208
+ name: "formula",
1209
+ label: req.__("Constraint formula"),
1210
+ validator: expressionValidator,
1211
+ type: "String",
1212
+ class: "validate-expression",
1213
+ sublabel:
1214
+ req.__(
1215
+ "Formula must evaluate to true for valid rows. In scope: "
1216
+ ) +
1217
+ fields
1218
+ .map((f) => f.name)
1219
+ .map((fn) => code(fn))
1220
+ .join(", "),
1221
+ },
1222
+ {
1223
+ name: "errormsg",
1224
+ label: "Error message",
1225
+ sublabel: "Shown the user if formula is false",
1226
+ type: "String",
1227
+ },
1228
+ ],
1229
+ });
1230
+ case "Unique":
1231
+ return new Form({
1232
+ action: `/table/add-constraint/${table_id}/${type}`,
1233
+ blurb: req.__(
1234
+ "Tick the boxes for the fields that should be jointly unique"
1235
+ ),
1236
+ fields: fields.map((f) => ({
1237
+ name: f.name,
1238
+ label: f.label,
1239
+ type: "Bool",
1240
+ })),
1241
+ });
1242
+ case "Index":
1243
+ return new Form({
1244
+ action: `/table/add-constraint/${table_id}/${type}`,
1245
+ blurb: req.__(
1246
+ "Choose the field to be indexed. This make searching the table faster."
1247
+ ),
1248
+ fields: [
1249
+ {
1250
+ type: "String",
1251
+ name: "field",
1252
+ label: "Field",
1253
+ required: true,
1254
+ attributes: {
1255
+ options: fields.map((f) => ({ label: f.label, name: f.name })),
1256
+ },
1257
+ },
1258
+ ],
1259
+ });
1260
+ }
1261
+ };
1196
1262
 
1197
1263
  /**
1198
1264
  * Add constraint GET handler
@@ -1203,10 +1269,10 @@ const constraintForm = (req, table_id, fields) =>
1203
1269
  * @function
1204
1270
  */
1205
1271
  router.get(
1206
- "/add-constraint/:id",
1272
+ "/add-constraint/:id/:type",
1207
1273
  isAdmin,
1208
1274
  error_catcher(async (req, res) => {
1209
- const { id } = req.params;
1275
+ const { id, type } = req.params;
1210
1276
  const table = await Table.findOne({ id });
1211
1277
  if (!table) {
1212
1278
  req.flash("error", `Table not found`);
@@ -1214,7 +1280,7 @@ router.get(
1214
1280
  return;
1215
1281
  }
1216
1282
  const fields = await table.getFields();
1217
- const form = constraintForm(req, table.id, fields);
1283
+ const form = constraintForm(req, table.id, fields, type);
1218
1284
  res.sendWrap(req.__(`Add constraint to %s`, table.name), {
1219
1285
  above: [
1220
1286
  {
@@ -1231,7 +1297,7 @@ router.get(
1231
1297
  },
1232
1298
  {
1233
1299
  type: "card",
1234
- title: req.__(`Add constraint to %s`, table.name),
1300
+ title: req.__(`Add %s constraint to %s`, type, table.name),
1235
1301
  contents: renderForm(form, req.csrfToken()),
1236
1302
  },
1237
1303
  ],
@@ -1247,10 +1313,10 @@ router.get(
1247
1313
  * @function
1248
1314
  */
1249
1315
  router.post(
1250
- "/add-constraint/:id",
1316
+ "/add-constraint/:id/:type",
1251
1317
  isAdmin,
1252
1318
  error_catcher(async (req, res) => {
1253
- const { id } = req.params;
1319
+ const { id, type } = req.params;
1254
1320
  const table = await Table.findOne({ id });
1255
1321
  if (!table) {
1256
1322
  req.flash("error", `Table not found`);
@@ -1258,16 +1324,20 @@ router.post(
1258
1324
  return;
1259
1325
  }
1260
1326
  const fields = await table.getFields();
1261
- const form = constraintForm(req, table.id, fields);
1327
+ const form = constraintForm(req, table.id, fields, type);
1262
1328
  form.validate(req.body);
1263
1329
  if (form.hasErrors) req.flash("error", req.__("An error occurred"));
1264
1330
  else {
1331
+ let configuration = {};
1332
+ if (type === "Unique")
1333
+ configuration.fields = fields
1334
+ .map((f) => f.name)
1335
+ .filter((f) => form.values[f]);
1336
+ else configuration = form.values;
1265
1337
  await TableConstraint.create({
1266
1338
  table_id: table.id,
1267
- type: "Unique",
1268
- configuration: {
1269
- fields: fields.map((f) => f.name).filter((f) => form.values[f]),
1270
- },
1339
+ type,
1340
+ configuration,
1271
1341
  });
1272
1342
  }
1273
1343
  res.redirect(`/table/constraints/${table.id}`);
package/routes/utils.js CHANGED
@@ -18,6 +18,24 @@ const cookieSession = require("cookie-session");
18
18
  const is = require("contractis/is");
19
19
  const { validateHeaderName, validateHeaderValue } = require("http");
20
20
  const Crash = require("@saltcorn/data/models/crash");
21
+ const si = require("systeminformation");
22
+
23
+ const get_sys_info = async () => {
24
+ const disks = await si.fsSize();
25
+ let size = 0;
26
+ let used = 0;
27
+ disks.forEach((d) => {
28
+ if (d && d.used && d.size) {
29
+ size += d.size;
30
+ used += d.used;
31
+ }
32
+ });
33
+ const diskUsage = Math.round(100 * (used / size));
34
+ const simem = await si.mem();
35
+ const memUsage = Math.round(100 - 100 * (simem.available / simem.total));
36
+ const cpuUsage = Math.round((await si.currentLoad()).currentLoad);
37
+ return { memUsage, diskUsage, cpuUsage };
38
+ };
21
39
 
22
40
  /**
23
41
  * Checks that user logged or not.
@@ -314,4 +332,5 @@ module.exports = {
314
332
  get_tenant_from_req,
315
333
  addOnDoneRedirect,
316
334
  is_relative_url,
335
+ get_sys_info,
317
336
  };
@@ -198,11 +198,11 @@ Gordon Kane, 217`;
198
198
  .set("Cookie", loginCookie)
199
199
  .expect(toInclude("books constraints"));
200
200
  await request(app)
201
- .get("/table/add-constraint/" + id)
201
+ .get("/table/add-constraint/" + id + "/Unique")
202
202
  .set("Cookie", loginCookie)
203
203
  .expect(toInclude("Add constraint to books"));
204
204
  await request(app)
205
- .post("/table/add-constraint/" + id)
205
+ .post("/table/add-constraint/" + id + "/Unique")
206
206
  .send("author=on")
207
207
  .send("pages=on")
208
208
  .set("Cookie", loginCookie)