@saltcorn/server 0.9.8-rc.0 → 1.0.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/locales/en.json CHANGED
@@ -1453,5 +1453,11 @@
1453
1453
  "Each row in a card. Not supported by all themes": "Each row in a card. Not supported by all themes",
1454
1454
  "Prune session interval (seconds)": "Prune session interval (seconds)",
1455
1455
  "Interval in seconds to check for expred sessions in the postgres db. 0, empty or a negative number to disable": "Interval in seconds to check for expred sessions in the postgres db. 0, empty or a negative number to disable",
1456
- "Progressive Web Application is not enabled": "Progressive Web Application is not enabled"
1456
+ "Progressive Web Application is not enabled": "Progressive Web Application is not enabled",
1457
+ "Events and Trigger settings": "Events and Trigger settings",
1458
+ "Periodic trigger timing (next event)": "Periodic trigger timing (next event)",
1459
+ "Hourly": "Hourly",
1460
+ "Daily": "Daily",
1461
+ "Weekly": "Weekly",
1462
+ "Code pages": "Code pages"
1457
1463
  }
package/markup/admin.js CHANGED
@@ -308,7 +308,7 @@ const send_events_page = (args) => {
308
308
  sub_sections: [
309
309
  { text: "Triggers", href: "/actions" },
310
310
  { text: "Custom", href: "/eventlog/custom" },
311
- { text: "Log settings", href: "/eventlog/settings" },
311
+ { text: "Settings", href: "/eventlog/settings" },
312
312
  { text: "Event log", href: "/eventlog" },
313
313
  ...(isRoot ? [{ text: "Crash log", href: "/crashlog" }] : []),
314
314
  ],
package/package.json CHANGED
@@ -1,20 +1,20 @@
1
1
  {
2
2
  "name": "@saltcorn/server",
3
- "version": "0.9.8-rc.0",
3
+ "version": "1.0.0-beta.0",
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
9
  "@aws-sdk/client-s3": "^3.451.0",
10
- "@saltcorn/base-plugin": "0.9.8-rc.0",
11
- "@saltcorn/builder": "0.9.8-rc.0",
12
- "@saltcorn/data": "0.9.8-rc.0",
13
- "@saltcorn/admin-models": "0.9.8-rc.0",
14
- "@saltcorn/filemanager": "0.9.8-rc.0",
15
- "@saltcorn/markup": "0.9.8-rc.0",
16
- "@saltcorn/plugins-loader": "0.9.8-rc.0",
17
- "@saltcorn/sbadmin2": "0.9.8-rc.0",
10
+ "@saltcorn/base-plugin": "1.0.0-beta.0",
11
+ "@saltcorn/builder": "1.0.0-beta.0",
12
+ "@saltcorn/data": "1.0.0-beta.0",
13
+ "@saltcorn/admin-models": "1.0.0-beta.0",
14
+ "@saltcorn/filemanager": "1.0.0-beta.0",
15
+ "@saltcorn/markup": "1.0.0-beta.0",
16
+ "@saltcorn/plugins-loader": "1.0.0-beta.0",
17
+ "@saltcorn/sbadmin2": "1.0.0-beta.0",
18
18
  "@socket.io/cluster-adapter": "^0.2.1",
19
19
  "@socket.io/sticky": "^1.0.1",
20
20
  "adm-zip": "0.5.10",
@@ -586,3 +586,7 @@ button.monospace-copy-btn {
586
586
  border-right: 1px #e3e6f0 !important;
587
587
  border-radius: 0 6px 6px 0;
588
588
  }
589
+
590
+ .custom-file-label {
591
+ margin-left: 10px;
592
+ }
package/routes/admin.js CHANGED
@@ -109,6 +109,7 @@ const Crash = require("@saltcorn/data/models/crash");
109
109
  const { get_help_markup } = require("../help/index.js");
110
110
  const Docker = require("dockerode");
111
111
  const npmFetch = require("npm-registry-fetch");
112
+ const Tag = require("@saltcorn/data/models/tag");
112
113
 
113
114
  const router = new Router();
114
115
  module.exports = router;
@@ -3226,7 +3227,8 @@ router.post(
3226
3227
  const userfields1 = await users1.getFields();
3227
3228
 
3228
3229
  for (const f of userfields1) {
3229
- if (f.name !== "email" && f.name !== "id") await f.delete();
3230
+ if (f.name !== "email" && f.name !== "id" && f.name !== "role_id")
3231
+ await f.delete();
3230
3232
  }
3231
3233
  await db.deleteWhere("users");
3232
3234
  await db.deleteWhere("_sc_roles", {
@@ -3451,7 +3453,64 @@ router.get(
3451
3453
  },
3452
3454
  ],
3453
3455
  });
3454
-
3456
+ const function_code_pages_tags = getState().getConfigCopy(
3457
+ "function_code_pages_tags",
3458
+ {}
3459
+ );
3460
+ const tags = await Tag.find();
3461
+ const tagMarkup = div(
3462
+ "Tags:",
3463
+ (function_code_pages_tags[name] || []).map((tagnm) =>
3464
+ span(
3465
+ {
3466
+ class: ["ms-2 badge bg-secondary"],
3467
+ },
3468
+ tagnm,
3469
+ a(
3470
+ {
3471
+ onclick: `rm_cp_tag('${tagnm}')`,
3472
+ },
3473
+ i({ class: "ms-1 fas fa-lg fa-times" })
3474
+ )
3475
+ )
3476
+ ),
3477
+ span(
3478
+ { class: "dropdown", id: `ddcodetags` },
3479
+ span(
3480
+ {
3481
+ class: ["ms-2 badge", "bg-secondary", "dropdown-toggle"],
3482
+ "data-bs-toggle": "dropdown",
3483
+ "aria-haspopup": "true",
3484
+ "aria-expanded": "false",
3485
+ },
3486
+ i({ class: "fas fa-lg fa-plus" })
3487
+ ),
3488
+ div(
3489
+ { class: "dropdown-menu", "aria-labelledby": "ddcodetags" },
3490
+ tags
3491
+ .map((t) =>
3492
+ a(
3493
+ {
3494
+ class: "dropdown-item",
3495
+ onclick: `add_cp_tag('${t.name}')`,
3496
+ },
3497
+ t.name
3498
+ )
3499
+ )
3500
+ .join("")
3501
+ )
3502
+ ),
3503
+ script(`function add_cp_tag(nm) {
3504
+ ajax_post("/admin/add-codepage-tag/${encodeURIComponent(
3505
+ name
3506
+ )}/"+encodeURIComponent(nm))
3507
+ }
3508
+ function rm_cp_tag(nm) {
3509
+ ajax_post("/admin/rm-codepage-tag/${encodeURIComponent(
3510
+ name
3511
+ )}/"+encodeURIComponent(nm))
3512
+ }`)
3513
+ );
3455
3514
  send_admin_page({
3456
3515
  res,
3457
3516
  req,
@@ -3460,7 +3519,7 @@ router.get(
3460
3519
  contents: {
3461
3520
  type: "card",
3462
3521
  title: req.__(`%s code page`, name),
3463
- contents: [renderForm(form, req.csrfToken())],
3522
+ contents: [renderForm(form, req.csrfToken()), tagMarkup],
3464
3523
  },
3465
3524
  });
3466
3525
  })
@@ -3496,7 +3555,50 @@ router.post(
3496
3555
  res.json({ goto: `/admin/dev` });
3497
3556
  })
