@saltcorn/server 0.7.3-beta.6 → 0.7.4-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.
@@ -90,6 +90,13 @@ const page_dropdown = (page, req) =>
90
90
  '<i class="far fa-copy"></i>&nbsp;' + req.__("Duplicate"),
91
91
  req
92
92
  ),
93
+ a(
94
+ {
95
+ class: "dropdown-item",
96
+ href: `javascript:ajax_modal('/admin/snapshot-restore/page/${page.name}')`,
97
+ },
98
+ '<i class="fas fa-undo-alt"></i>&nbsp;' + req.__("Restore")
99
+ ),
93
100
  div({ class: "dropdown-divider" }),
94
101
  post_dropdown_item(
95
102
  `/pageedit/delete/${page.id}`,
@@ -180,7 +187,8 @@ const pageBuilderData = async (req, context) => {
180
187
  const fixed_state_fields = {};
181
188
  for (const view of views) {
182
189
  fixed_state_fields[view.name] = [];
183
- const table = Table.findOne({ id: view.table_id });
190
+ const table = Table.findOne(view.table_id || view.exttable_name);
191
+
184
192
  const fs = await view.get_state_fields();
185
193
  for (const frec of fs) {
186
194
  const f = new Field(frec);
package/routes/plugins.js CHANGED
@@ -561,12 +561,30 @@ router.get(
561
561
  }
562
562
  const flow = module.configuration_workflow();
563
563
  flow.action = `/plugins/configure/${encodeURIComponent(plugin.name)}`;
564
+ flow.autoSave = true;
565
+ flow.saveURL = `/plugins/saveconfig/${encodeURIComponent(plugin.name)}`;
564
566
  const wfres = await flow.run(plugin.configuration || {});
567
+ if (module.layout) {
568
+ wfres.renderForm.additionalButtons = [
569
+ ...(wfres.renderForm.additionalButtons || []),
570
+ {
571
+ label: "Reload page to see changes",
572
+ id: "btnReloadNow",
573
+ class: "btn btn-outline-secondary",
574
+ onclick: "location.reload()",
575
+ },
576
+ ];
577
+ wfres.renderForm.onChange = `${
578
+ wfres.renderForm.onChange || ""
579
+ };$('#btnReloadNow').removeClass('btn-outline-secondary').addClass('btn-secondary')`;
580
+ }
565
581
 
566
- res.sendWrap(
567
- req.__(`Configure %s Plugin`, plugin.name),
568
- renderForm(wfres.renderForm, req.csrfToken())
569
- );
582
+ res.sendWrap(req.__(`Configure %s Plugin`, plugin.name), {
583
+ type: "card",
584
+ class: "mt-0",
585
+ title: req.__(`Configure %s Plugin`, plugin.name),
586
+ contents: renderForm(wfres.renderForm, req.csrfToken()),
587
+ });
570
588
  })
571
589
  );
572
590
 
@@ -588,13 +606,31 @@ router.post(
588
606
  }
589
607
  const flow = module.configuration_workflow();
590
608
  flow.action = `/plugins/configure/${encodeURIComponent(plugin.name)}`;
609
+ flow.autoSave = true;
610
+ flow.saveURL = `/plugins/saveconfig/${encodeURIComponent(plugin.name)}`;
591
611
  const wfres = await flow.run(req.body);
592
- if (wfres.renderForm)
593
- res.sendWrap(
594
- req.__(`Configure %s Plugin`, plugin.name),
595
- renderForm(wfres.renderForm, req.csrfToken())
596
- );
597
- else {
612
+ if (wfres.renderForm) {
613
+ if (module.layout) {
614
+ wfres.renderForm.additionalButtons = [
615
+ ...(wfres.renderForm.additionalButtons || []),
616
+ {
617
+ label: "Reload page to see changes",
618
+ id: "btnReloadNow",
619
+ class: "btn btn-outline-secondary",
620
+ onclick: "location.reload()",
621
+ },
622
+ ];
623
+ wfres.renderForm.onChange = `${
624
+ wfres.renderForm.onChange || ""
625
+ };$('#btnReloadNow').removeClass('btn-outline-secondary').addClass('btn-secondary')`;
626
+ }
627
+ res.sendWrap(req.__(`Configure %s Plugin`, plugin.name), {
628
+ type: "card",
629
+ class: "mt-0",
630
+ title: req.__(`Configure %s Plugin`, plugin.name),
631
+ contents: renderForm(wfres.renderForm, req.csrfToken()),
632
+ });
633
+ } else {
598
634
  plugin.configuration = wfres;
599
635
  await plugin.upsert();
600
636
  await load_plugins.loadPlugin(plugin);
@@ -606,12 +642,42 @@ router.post(
606
642
  refresh_plugin_cfg: plugin.name,
607
643
  tenant: db.getTenantSchema(),
608
644
  });
609
- await sleep(500); // Allow other workers to reload this plugin
645
+ if (module.layout) await sleep(500); // Allow other workers to reload this plugin
610
646
  res.redirect("/plugins");
611
647
  }
612
648
  })
