@saltcorn/server 0.7.2-beta.7 → 0.7.2

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,17 +1,17 @@
1
1
  {
2
2
  "name": "@saltcorn/server",
3
- "version": "0.7.2-beta.7",
3
+ "version": "0.7.2",
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.7.2-beta.7",
10
- "@saltcorn/builder": "0.7.2-beta.7",
11
- "@saltcorn/data": "0.7.2-beta.7",
12
- "@saltcorn/admin-models": "0.7.2-beta.7",
13
- "@saltcorn/markup": "0.7.2-beta.7",
14
- "@saltcorn/sbadmin2": "0.7.2-beta.7",
9
+ "@saltcorn/base-plugin": "0.7.2",
10
+ "@saltcorn/builder": "0.7.2",
11
+ "@saltcorn/data": "0.7.2",
12
+ "@saltcorn/admin-models": "0.7.2",
13
+ "@saltcorn/markup": "0.7.2",
14
+ "@saltcorn/sbadmin2": "0.7.2",
15
15
  "@socket.io/cluster-adapter": "^0.1.0",
16
16
  "@socket.io/sticky": "^1.0.1",
17
17
  "aws-sdk": "^2.1037.0",
@@ -215,6 +215,40 @@ function initialize_page() {
215
215
  $(".blur-on-enter-keypress").bind("keyup", function (e) {
216
216
  if (e.keyCode === 13) e.target.blur();
217
217
  });
218
+
219
+ const validate_expression_elem = (target) => {
220
+ const next = target.next();
221
+ if (next.hasClass("expr-error")) next.remove();
222
+ const val = target.val();
223
+ if (target.hasClass("validate-expression-conditional")) {
224
+ const box = target
225
+ .closest(".form-namespace")
226
+ .find(`[name="${target.attr("name")}_formula"]`);
227
+ if (!box.prop("checked")) return;
228
+ }
229
+ if (!val) return;
230
+ try {
231
+ Function("return " + val);
232
+ } catch (error) {
233
+ target.after(`<small class="text-danger font-monospace d-block expr-error">
234
+ ${error.message}
235
+ </small>`);
236
+ }
237
+ };
238
+ $(".validate-expression").bind("input", function (e) {
239
+ const target = $(e.target);
240
+ validate_expression_elem(target);
241
+ });
242
+ $(".validate-expression-conditional").each(function () {
243
+ const theInput = $(this);
244
+ theInput
245
+ .closest(".form-namespace")
246
+ .find(`[name="${theInput.attr("name")}_formula"]`)
247
+ .bind("change", function (e) {
248
+ validate_expression_elem(theInput);
249
+ });
250
+ });
251
+
218
252
  $("form").change(apply_showif);
219
253
  apply_showif();
220
254
  apply_showif();
@@ -17,6 +17,8 @@ const relevantPackages = [
17
17
  "db-common",
18
18
  "postgres",
19
19
  "saltcorn-data",
20
+ "saltcorn-builder",
21
+ "saltcorn-admin-models",
20
22
  "saltcorn-markup",
21
23
  "saltcorn-sbadmin2",
22
24
  "server",
@@ -28,7 +30,6 @@ const relevantPackages = [
28
30
  */
29
31
  const excludePatterns = [
30
32
  /\/node_modules/,
31
- /\/public/,
32
33
  /\.git/,
33
34
  /\.docs/,
34
35
  /\.docs/,
package/routes/actions.js CHANGED
@@ -618,6 +618,7 @@ router.get(
618
618
  console: fakeConsole,
619
619
  table,
620
620
  row,
621
+ req,
621
622
  ...(row || {}),
622
623
  Table,
623
624
  user: req.user,
package/routes/admin.js CHANGED
@@ -48,6 +48,7 @@ const { loadAllPlugins } = require("../load_plugins");
48
48
  const {
49
49
  create_backup,
50
50
  restore,
51
+ auto_backup_now,
51
52
  } = require("@saltcorn/admin-models/models/backup");
52
53
  const {
53
54
  runConfigurationCheck,
@@ -293,41 +294,163 @@ router.get(
293
294
  "/backup",
294
295
  isAdmin,
295
296
  error_catcher(async (req, res) => {
297
+ const backupForm = autoBackupForm(req);
298
+ backupForm.values.auto_backup_frequency = getState().getConfig(
299
+ "auto_backup_frequency"
300
+ );
301
+ backupForm.values.auto_backup_destination = getState().getConfig(
302
+ "auto_backup_destination"
303
+ );
304
+ backupForm.values.auto_backup_directory = getState().getConfig(
305
+ "auto_backup_directory"
306
+ );
307
+ backupForm.values.auto_backup_expire_days = getState().getConfig(
308
+ "auto_backup_expire_days"
309
+ );
310
+ const isRoot = db.getTenantSchema() === db.connectObj.default_schema;
311
+
296
312
  send_admin_page({
297
313
  res,
298
314
  req,
299
315
  active_sub: "Backup",
300
316
  contents: {
301
- type: "card",
302
- title: req.__("Backup"),
303
- contents: table(
304
- tbody(
305
- tr(
306
- td(
317
+ above: [
318
+ {
319
+ type: "card",
320
+ title: req.__("Manual backup"),
321
+ contents: {
322
+ besides: [
307
323
  div(
308
- post_btn("/admin/backup", req.__("Backup"), req.csrfToken())
309
- )
310
- ),
311
- td(p({ class: "ms-4 pt-2" }, req.__("Download a backup")))
312
- ),
313
- tr(td(div({ class: "my-4" }))),
314
- tr(
315
- td(
316
- restore_backup(req.csrfToken(), [
317
- i({ class: "fas fa-2x fa-upload" }),
318
- "<br/>",
319
- req.__("Restore"),
320
- ])
321
- ),
322
- td(p({ class: "ms-4" }, req.__("Restore a backup")))
323
- )
324
- )
325
- ),
324
+ post_btn(
325
+ "/admin/backup",
326
+ i({ class: "fas fa-download me-2" }) +
327
+ req.__("Download a backup"),
328
+ req.csrfToken(),
329
+ {
330
+ btnClass: "btn-outline-primary",
331
+ }
332
+ )
333
+ ),
334
+ div(
335
+ restore_backup(req.csrfToken(), [
336
+ i({ class: "fas fa-2x fa-upload me-2" }),
337
+ "",
338
+ req.__("Restore a backup"),
339
+ ])
340
+ ),
341
+ ],
342
+ },
343
+ },
344
+ isRoot
345
+ ? {
346
+ type: "card",
347
+ title: req.__("Automated backup"),
348
+ contents: div(renderForm(backupForm, req.csrfToken())),
349
+ }
350
+ : { type: "blank", contents: "" },
351
+ ],
326
352
  },
327
353
  });
328
354
  })
329
355
  );
330
356
 
357
+ /**
358
+ * Auto backup Form
359
+ * @param {object} req
360
+ * @returns {Form} form
361
+ */
362
+ const autoBackupForm = (req) =>
363
+ new Form({
364
+ action: "/admin/set-auto-backup",
365
+ submitButtonClass: "btn-outline-primary",
366
+ onChange: "remove_outline(this)",
367
+ submitLabel: "Save settings",
368
+ additionalButtons: [
369
+ {
370
+ label: "Backup now",
371
+ id: "btnBackupNow",
372
+ class: "btn btn-outline-secondary",
373
+ onclick: "ajax_post('/admin/auto-backup-now')",
374
+ },
375
+ ],
376
+ fields: [
377
+ {
378
+ type: "String",
379
+ label: req.__("Frequency"),
380
+ name: "auto_backup_frequency",
381
+ required: true,
382
+ attributes: { options: ["Never", "Daily", "Weekly"] },
383
+ },
384
+ {
385
+ type: "String",
386
+ label: req.__("Destination"),
387
+ name: "auto_backup_destination",
388
+ required: true,
389
+ showIf: { auto_backup_frequency: ["Daily", "Weekly"] },
390
+ attributes: { options: ["Saltcorn files", "Local directory"] },
391
+ },
392
+ {
393
+ type: "String",
394
+ label: req.__("Directory"),
395
+ name: "auto_backup_directory",
396
+ showIf: {
397
+ auto_backup_frequency: ["Daily", "Weekly"],
398
+ auto_backup_destination: "Local directory",
399
+ },
400
+ },
401
+ {
402
+ type: "Integer",
403
+ label: req.__("Expiration in days"),
404
+ sublabel: req.__(
405
+ "Delete old backup files in this directory after the set number of days"
406
+ ),
407
+ name: "auto_backup_expire_days",
408
+ showIf: {
409
+ auto_backup_frequency: ["Daily", "Weekly"],
410
+ auto_backup_destination: "Local directory",
411
+ },
412
+ },
413
+ ],
414
+ });
415
+
416
+ router.post(
417
+ "/set-auto-backup",
418
+ isAdmin,
419
+ error_catcher(async (req, res) => {
420
+ const form = await autoBackupForm(req);
421
+ form.validate(req.body);
422
+ if (form.hasErrors) {
423
+ send_admin_page({
424
+ res,
425
+ req,
426
+ active_sub: "Backup",
427
+ contents: {
428
+ type: "card",
429
+ title: req.__("Backup settings"),
430
+ contents: [renderForm(form, req.csrfToken())],
431
+ },
432
+ });
433
+ } else {
434
+ await save_config_from_form(form);
435
+ req.flash("success", req.__("Backup settings updated"));
436
+ res.redirect("/admin/backup");
437
+ }
438
+ })
439
+ );
440
+ router.post(
441
+ "/auto-backup-now",
442
+ isAdmin,
443
+ error_catcher(async (req, res) => {
444
+ try {
445
+ await auto_backup_now();
446
+ req.flash("success", req.__("Backup successful"));
447
+ } catch (e) {
448
+ req.flash("error", e.message);
449
+ }
450
+ res.json({ reload_page: true });
451
+ })
452
+ );
453
+
331
454
  /**
332
455
  * @name get/system
333
456
  * @function
@@ -550,8 +673,8 @@ router.post(
550
673
  const fileName = await create_backup();
551
674
  res.type("application/zip");
552
675
  res.attachment(fileName);
553
- const file = fs.createReadStream(fileName);
554
- file.on("end", function () {
676
+ const file = fs.createReadStream(fileName);
677
+ file.on("end", function () {
555
678
  fs.unlink(fileName, function () {});
556
679
  });
557
680
  file.pipe(res);
@@ -794,7 +917,10 @@ router.get(
794
917
  ? div(
795
918
  { class: "alert alert-success", role: "alert" },
796
919
  i({ class: "fas fa-check-circle fa-lg me-2" }),
797
- h5({ class: "d-inline" }, req.__("No errors detected during configuration check"))
920
+ h5(
921
+ { class: "d-inline" },
922
+ req.__("No errors detected during configuration check")
923
+ )
798
924
  )
799
925
  : errors.map(mkError)
800
926
  ),
package/routes/fields.js CHANGED
@@ -300,7 +300,9 @@ const fieldFlow = (req) =>
300
300
  name: "also_delete_file",
301
301
  type: "Bool",
302
302
  label: req.__("Cascade delete to file"),
303
- sublabel: req.__("Deleting a row will also delete the file referenced by this field")
303
+ sublabel: req.__(
304
+ "Deleting a row will also delete the file referenced by this field"
305
+ ),
304
306
  },
305
307
  ],
306
308
  });
@@ -337,6 +339,7 @@ const fieldFlow = (req) =>
337
339
  label: req.__("Formula"),
338
340
  // todo sublabel
339
341
  type: "String",
342
+ class: "validate-expression",
340
343
  validator: expressionValidator,
341
344
  }),
342
345
  new Field({
package/routes/tables.js CHANGED
@@ -21,7 +21,10 @@ const {
21
21
  post_delete_btn,
22
22
  post_dropdown_item,
23
23
  } = require("@saltcorn/markup");
24
- const { recalculate_for_stored } = require("@saltcorn/data/models/expression");
24
+ const {
25
+ recalculate_for_stored,
26
+ expressionValidator,
27
+ } = require("@saltcorn/data/models/expression");
25
28
  const { isAdmin, error_catcher, setTenant } = require("./utils.js");
26
29
  const Form = require("@saltcorn/data/models/form");
27
30
  const {
@@ -101,7 +104,9 @@ const tableForm = async (table, req) => {
101
104
  {
102
105
  name: "ownership_formula",
103
106
  label: req.__("Ownership formula"),
107
+ validator: expressionValidator,
104
108
  type: "String",
109
+ class: "validate-expression",
105
110
  sublabel:
106
111
  req.__("User is treated as owner if true. In scope: ") +
107
112
  ["user", ...fields.map((f) => f.name)]
@@ -878,10 +883,20 @@ router.post(
878
883
  const { id, _csrf, ...rest } = v;
879
884
  const table = await Table.findOne({ id: parseInt(id) });
880
885
  const old_versioned = table.versioned;
886
+ let hasError = false;
881
887
  if (!rest.versioned) rest.versioned = false;
882
- if (rest.ownership_field_id === "_formula")
888
+ if (rest.ownership_field_id === "_formula") {
883
889
  rest.ownership_field_id = null;
884
- else rest.ownership_formula = null;
890
+ const fmlValidRes = expressionValidator(rest.ownership_formula);
891
+ console.log({ fmlValidRes });
892
+ if (typeof fmlValidRes === "string") {
893
+ req.flash(
894
+ "error",
895
+ req.__(`Invalid ownership formula: %s`, fmlValidRes)
896
+ );
897
+ hasError = true;
898
+ }
899
+ } else rest.ownership_formula = null;
885
900
  await table.update(rest);
886
901
  if (!old_versioned && rest.versioned)
887
902
  req.flash(
@@ -893,7 +908,7 @@ router.post(
893
908
  "success",
894
909
  req.__("Table saved with version history disabled")
895
910
  );
896
- else req.flash("success", req.__("Table saved"));
911
+ else if (!hasError) req.flash("success", req.__("Table saved"));
897
912
 
898
913
  res.redirect(`/table/${id}`);
899
914
  }
package/serve.js CHANGED
@@ -43,6 +43,7 @@ const {
43
43
  eachTenant,
44
44
  getAllTenants,
45
45
  } = require("@saltcorn/admin-models/models/tenant");
46
+ const { auto_backup_now } = require("@saltcorn/admin-models/models/backup");
46
47
 
47
48
  // helpful https://gist.github.com/jpoehls/2232358
48
49
  /**
@@ -135,7 +136,13 @@ const onMessageFromWorker =
135
136
  //console.log("worker msg", typeof msg, msg);
136
137
  if (msg === "Start" && !masterState.started) {
137
138
  masterState.started = true;
138
- runScheduler({ port, watchReaper, disableScheduler, eachTenant });
139
+ runScheduler({
140
+ port,
141
+ watchReaper,
142
+ disableScheduler,
143
+ eachTenant,
144
+ auto_backup_now,
145
+ });
139
146
  require("./systemd")({ port });
140
147
  return true;
141
148
  } else if (msg === "RestartServer") {
@@ -261,6 +268,13 @@ module.exports =
261
268
  });
262
269
  } else {
263
270
  await nonGreenlockWorkerSetup(appargs, port);
271
+ runScheduler({
272
+ port,
273
+ watchReaper,
274
+ disableScheduler,
275
+ eachTenant,
276
+ auto_backup_now,
277
+ });
264
278
  }
265
279
  Trigger.emitEvent("Startup");
266
280
  } else {