3498
3557
  );
3558
+ router.post(
3559
+ "/add-codepage-tag/:cpname/:tagnm",
3560
+ isAdmin,
3561
+ error_catcher(async (req, res) => {
3562
+ const { cpname, tagnm } = req.params;
3563
+ const function_code_pages_tags = getState().getConfigCopy(
3564
+ "function_code_pages_tags",
3565
+ {}
3566
+ );
3567
+
3568
+ function_code_pages_tags[cpname] = [
3569
+ ...(function_code_pages_tags[cpname] || []),
3570
+ tagnm,
3571
+ ];
3572
+ await getState().setConfig(
3573
+ "function_code_pages_tags",
3574
+ function_code_pages_tags
3575
+ );
3576
+
3577
+ res.json({ reload_page: true });
3578
+ })
3579
+ );
3580
+ router.post(
3581
+ "/rm-codepage-tag/:cpname/:tagnm",
3582
+ isAdmin,
3583
+ error_catcher(async (req, res) => {
3584
+ const { cpname, tagnm } = req.params;
3585
+ const function_code_pages_tags = getState().getConfigCopy(
3586
+ "function_code_pages_tags",
3587
+ {}
3588
+ );
3589
+
3590
+ function_code_pages_tags[cpname] = (
3591
+ function_code_pages_tags[cpname] || []
3592
+ ).filter((t) => t != tagnm);
3499
3593
 
3594
+ await getState().setConfig(
3595
+ "function_code_pages_tags",
3596
+ function_code_pages_tags
3597
+ );
3598
+
3599
+ res.json({ reload_page: true });
3600
+ })
3601
+ );
3500
3602
  /**
3501
3603
  * Notifications
3502
3604
  */