613
649
  );
614
650
 
651
+ router.post(
652
+ "/saveconfig/:name",
653
+ isAdmin,
654
+ error_catcher(async (req, res) => {
655
+ const { name } = req.params;
656
+ const plugin = await Plugin.findOne({ name: decodeURIComponent(name) });
657
+ let module = getState().plugins[plugin.name];
658
+ if (!module) {
659
+ module = getState().plugins[getState().plugin_module_names[plugin.name]];
660
+ }
661
+ const flow = module.configuration_workflow();
662
+ const step = await flow.singleStepForm(req.body, req);
663
+ if (step?.renderForm) {
664
+ if (!step.renderForm.hasErrors) {
665
+ plugin.configuration = {
666
+ ...plugin.configuration,
667
+ ...step.renderForm.values,
668
+ };
669
+ await plugin.upsert();
670
+ await load_plugins.loadPlugin(plugin);
671
+ process.send &&
672
+ process.send({
673
+ refresh_plugin_cfg: plugin.name,
674
+ tenant: db.getTenantSchema(),
675
+ });
676
+ res.json({ success: "ok" });
677
+ }
678
+ }
679
+ })
680
+ );
615
681
  /**
616
682
  * @name get/new
617
683
  * @function
package/routes/search.js CHANGED
@@ -55,7 +55,8 @@ const searchConfigForm = (tables, views, req) => {
55
55
  );
56
56
  return new Form({
57
57
  action: "/search/config",
58
- submitLabel: req.__("Save"),
58
+ noSubmitButton: true,
59
+ onChange: `saveAndContinue(this)`,
59
60
  blurb:
60
61
  blurb1 +
61
62
  (tbls_noviews.length > 0
@@ -111,7 +112,8 @@ router.post(
111
112
 
112
113
  if (result.success) {
113
114
  await getState().setConfig("globalSearch", result.success);
114
- res.redirect("/search/config");
115
+ if (!req.xhr) res.redirect("/search/config");
116
+ else res.json({ success: "ok" });
115
117
  } else {
116
118
  send_infoarch_page({
117
119
  res,
package/routes/tables.js CHANGED
@@ -83,8 +83,8 @@ const tableForm = async (table, req) => {
83
83
  .map((f) => ({ value: f.id, label: f.name }));
84
84
  const form = new Form({
85
85
  action: "/table",
86
- submitButtonClass: "btn-outline-primary",
87
- onChange: "remove_outline(this)",
86
+ noSubmitButton: true,
87
+ onChange: "saveAndContinue(this)",
88
88
  fields: [
89
89
  ...(!table.external
90
90
  ? [
@@ -910,7 +910,8 @@ router.post(
910
910
  );
911
911
  else if (!hasError) req.flash("success", req.__("Table saved"));
912
912
 
913
- res.redirect(`/table/${id}`);
913
+ if (!req.xhr) res.redirect(`/table/${id}`);
914
+ else res.json({ success: "ok" });
914
915
  }
915
916
  })
916
917
  );
package/routes/tenant.js CHANGED
@@ -452,8 +452,10 @@ router.post(
452
452
  } else {
453
453
  await save_config_from_form(form);
454
454
 
455
- req.flash("success", req.__("Tenant settings updated"));
456
- res.redirect("/tenant/settings");
455
+ if (!req.xhr) {
456
+ req.flash("success", req.__("Tenant settings updated"));
457
+ res.redirect("/tenant/settings");
458
+ } else res.json({ success: "ok" });
457
459
  }
458
460
  })
459
461
  );
package/routes/utils.js CHANGED
@@ -128,6 +128,7 @@ const setTenant = (req, res, next) => {
128
128
  } else {
129
129
  db.runWithTenant(other_domain, () => {
130
130
  setLanguage(req, res, state);
131
+ state.log(5, `${req.method} ${req.originalUrl}`);
131
132
  next();
132
133
  });
133
134
  }
@@ -140,12 +141,14 @@ const setTenant = (req, res, next) => {
140
141
  } else {
141
142
  db.runWithTenant(ten, () => {
142
143
  setLanguage(req, res, state);
144
+ state.log(5, `${req.method} ${req.originalUrl}`);
143
145
  next();
144
146
  });
145
147
  }
146
148
  }
147
149
  } else {
148
150
  setLanguage(req, res);
151
+ getState().log(5, `${req.method} ${req.originalUrl}`);
149
152
  next();
150
153
  }
151
154
  };
@@ -240,4 +243,5 @@ module.exports = {
240
243
  getGitRevision,
241
244
  getSessionStore,
242
245
  setTenant,
246
+ get_tenant_from_req
243
247
  };
package/routes/view.js CHANGED
@@ -20,6 +20,7 @@ const {
20
20
  } = require("../routes/utils.js");
21
21
  const { add_edit_bar } = require("../markup/admin.js");
22
22
  const { InvalidConfiguration } = require("@saltcorn/data/utils");
23
+ const { getState } = require("@saltcorn/data/db/state");
23
24
 
24
25
  /**
25
26
  * @type {object}
@@ -44,8 +45,11 @@ router.get(
44
45
  const query = { ...req.query };
45
46
  const view = await View.findOne({ name: viewname });
46
47
  const role = req.user && req.user.id ? req.user.role_id : 10;
48
+ const state = getState();
49
+ state.log(3, `Route /view/${viewname} user=${req.user?.id}`);
47
50
  if (!view) {
48
51
  req.flash("danger", req.__(`No such view: %s`, text(viewname)));
52
+ state.log(2, `View ${viewname} not found`);
49
53
  res.redirect("/");
50
54
  return;
51
55
  }
@@ -56,6 +60,7 @@ router.get(
56
60
  !(await view.authorise_get({ query, req, ...view }))
57
61
  ) {
58
62
  req.flash("danger", req.__("Not authorized"));
63
+ state.log(2, `View ${viewname} not authorized`);
59
64
  res.redirect("/");
60
65
  return;
61
66
  }
@@ -123,13 +128,21 @@ router.post(
123
128
  error_catcher(async (req, res) => {
124
129
  const { viewname, route } = req.params;
125
130
  const role = req.user && req.user.id ? req.user.role_id : 10;
131
+ const state = getState();
132
+ state.log(
133
+ 3,
134
+ `Route /view/${viewname} viewroute ${route} user=${req.user?.id}`
135
+ );
126
136
 
127
137
  const view = await View.findOne({ name: viewname });
128
138
  if (!view) {
129
139
  req.flash("danger", req.__(`No such view: %s`, text(viewname)));
140
+ state.log(2, `View ${viewname} not found`);
130
141
  res.redirect("/");
131
142
  } else if (role > view.min_role) {
132
143
  req.flash("danger", req.__("Not authorized"));
144
+ state.log(2, `View ${viewname} viewroute ${route} not authorized`);
145
+
133
146
  res.redirect("/");
134
147
  } else {
135
148
  await view.runRoute(route, req.body, res, { res, req });
@@ -150,10 +163,12 @@ router.post(
150
163
  const { viewname } = req.params;
151
164
  const role = req.user && req.user.id ? req.user.role_id : 10;
152
165
  const query = { ...req.query };
153
-
166
+ const state = getState();
167
+ state.log(3, `Route /view/${viewname} POST user=${req.user?.id}`);
154
168
  const view = await View.findOne({ name: viewname });
155
169
  if (!view) {
156
170
  req.flash("danger", req.__(`No such view: %s`, text(viewname)));
171
+ state.log(2, `View ${viewname} not found`);
157
172
  res.redirect("/");
158
173
  return;
159
174
  }
@@ -164,6 +179,8 @@ router.post(
164
179
  !(await view.authorise_post({ body: req.body, req, ...view }))
165
180
  ) {
166
181
  req.flash("danger", req.__("Not authorized"));
182
+ state.log(2, `View ${viewname} POST not authorized`);
183
+
167
184
  res.redirect("/");
168
185
  } else if (!view.runPost) {
169
186
  throw new InvalidConfiguration(
@@ -98,6 +98,13 @@ const view_dropdown = (view, req) =>
98
98
  '<i class="far fa-copy"></i>&nbsp;' + req.__("Duplicate"),
99
99
  req
100
100
  ),
101
+ a(
102
+ {
103
+ class: "dropdown-item",
104
+ href: `javascript:ajax_modal('/admin/snapshot-restore/view/${view.name}')`,
105
+ },
106
+ '<i class="fas fa-undo-alt"></i>&nbsp;' + req.__("Restore")
107
+ ),
101
108
  div({ class: "dropdown-divider" }),
102
109
  post_dropdown_item(
103
110
  `/viewedit/delete/${view.id}`,
@@ -355,7 +362,7 @@ router.get(
355
362
  }
356
363
  const tables = await Table.find_with_external();
357
364
  const currentTable = tables.find(
358
- (t) => t.id === viewrow.table_id || t.name === viewrow.exttable_name
365
+ (t) => (t.id && t.id === viewrow.table_id) || t.name === viewrow.exttable_name
359
366
  );
360
367
  viewrow.table_name = currentTable && currentTable.name;
361
368
  if (viewrow.slug && currentTable) {
@@ -380,7 +387,12 @@ router.get(
380
387
  {
381
388
  type: "card",
382
389
  class: "mt-0",
383
- title: req.__(`Edit %s view`, viewname),
390
+ title: req.__(
391
+ `%s view - %s on %s`,
392
+ viewname,
393
+ viewrow.viewtemplate,
394
+ viewrow.table_name
395
+ ),
384
396
  contents: renderForm(form, req.csrfToken()),
385
397
  },
386
398
  ],
@@ -579,7 +591,7 @@ router.get(
579
591
  isAdmin,
580
592
  error_catcher(async (req, res) => {
581
593
  const { name } = req.params;
582
-
594
+ const { step } = req.query;
583
595
  const view = await View.findOne({ name });
584
596
  if (!view) {
585
597
  req.flash("error", `View not found: ${text(name)}`);
@@ -596,6 +608,7 @@ router.get(
596
608
  table_id: view.table_id,
597
609
  exttable_name: view.exttable_name,
598
610
  viewname: name,
611
+ ...(step ? { stepName: step } : {}),
599
612
  },
600
613
  req
601
614
  );
package/serve.js CHANGED
@@ -30,7 +30,7 @@ const { getConfig } = require("@saltcorn/data/models/config");
30
30
  const { migrate } = require("@saltcorn/data/migrate");
31
31
  const socketio = require("socket.io");
32
32
  const { createAdapter, setupPrimary } = require("@socket.io/cluster-adapter");
33
- const { setTenant, getSessionStore } = require("./routes/utils");
33
+ const { setTenant, getSessionStore, get_tenant_from_req } = require("./routes/utils");
34
34
  const passport = require("passport");
35
35
  const { authenticate } = require("passport");
36
36
  const View = require("@saltcorn/data/models/view");
@@ -44,6 +44,11 @@ const {
44
44
  getAllTenants,
45
45
  } = require("@saltcorn/admin-models/models/tenant");
46
46
  const { auto_backup_now } = require("@saltcorn/admin-models/models/backup");
47
+ const Snapshot = require("@saltcorn/admin-models/models/snapshot");
48
+
49
+ const take_snapshot = async () => {
50
+ return await Snapshot.take_if_changed();
51
+ };
47
52
 
48
53
  // helpful https://gist.github.com/jpoehls/2232358
49
54
  /**
@@ -132,32 +137,33 @@ const workerDispatchMsg = ({ tenant, ...msg }) => {
132
137
  */
