@saltcorn/server 0.7.2-beta.9 → 0.7.3-beta.1

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.9",
3
+ "version": "0.7.3-beta.1",
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.9",
10
- "@saltcorn/builder": "0.7.2-beta.9",
11
- "@saltcorn/data": "0.7.2-beta.9",
12
- "@saltcorn/admin-models": "0.7.2-beta.9",
13
- "@saltcorn/markup": "0.7.2-beta.9",
14
- "@saltcorn/sbadmin2": "0.7.2-beta.9",
9
+ "@saltcorn/base-plugin": "0.7.3-beta.1",
10
+ "@saltcorn/builder": "0.7.3-beta.1",
11
+ "@saltcorn/data": "0.7.3-beta.1",
12
+ "@saltcorn/admin-models": "0.7.3-beta.1",
13
+ "@saltcorn/markup": "0.7.3-beta.1",
14
+ "@saltcorn/sbadmin2": "0.7.3-beta.1",
15
15
  "@socket.io/cluster-adapter": "^0.1.0",
16
16
  "@socket.io/sticky": "^1.0.1",
17
17
  "aws-sdk": "^2.1037.0",
@@ -48,7 +48,7 @@
48
48
  "pg": "^8.2.1",
49
49
  "pluralize": "^8.0.0",
50
50
  "qrcode": "1.5.0",
51
- "sharp": "0.30.3",
51
+ "resize-with-sharp-or-jimp": "0.1.3",
52
52
  "socket.io": "4.2.0",
53
53
  "thirty-two": "1.0.2",
54
54
  "tmp-promise": "^3.0.2",
@@ -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();
@@ -437,7 +471,7 @@ function common_done(res, isWeb = true) {
437
471
 
438
472
  const repeaterCopyValuesToForm = (form, editor) => {
439
473
  const vs = JSON.parse(editor.getString());
440
- const allNames = new Set([]);
474
+
441
475
  const setVal = (k, ix, v) => {
442
476
  const $e = form.find(`input[name="${k}_${ix}"]`);
443
477
  if ($e.length) $e.val(v);
@@ -449,16 +483,22 @@ const repeaterCopyValuesToForm = (form, editor) => {
449
483
  vs.forEach((v, ix) => {
450
484
  Object.entries(v).forEach(([k, v]) => {
451
485
  //console.log(ix, k, typeof v, v)
452
- allNames.add(k);
453
486
  if (typeof v === "boolean") setVal(k, ix, v ? "on" : "");
454
487
  else setVal(k, ix, v);
455
488
  });
456
489
  });
457
490
  //delete
458
- [...allNames].forEach((k) => {
459
- for (let ix = vs.length; ix < vs.length + 20; ix++) {
460
- $(`input[name="${k}_${ix}"]`).remove();
461
- }
491
+ //for (let ix = vs.length; ix < vs.length + 20; ix++) {
492
+ // $(`input[name="${k}_${ix}"]`).remove();
493
+ //}
494
+ $(`input[type=hidden]`).each(function () {
495
+ const name = $(this).attr("name");
496
+ if (!name) return;
497
+ const m = name.match(/_(\d+)$/);
498
+ if (!m || !m[1]) return;
499
+ const ix = parseInt(m[1], 10);
500
+ if (typeof ix !== "number" || isNaN(ix)) return;
501
+ if (ix >= vs.length) $(this).remove();
462
502
  });
463
503
  };
464
504
  function align_dropdown(id) {
@@ -542,3 +582,9 @@ function unique_field_from_rows(
542
582
  }
543
583
  }
544
584
  }
585
+
586
+ function cancel_form(form) {
587
+ if (!form) return;
588
+ $(form).append(`<input type="hidden" name="_cancel" value="on">`);
589
+ $(form).submit();
590
+ }
@@ -294,3 +294,7 @@ section.range-slider input:last-of-type::-moz-range-track {
294
294
  section.range-slider input[type="range"]::-moz-focus-outer {
295
295
  border: 0;
296
296
  }
297
+
298
+ .btn-xs {
299
+ padding: 0.1rem 0.4rem !important;
300
+ }
@@ -162,6 +162,13 @@ function globalErrorCatcher(message, source, lineno, colno, error) {
162
162
  });
163
163
  }
164
164
 
165
+ function close_saltcorn_modal() {
166
+ var myModalEl = document.getElementById("scmodal");
167
+ if (!myModalEl) return;
168
+ var modal = bootstrap.Modal.getInstance(myModalEl);
169
+ if (modal) modal.dispose();
170
+ }
171
+
165
172
  function ajax_modal(url, opts = {}) {
166
173
  if ($("#scmodal").length === 0) {
167
174
  $("body").append(`<div id="scmodal", class="modal">
@@ -179,9 +186,7 @@ function ajax_modal(url, opts = {}) {
179
186
  </div>
180
187
  </div>`);
181
188
  } else if ($("#scmodal").hasClass("show")) {
182
- var myModalEl = document.getElementById("scmodal");
183
- var modal = bootstrap.Modal.getInstance(myModalEl);
184
- modal.dispose();
189
+ close_saltcorn_modal();
185
190
  }
186
191
  if (opts.submitReload === false) $("#scmodal").addClass("no-submit-reload");
187
192
  else $("#scmodal").removeClass("no-submit-reload");
@@ -429,7 +434,6 @@ async function fill_formula_btn_click(btn, k) {
429
434
  if (k) k();
430
435
  }
431
436
 
432
-
433
437
  /*
434
438
  https://github.com/jeffdavidgreen/bootstrap-html5-history-tabs/blob/master/bootstrap-history-tabs.js
435
439
  Copyright (c) 2015 Jeff Green
@@ -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/files.js CHANGED
@@ -10,7 +10,7 @@ const File = require("@saltcorn/data/models/file");
10
10
  const User = require("@saltcorn/data/models/user");
11
11
  const { getState } = require("@saltcorn/data/db/state");
12
12
  const s3storage = require("../s3storage");
13
- const sharp = require("sharp");
13
+ const resizer = require("resize-with-sharp-or-jimp");
14
14
 
15
15
  const {
16
16
  mkTable,
@@ -214,7 +214,11 @@ router.get(
214
214
  }
215
215
  const fnm = `${file.location}_w${width}`;
216
216
  if (!fs.existsSync(fnm)) {
217
- await sharp(file.location).resize({ width }).toFile(fnm);
217
+ await resizer({
218
+ fromFileName: file.location,
219
+ width,
220
+ toFileName: fnm,
221
+ });
218
222
  }
219
223
  res.sendFile(fnm);
220
224
  }
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 {