package/routes/api.js CHANGED
@@ -380,8 +380,11 @@ router.all(
380
380
  else if (req.headers?.scgotourl)
381
381
  res.redirect(req.headers?.scgotourl);
382
382
  else {
383
- if (trigger.configuration?._raw_output) res.json({ resp });
384
- else res.json({ success: true, data: resp });
383
+ if (trigger.configuration?._raw_output) res.json(resp);
384
+ else if (resp?.error) {
385
+ const { error, ...rest } = resp;
386
+ res.json({ success: false, error, data: rest });
387
+ } else res.json({ success: true, data: resp });
385
388
  }
386
389
  } catch (e) {
387
390
  Crash.create(e, req);
@@ -50,8 +50,41 @@ const EventLog = require("@saltcorn/data/models/eventlog");
50
50
  * @param {object} req
51
51
  * @returns {Promise<Form>}
52
52
  */
53
+
53
54
  const logSettingsForm = async (req) => {
54
- const fields = [];
55
+ const hoursFuture = (nhrs) => {
56
+ const t = new Date();
57
+ t.setHours(t.getHours() + nhrs);
58
+ return t;
59
+ };
60
+ const fields = [
61
+ {
62
+ input_type: "section_header",
63
+ label: req.__("Periodic trigger timing (next event)"),
64
+ },
65
+ {
66
+ name: "next_hourly_event",
67
+ label: req.__("Hourly"),
68
+ input_type: "date",
69
+ attributes: { minDate: new Date(), maxDate: hoursFuture(2) },
70
+ },
71
+ {
72
+ name: "next_daily_event",
73
+ label: req.__("Daily"),
74
+ input_type: "date",
75
+ attributes: { minDate: new Date(), maxDate: hoursFuture(48) },
76
+ },
77
+ {
78
+ name: "next_weekly_event",
79
+ label: req.__("Weekly"),
80
+ input_type: "date",
81
+ attributes: { minDate: new Date(), maxDate: hoursFuture(24 * 7 * 2) },
82
+ },
83
+ {
84
+ input_type: "section_header",
85
+ label: req.__("Which events should be logged?"),
86
+ },
87
+ ];
55
88
  for (const w of Trigger.when_options) {
56
89
  fields.push({
57
90
  name: w,
@@ -82,7 +115,6 @@ const logSettingsForm = async (req) => {
82
115
  }
83
116
  return new Form({
84
117
  action: "/eventlog/settings",
85
- blurb: req.__("Which events should be logged?"),
86
118
  noSubmitButton: true,
87
119
  onChange: "saveAndContinue(this)",
88
120
  fields,
@@ -101,15 +133,25 @@ router.get(
101
133
  error_catcher(async (req, res) => {
102
134
  const form = await logSettingsForm(req);
103
135
  form.values = getState().getConfig("event_log_settings", {});
136
+ form.values.next_hourly_event = getState().getConfig(
137
+ "next_hourly_event",
138
+ {}
139
+ );
140
+ form.values.next_daily_event = getState().getConfig("next_daily_event", {});
141
+ form.values.next_weekly_event = getState().getConfig(
142
+ "next_weekly_event",
143
+ {}
144
+ );
145
+
104
146
  send_events_page({
105
147
  res,
106
148
  req,
107
- active_sub: "Log settings",
149
+ active_sub: "Settings",
108
150
  //sub2_page: "Events to log",
109
151
  contents: {
110
152
  type: "card",
111
153
  titleAjaxIndicator: true,
112
- title: req.__("Events to log"),
154
+ title: req.__("Events and Trigger settings"),
113
155
  contents: renderForm(form, req.csrfToken()),
114
156
  },
115
157
  });
@@ -289,7 +331,7 @@ router.post(
289
331
  send_events_page({
290
332
  res,
291
333
  req,
292
- active_sub: "Log settings",
334
+ active_sub: "Settings",
293
335
  //sub2_page: "Events to log",
294
336
  contents: {
295
337
  type: "card",
@@ -298,6 +340,14 @@ router.post(
298
340
  },
299
341
  });
300
342
  } else {
343
+ for (const tm of ["hourly", "daily", "weekly"]) {
344
+ const k = `next_${tm}_event`;
345
+ if (form.values[k]) {
346
+ await getState().setConfig(k, form.values[k]);
347
+ delete form.values[k];
348
+ }
349
+ }
350
+
301
351
  await getState().setConfig("event_log_settings", form.values);
302
352
 
303
353
  if (!req.xhr) res.redirect(`/eventlog/settings`);
package/routes/tags.js CHANGED
@@ -1,4 +1,4 @@
1
- const { a, text, i } = require("@saltcorn/markup/tags");
1
+ const { a, text, i, div } = require("@saltcorn/markup/tags");
2
2
 
3
3
  const Tag = require("@saltcorn/data/models/tag");
4
4
  const Router = require("express-promise-router");
@@ -171,6 +171,13 @@ router.get(
171
171
  const viewsDomId = "viewsListId";
172
172
  const pagesDomId = "pagesDomId";
173
173
  const triggersDomId = "triggerDomId";
174
+ const function_code_pages_tags = getState().getConfigCopy(
175
+ "function_code_pages_tags",
176
+ {}
177
+ );
178
+ const code_pages = Object.entries(function_code_pages_tags)
179
+ .filter(([nm, tags]) => (tags || []).includes(tag.name))
180
+ .map(([nm, tags]) => nm);
174
181
  res.sendWrap(req.__("%s Tag", tag.name), {
175
182
  above: [
176
183
  {
@@ -270,6 +277,20 @@ router.get(
270
277
  ),
271
278
  ],
272
279
  },
280
+ {
281
+ type: "card",
282
+ title: req.__("Code pages") + ` (${code_pages.length})`,
283
+
284
+ contents: code_pages.map((cp) =>
285
+ a(
286
+ {
287
+ class: "me-2",
288
+ href: `/admin/edit-codepage/${encodeURIComponent(cp)}`,
289
+ },
290
+ cp
291
+ )
292
+ ),
293
+ },
273
294
  {
274
295
  type: "card",
275
296
  contents: [
package/routes/utils.js CHANGED
@@ -239,8 +239,14 @@ const setTenant = (req, res, next) => {
239
239
  }
240
240
  }
241
241
  } else {
242
- setLanguage(req, res);
243
- getState().log(5, `${req.method} ${req.originalUrl}`);
242
+ const state = getState();
243
+ setLanguage(req, res, state);
244
+ state.log(
245
+ 5,
246
+ `${req.method} ${req.originalUrl}${
247
+ state.getConfig("log_ip_address", false) ? ` IP=${req.ip}` : ""
248
+ }`
249
+ );
244
250
  next();
245
251
  }
246
252
  };
@@ -18,6 +18,11 @@ const fs = require("fs").promises;
18
18
  const File = require("@saltcorn/data/models/file");
19
19
  const User = require("@saltcorn/data/models/user");
20
20
  const EventLog = require("@saltcorn/data/models/eventlog");
21
+ const {
22
+ create_backup,
23
+ restore,
24
+ auto_backup_now,
25
+ } = require("@saltcorn/admin-models/models/backup");
21
26
 
22
27
  jest.setTimeout(30000);
23
28
 
@@ -615,6 +620,11 @@ describe("server logs viewer", () => {
615
620
  */
616
621
  describe("clear all page", () => {
617
622
  itShouldRedirectUnauthToLogin("/admin/clear-all");
623
+ let backup_fnm;
624
+ it("backs up before clear all", async () => {
625
+ backup_fnm = await create_backup();
626
+ });
627
+
618
628
  it("show page", async () => {
619
629
  const app = await getApp({ disableCsrf: true });
620
630
  const loginCookie = await getAdminLoginCookie();
@@ -639,4 +649,11 @@ describe("clear all page", () => {
639
649
  .send("plugins=on")
640
650
  .expect(toRedirect("/auth/create_first_user"));
641
651
  });
652
+ it("restores backup after clear all", async () => {
653
+ const restore_res = await restore(backup_fnm, (p) => {}, true);
654
+ await fs.unlink(backup_fnm);
655
+
656
+ if (restore_res) console.log("rr", restore_res);
657
+ expect(!!restore_res).toBe(false);
658
+ });
642
659
  });
package/tests/api.test.js CHANGED
@@ -376,6 +376,34 @@ describe("API action", () => {
376
376
  `,
377
377
  },
378
378
  });
379
+ await Trigger.create({
380
+ action: "run_js_code",
381
+ when_trigger: "API call",
382
+ name: "apicallraw",
383
+ min_role: 100,
384
+ configuration: {
385
+ code: `return {studio: 54}`,
386
+ _raw_output: true,
387
+ },
388
+ });
389
+ await Trigger.create({
390
+ action: "run_js_code",
391
+ when_trigger: "API call",
392
+ name: "apicallerror",
393
+ min_role: 100,
394
+ configuration: {
395
+ code: `return {error: "bad"}`,
396
+ },
397
+ });
398
+ await Trigger.create({
399
+ action: "run_js_code",
400
+ when_trigger: "API call",
401
+ name: "apicallundef",
402
+ min_role: 100,
403
+ configuration: {
404
+ code: `return;`,
405
+ },
406
+ });
379
407
  });
380
408
  it("should POST to trigger", async () => {
381
409
  const app = await getApp({ disableCsrf: true });
@@ -386,12 +414,44 @@ describe("API action", () => {
386
414
  })
387
415
  .set("Content-Type", "application/json")
388
416
  .set("Accept", "application/json")
389
- .expect(succeedJsonWithWholeBody((resp) => resp?.data?.studio === 54));
417
+ .expect(
418
+ succeedJsonWithWholeBody(
419
+ (resp) => resp?.data?.studio === 54 && resp.success === true
420
+ )
421
+ );
390
422
  const table = Table.findOne({ name: "triggercounter" });
391
423
  const counts = await table.getRows({});
392
424
  expect(counts.map((c) => c.thing)).toContain("inthebody");
393
425
  expect(counts.map((c) => c.thing)).not.toContain("no body");
394
426
  });
427
+ it("should POST to raw trigger", async () => {
428
+ const app = await getApp({ disableCsrf: true });
429
+ await request(app)
430
+ .post("/api/action/apicallraw")
431
+ .set("Content-Type", "application/json")
432
+ .set("Accept", "application/json")
433
+ .expect(succeedJsonWithWholeBody((resp) => resp?.studio === 54));
434
+ });
435
+ it("should POST to raw trigger", async () => {
436
+ const app = await getApp({ disableCsrf: true });
437
+ await request(app)
438
+ .post("/api/action/apicallerror")
439
+ .set("Content-Type", "application/json")
440
+ .set("Accept", "application/json")
441
+ .expect(
442
+ succeedJsonWithWholeBody(
443
+ (resp) => resp?.error === "bad" && resp.success === false
444
+ )
445
+ );
446
+ });
447
+ it("should POST to undefiend trigger", async () => {
448
+ const app = await getApp({ disableCsrf: true });
449
+ await request(app)
450
+ .post("/api/action/apicallundef")
451
+ .set("Content-Type", "application/json")
452
+ .set("Accept", "application/json")
453
+ .expect(succeedJsonWithWholeBody((resp) => resp.success === true));
454
+ });
395
455
  it("should GET with query to trigger", async () => {
396
456
  const app = await getApp({ disableCsrf: true });
397
457
  await request(app)
@@ -187,7 +187,7 @@ describe("files admin", () => {
187
187
  const app = await getApp({ disableCsrf: true });
188
188
  const loginCookie = await getAdminLoginCookie();
189
189
  const checkFiles = (files, expecteds) =>
190
- files.length === expecteds.length &&
190
+ files.length >= expecteds.length &&
191
191
  expecteds.every(({ filename, location }) =>
192
192
  files.find(
193
193
  (file) => file.filename === filename && file.location === location