133
138
  const onMessageFromWorker =
134
139
  (masterState, { port, watchReaper, disableScheduler, pid }) =>
135
- (msg) => {
136
- //console.log("worker msg", typeof msg, msg);
137
- if (msg === "Start" && !masterState.started) {
138
- masterState.started = true;
139
- runScheduler({
140
- port,
141
- watchReaper,
142
- disableScheduler,
143
- eachTenant,
144
- auto_backup_now,
145
- });
146
- require("./systemd")({ port });
147
- return true;
148
- } else if (msg === "RestartServer") {
149
- process.exit(0);
150
- return true;
151
- } else if (msg.tenant || msg.createTenant) {
152
- ///ie from saltcorn
153
- //broadcast
154
- Object.entries(cluster.workers).forEach(([wpid, w]) => {
155
- if (wpid !== pid) w.send(msg);
156
- });
157
- workerDispatchMsg(msg); //also master
158
- return true;
159
- }
160
- };
140
+ (msg) => {
141
+ //console.log("worker msg", typeof msg, msg);
142
+ if (msg === "Start" && !masterState.started) {
143
+ masterState.started = true;
144
+ runScheduler({
145
+ port,
146
+ watchReaper,
147
+ disableScheduler,
148
+ eachTenant,
149
+ auto_backup_now,
150
+ take_snapshot,
151
+ });
152
+ require("./systemd")({ port });
153
+ return true;
154
+ } else if (msg === "RestartServer") {
155
+ process.exit(0);
156
+ return true;
157
+ } else if (msg.tenant || msg.createTenant) {
158
+ ///ie from saltcorn
159
+ //broadcast
160
+ Object.entries(cluster.workers).forEach(([wpid, w]) => {
161
+ if (wpid !== pid) w.send(msg);
162
+ });
163
+ workerDispatchMsg(msg); //also master
164
+ return true;
165
+ }
166
+ };
161
167
 
162
168
  module.exports =
163
169
  /**
@@ -274,6 +280,7 @@ module.exports =
274
280
  disableScheduler,
275
281
  eachTenant,
276
282
  auto_backup_now,
283
+ take_snapshot,
277
284
  });
278
285
  }
279
286
  Trigger.emitEvent("Startup");
@@ -345,24 +352,33 @@ const setupSocket = (...servers) => {
345
352
  io.attach(server);
346
353
  }
347
354
 
348
- io.use(wrap(setTenant));
355
+ //io.use(wrap(setTenant));
349
356
  io.use(wrap(getSessionStore()));
350
357
  io.use(wrap(passport.initialize()));
351
358
  io.use(wrap(passport.authenticate(["jwt", "session"])));
352
359
  if (process.send && !cluster.isMaster) io.adapter(createAdapter());
353
- getState().setRoomEmitter((viewname, room_id, msg) => {
354
- io.to(`${viewname}_${room_id}`).emit("message", msg);
360
+ getState().setRoomEmitter((tenant, viewname, room_id, msg) => {
361
+ io.to(`${tenant}_${viewname}_${room_id}`).emit("message", msg);
355
362
  });
356
363
  io.on("connection", (socket) => {
357
364
  socket.on("join_room", ([viewname, room_id]) => {
358
- const view = View.findOne({ name: viewname });
359
- if (view.viewtemplateObj.authorize_join) {
360
- view.viewtemplateObj
361
- .authorize_join(view.configuration, room_id, socket.request.user)
362
- .then((authorized) => {
363
- if (authorized) socket.join(`${viewname}_${room_id}`);
364
- });
365
- } else socket.join(`${viewname}_${room_id}`);
365
+ const ten = get_tenant_from_req(socket.request) || "public";
366
+ const f = () => {
367
+ try {
368
+ const view = View.findOne({ name: viewname });
369
+ if (view.viewtemplateObj.authorize_join) {
370
+ view.viewtemplateObj
371
+ .authorize_join(view.configuration, room_id, socket.request.user)
372
+ .then((authorized) => {
373
+ if (authorized) socket.join(`${ten}_${viewname}_${room_id}`);
374
+ });
375
+ } else socket.join(`${ten}_${viewname}_${room_id}`);
376
+ } catch (err) {
377
+ getState().log(1, `Socket join_room error: ${err.stack}`);
378
+ }
379
+ }
380
+ if (ten && ten !== "public") db.runWithTenant(ten, f);
381
+ else f();
366
382
  });
367
383
  });
368
384
  };
package/tests/api.test.js CHANGED
@@ -84,6 +84,23 @@ describe("API read", () => {
84
84
  )
85
85
  );
86
86
  });
87
+ it("should handle fkey args ", async () => {
88
+ const loginCookie = await getAdminLoginCookie();
89
+ const app = await getApp({ disableCsrf: true });
90
+ await request(app)
91
+ .get("/api/patients/?favbook=1")
92
+ .set("Cookie", loginCookie)
93
+ .expect(succeedJsonWith((rows) => rows.length == 1));
94
+ });
95
+ it("should handle fkey args with no value", async () => {
96
+ const loginCookie = await getAdminLoginCookie();
97
+ const app = await getApp({ disableCsrf: true });
98
+ await request(app)
99
+ .get("/api/patients/?favbook=")
100
+ .set("Cookie", loginCookie)
101
+ .expect(succeedJsonWith((rows) => rows.length == 0));
102
+ });
103
+
87
104
  it("should get books for public with search and one field", async () => {
88
105
  const app = await getApp({ disableCsrf: true });
89
106
  await request(app)
@@ -34,8 +34,18 @@ test("updateQueryStringParameter", () => {
34
34
  expect(removeQueryStringParameter("/foo?name=Bar&age=45", "age")).toBe(
35
35
  "/foo?name=Bar"
36
36
  );
37
+ expect(
38
+ updateQueryStringParameter("/foo", "publisher.publisher->name", "AK")
39
+ ).toBe("/foo?publisher.publisher->name=AK");
40
+ expect(
41
+ updateQueryStringParameter(
42
+ "/foo?publisher.publisher->name=AB",
43
+ "publisher.publisher->name",
44
+ "AK"
45
+ )
46
+ ).toBe("/foo?publisher.publisher->name=AK");
37
47
  });
38
-
48
+ //publisher.publisher->name
39
49
  test("updateQueryStringParameter hash", () => {
40
50
  expect(updateQueryStringParameter("/foo#baz", "age", 43)).toBe(
41
51
  "/foo?age=43#baz"