@saltcorn/server 0.9.6-beta.2 → 0.9.6-beta.20

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.
Files changed (49) hide show
  1. package/app.js +6 -1
  2. package/auth/admin.js +55 -53
  3. package/auth/routes.js +28 -10
  4. package/auth/testhelp.js +86 -0
  5. package/help/Field label.tmd +11 -0
  6. package/help/Field types.tmd +39 -0
  7. package/help/Ownership field.tmd +76 -0
  8. package/help/Ownership formula.tmd +75 -0
  9. package/help/Table roles.tmd +20 -0
  10. package/help/User groups.tmd +35 -0
  11. package/load_plugins.js +33 -5
  12. package/locales/en.json +29 -1
  13. package/locales/it.json +3 -2
  14. package/markup/admin.js +1 -0
  15. package/markup/forms.js +5 -1
  16. package/package.json +9 -9
  17. package/public/log_viewer_utils.js +32 -0
  18. package/public/mermaid.min.js +705 -306
  19. package/public/saltcorn-builder.css +23 -0
  20. package/public/saltcorn-common.js +248 -80
  21. package/public/saltcorn.css +80 -0
  22. package/public/saltcorn.js +86 -2
  23. package/restart_watcher.js +1 -0
  24. package/routes/actions.js +27 -0
  25. package/routes/admin.js +175 -64
  26. package/routes/api.js +6 -0
  27. package/routes/common_lists.js +42 -32
  28. package/routes/fields.js +70 -42
  29. package/routes/homepage.js +2 -0
  30. package/routes/index.js +2 -0
  31. package/routes/menu.js +69 -4
  32. package/routes/notifications.js +90 -10
  33. package/routes/pageedit.js +18 -13
  34. package/routes/plugins.js +11 -2
  35. package/routes/registry.js +289 -0
  36. package/routes/search.js +10 -4
  37. package/routes/tables.js +51 -27
  38. package/routes/tenant.js +4 -15
  39. package/routes/utils.js +25 -8
  40. package/routes/view.js +1 -1
  41. package/routes/viewedit.js +11 -7
  42. package/serve.js +27 -5
  43. package/tests/edit.test.js +426 -0
  44. package/tests/fields.test.js +21 -0
  45. package/tests/filter.test.js +68 -0
  46. package/tests/page.test.js +2 -2
  47. package/tests/plugins.test.js +2 -0
  48. package/tests/sync.test.js +59 -0
  49. package/wrapper.js +4 -1
package/serve.js CHANGED
@@ -235,6 +235,10 @@ module.exports =
235
235
  : defaultNCPUs;
236
236
 
237
237
  const letsEncrypt = await getConfig("letsencrypt", false);
238
+ const pruneSessionInterval = +(await getConfig(
239
+ "prune_session_interval",
240
+ 900
241
+ ));
238
242
  const masterState = {
239
243
  started: false,
240
244
  listeningTo: new Set([]),
@@ -287,7 +291,11 @@ module.exports =
287
291
  })
288
292
  .ready((glx) => {
289
293
  const httpsServer = glx.httpsServer();
290
- setupSocket(appargs?.subdomainOffset, httpsServer);
294
+ setupSocket(
295
+ appargs?.subdomainOffset,
296
+ pruneSessionInterval,
297
+ httpsServer
298
+ );
291
299
  httpsServer.setTimeout(timeout * 1000);
292
300
  process.on("message", workerDispatchMsg);
293
301
  glx.serveApp(app);
@@ -344,6 +352,10 @@ const nonGreenlockWorkerSetup = async (appargs, port) => {
344
352
  const cert = getState().getConfig("custom_ssl_certificate", "");
345
353
  const key = getState().getConfig("custom_ssl_private_key", "");
346
354
  const timeout = +getState().getConfig("timeout", 120);
355
+ const pruneSessionInterval = +(await getState().getConfig(
356
+ "prune_session_interval",
357
+ 900
358
+ ));
347
359
  // Server with http on port 80 / https on 443
348
360
  // todo resolve hardcode
349
361
  if (port === 80 && cert && key) {
@@ -354,7 +366,12 @@ const nonGreenlockWorkerSetup = async (appargs, port) => {
354
366
  // todo timeout to config
355
367
  httpServer.setTimeout(timeout * 1000);
356
368
  httpsServer.setTimeout(timeout * 1000);
357
- setupSocket(appargs?.subdomainOffset, httpServer, httpsServer);
369
+ setupSocket(
370
+ appargs?.subdomainOffset,
371
+ pruneSessionInterval,
372
+ httpServer,
373
+ httpsServer
374
+ );
358
375
  httpServer.listen(port, () => {
359
376
  console.log("HTTP Server running on port 80");
360
377
  });
@@ -367,7 +384,7 @@ const nonGreenlockWorkerSetup = async (appargs, port) => {
367
384
  // server with http only
368
385
  const http = require("http");
369
386
  const httpServer = http.createServer(app);
370
- setupSocket(appargs?.subdomainOffset, httpServer);
387
+ setupSocket(appargs?.subdomainOffset, pruneSessionInterval, httpServer);
371
388
 
372
389
  // todo timeout to config
373
390
  // todo refer in doc to httpserver doc
@@ -384,7 +401,7 @@ const nonGreenlockWorkerSetup = async (appargs, port) => {
384
401
  *
385
402
  * @param {...*} servers
386
403
  */
387
- const setupSocket = (subdomainOffset, ...servers) => {
404
+ const setupSocket = (subdomainOffset, pruneSessionInterval, ...servers) => {
388
405
  // https://socket.io/docs/v4/middlewares/
389
406
  const wrap = (middleware) => (socket, next) =>
390
407
  middleware(socket.request, {}, next);
@@ -395,7 +412,7 @@ const setupSocket = (subdomainOffset, ...servers) => {
395
412
  }
396
413
 
397
414
  const passportInit = passport.initialize();
398
- const sessionStore = getSessionStore();
415
+ const sessionStore = getSessionStore(pruneSessionInterval);
399
416
  const setupNamespace = (namespace) => {
400
417
  //io.of(namespace).use(wrap(setTenant));
401
418
  io.of(namespace).use(wrap(sessionStore));
@@ -452,6 +469,11 @@ const setupSocket = (subdomainOffset, ...servers) => {
452
469
  socketIds.push(socket.id);
453
470
  await getState().setConfig("joined_log_socket_ids", [...socketIds]);
454
471
  callback({ status: "ok" });
472
+ setTimeout(() => {
473
+ io.of("/")
474
+ .to(`_logs_${tenant}_`)
475
+ .emit("test_conn_msg", { text: "test message" });
476
+ }, 1000);
455
477
  }
456
478
  } catch (err) {
457
479
  getState().log(1, `Socket join_logs stream: ${err.stack}`);
@@ -0,0 +1,426 @@
1
+ const request = require("supertest");
2
+ const getApp = require("../app");
3
+ const { resetToFixtures, load_url_dom } = require("../auth/testhelp");
4
+ const db = require("@saltcorn/data/db");
5
+ const { getState } = require("@saltcorn/data/db/state");
6
+ const View = require("@saltcorn/data/models/view");
7
+ const Field = require("@saltcorn/data/models/field");
8
+ const Table = require("@saltcorn/data/models/table");
9
+ const { plugin_with_routes, sleep } = require("@saltcorn/data/tests/mocks");
10
+
11
+ afterAll(db.close);
12
+ beforeAll(async () => {
13
+ await resetToFixtures();
14
+ const table = Table.findOne("books");
15
+ await table.update({ min_role_read: 100 });
16
+ await Field.create({
17
+ table,
18
+ name: "sequel_to",
19
+ type: "Key to books",
20
+ attributes: { summary_field: "author" },
21
+ });
22
+ await Field.create({
23
+ table,
24
+ label: "pagesp1",
25
+ type: "Integer",
26
+ calculated: true,
27
+ expression: "pages+1",
28
+ });
29
+ await table.insertRow({
30
+ author: "Peter Kropotkin",
31
+ pages: 189,
32
+ publisher: 1,
33
+ });
34
+
35
+ await table.insertRow({
36
+ author: "Mary Boas",
37
+ pages: 864,
38
+ publisher: 2,
39
+ });
40
+ const ptable = Table.findOne("publisher");
41
+ await ptable.update({ min_role_read: 100 });
42
+
43
+ //await getState().setConfig("log_level", 6);
44
+ });
45
+
46
+ jest.setTimeout(30000);
47
+
48
+ const makeJoinSelectView = async ({ name, showIfFormula }) => {
49
+ await View.create({
50
+ viewtemplate: "Edit",
51
+ description: "",
52
+ min_role: 100,
53
+ name,
54
+ table_id: Table.findOne("books")?.id,
55
+ default_render_page: "",
56
+ slug: {},
57
+ attributes: {},
58
+ configuration: {
59
+ layout: {
60
+ above: [
61
+ {
62
+ gx: null,
63
+ gy: null,
64
+ style: {
65
+ "margin-bottom": "1.5rem",
66
+ },
67
+ aligns: ["end", "start"],
68
+ widths: [2, 10],
69
+ besides: [
70
+ {
71
+ font: "",
72
+ type: "blank",
73
+ block: false,
74
+ style: {},
75
+ inline: false,
76
+ contents: "Publisher",
77
+ labelFor: "publisher",
78
+ isFormula: {},
79
+ textStyle: "",
80
+ },
81
+ {
82
+ above: [
83
+ {
84
+ type: "field",
85
+ block: false,
86
+ fieldview: "select",
87
+ textStyle: "",
88
+ field_name: "publisher",
89
+ configuration: {},
90
+ },
91
+ {
92
+ type: "container",
93
+ style: {},
94
+ bgType: "None",
95
+ hAlign: "left",
96
+ margin: [0, 0, 0, 0],
97
+ rotate: 0,
98
+ vAlign: "top",
99
+ bgColor: "#ffffff",
100
+ display: "block",
101
+ padding: [0, 0, 0, 0],
102
+ bgFileId: 0,
103
+ contents: {
104
+ above: [
105
+ {
106
+ font: "",
107
+ icon: "",
108
+ type: "blank",
109
+ block: false,
110
+ style: {},
111
+ inline: false,
112
+ contents: "Warning",
113
+ labelFor: "",
114
+ isFormula: {},
115
+ textStyle: "",
116
+ },
117
+ {
118
+ type: "join_field",
119
+ block: false,
120
+ fieldview: "as_text",
121
+ textStyle: "",
122
+ join_field: "publisher.name",
123
+ configuration: {},
124
+ },
125
+ ],
126
+ },
127
+ imageSize: "contain",
128
+ isFormula: {},
129
+ minHeight: 0,
130
+ textColor: "#ffffff",
131
+ widthUnit: "px",
132
+ heightUnit: "px",
133
+ customClass: "pubwarn",
134
+ htmlElement: "div",
135
+ showForRole: [],
136
+ gradEndColor: "#88ff88",
137
+ setTextColor: false,
138
+ fullPageWidth: false,
139
+ gradDirection: "0",
140
+ minHeightUnit: "px",
141
+ showIfFormula,
142
+ gradStartColor: "#ff8888",
143
+ maxScreenWidth: "",
144
+ minScreenWidth: "",
145
+ show_for_owner: false,
146
+ },
147
+ ],
148
+ },
149
+ ],
150
+ breakpoints: ["", ""],
151
+ },
152
+ {
153
+ gx: null,
154
+ gy: null,
155
+ style: {
156
+ "margin-bottom": "1.5rem",
157
+ },
158
+ aligns: ["end", "start"],
159
+ widths: [2, 10],
160
+ besides: [
161
+ {
162
+ font: "",
163
+ type: "blank",
164
+ block: false,
165
+ style: {},
166
+ inline: false,
167
+ contents: "sequel_to",
168
+ labelFor: "sequel_to",
169
+ isFormula: {},
170
+ textStyle: "",
171
+ },
172
+ {
173
+ type: "field",
174
+ block: false,
175
+ fieldview: "select",
176
+ textStyle: "",
177
+ field_name: "sequel_to",
178
+ configuration: {
179
+ where: "publisher == $publisher",
180
+ },
181
+ },
182
+ ],
183
+ breakpoints: ["", ""],
184
+ },
185
+ {
186
+ gx: null,
187
+ gy: null,
188
+ style: {
189
+ "margin-bottom": "1.5rem",
190
+ },
191
+ aligns: ["end", "start"],
192
+ widths: [2, 10],
193
+ besides: [
194
+ {
195
+ font: "",
196
+ icon: "",
197
+ type: "blank",
198
+ block: false,
199
+ style: {},
200
+ inline: false,
201
+ contents: "Pages",
202
+ labelFor: "sequel_to",
203
+ isFormula: {},
204
+ textStyle: "",
205
+ },
206
+ {
207
+ above: [
208
+ {
209
+ type: "field",
210
+ block: false,
211
+ fieldview: "edit",
212
+ textStyle: "",
213
+ field_name: "pages",
214
+ configuration: {
215
+ where: "publisher == $publisher",
216
+ },
217
+ },
218
+ {
219
+ type: "field",
220
+ block: false,
221
+ fieldview: "show",
222
+ textStyle: "",
223
+ field_name: "pagesp1",
224
+ configuration: {
225
+ input_type: "text",
226
+ },
227
+ },
228
+ ],
229
+ },
230
+ ],
231
+ breakpoints: ["", ""],
232
+ },
233
+ {
234
+ type: "action",
235
+ block: false,
236
+ rndid: "cb94bd",
237
+ nsteps: "",
238
+ minRole: 100,
239
+ isFormula: {},
240
+ action_icon: "",
241
+ action_name: "Save",
242
+ action_size: "",
243
+ action_bgcol: "",
244
+ action_label: "",
245
+ action_style: "btn-primary",
246
+ action_title: "",
247
+ configuration: {},
248
+ step_only_ifs: "",
249
+ action_textcol: "",
250
+ action_bordercol: "",
251
+ step_action_names: "",
252
+ },
253
+ ],
254
+ },
255
+ columns: [
256
+ {
257
+ type: "Field",
258
+ block: false,
259
+ fieldview: "select",
260
+ textStyle: "",
261
+ field_name: "publisher",
262
+ configuration: {},
263
+ },
264
+ {
265
+ type: "JoinField",
266
+ block: false,
267
+ fieldview: "as_text",
268
+ textStyle: "",
269
+ join_field: "publisher.name",
270
+ configuration: {},
271
+ },
272
+ {
273
+ type: "Field",
274
+ block: false,
275
+ fieldview: "select",
276
+ textStyle: "",
277
+ field_name: "sequel_to",
278
+ configuration: {
279
+ where: "publisher == $publisher",
280
+ },
281
+ },
282
+ {
283
+ type: "Field",
284
+ block: false,
285
+ fieldview: "edit",
286
+ textStyle: "",
287
+ field_name: "pages",
288
+ configuration: {
289
+ where: "publisher == $publisher",
290
+ },
291
+ },
292
+ {
293
+ type: "Field",
294
+ block: false,
295
+ fieldview: "show",
296
+ textStyle: "",
297
+ field_name: "pagesp1",
298
+ configuration: {
299
+ input_type: "text",
300
+ },
301
+ },
302
+ {
303
+ type: "Action",
304
+ rndid: "cb94bd",
305
+ nsteps: "",
306
+ minRole: 100,
307
+ isFormula: {},
308
+ action_icon: "",
309
+ action_name: "Save",
310
+ action_size: "",
311
+ action_bgcol: "",
312
+ action_label: "",
313
+ action_style: "btn-primary",
314
+ action_title: "",
315
+ configuration: {},
316
+ step_only_ifs: "",
317
+ action_textcol: "",
318
+ action_bordercol: "",
319
+ step_action_names: "",
320
+ },
321
+ ],
322
+ viewname: "AuthorEditForTest",
323
+ auto_save: false,
324
+ split_paste: false,
325
+ exttable_name: null,
326
+ page_when_done: null,
327
+ view_when_done: "authorlist",
328
+ dest_url_formula: null,
329
+ destination_type: "View",
330
+ formula_destinations: [],
331
+ page_group_when_done: null,
332
+ },
333
+ });
334
+ };
335
+
336
+ const newEvent = (dom, type) =>
337
+ new dom.window.CustomEvent(type, {
338
+ bubbles: true,
339
+ cancelable: true,
340
+ });
341
+
342
+ describe("JSDOM-E2E edit test", () => {
343
+ it("join select should set dynamic where and show if with joinfield", async () => {
344
+ await makeJoinSelectView({
345
+ name: "AuthorEditForTest",
346
+ showIfFormula: 'publisher?.name == "AK Press"',
347
+ });
348
+ const dom = await load_url_dom("/view/AuthorEditForTest");
349
+ await sleep(1000);
350
+ const pubwarn = dom.window.document.querySelector("div.pubwarn");
351
+ //console.log(dom.serialize());
352
+ expect(pubwarn.style.display).toBe("none");
353
+
354
+ const select_seq = dom.window.document.querySelector(
355
+ "select[name=sequel_to]"
356
+ );
357
+ expect([...select_seq.options].map((o) => o.text)).toStrictEqual([
358
+ "",
359
+ "Herman Melville",
360
+ ]);
361
+ const select = dom.window.document.querySelector("select[name=publisher]");
362
+ select.value = "1";
363
+ select.dispatchEvent(newEvent(dom, "change"));
364
+
365
+ await sleep(1000);
366
+ expect([...select_seq.options].map((o) => o.text)).toStrictEqual([
367
+ "",
368
+ "Leo Tolstoy",
369
+ "Peter Kropotkin",
370
+ ]);
371
+
372
+ expect(pubwarn.style.display).toBe("");
373
+
374
+ const jf = dom.window.document.querySelector(
375
+ "div.pubwarn div[data-source-url]"
376
+ );
377
+ expect(jf.innerHTML).toBe("AK Press");
378
+ });
379
+ it("calculated field", async () => {
380
+ const dom = await load_url_dom("/view/AuthorEditForTest");
381
+ const input = dom.window.document.querySelector("input[name=pages]");
382
+ input.value = "13";
383
+ input.dispatchEvent(newEvent(dom, "change"));
384
+ await sleep(1000);
385
+ const cf = dom.window.document.querySelector(
386
+ `div[data-source-url="/field/show-calculated/books/pagesp1/show?input_type=text"]`
387
+ );
388
+ expect(cf.innerHTML).toBe("14");
389
+ });
390
+
391
+ it("join select should set dynamic where and show if with no joinfield", async () => {
392
+ await makeJoinSelectView({
393
+ name: "AuthorEditForTest1",
394
+ showIfFormula: "publisher == 1",
395
+ });
396
+ const dom = await load_url_dom("/view/AuthorEditForTest1");
397
+ await sleep(1000);
398
+ const pubwarn = dom.window.document.querySelector("div.pubwarn");
399
+
400
+ expect(pubwarn.style.display).toBe("none");
401
+
402
+ const select_seq = dom.window.document.querySelector(
403
+ "select[name=sequel_to]"
404
+ );
405
+ expect([...select_seq.options].map((o) => o.text)).toStrictEqual([
406
+ "",
407
+ "Herman Melville",
408
+ ]);
409
+ const select = dom.window.document.querySelector("select[name=publisher]");
410
+ select.value = "1";
411
+ select.dispatchEvent(newEvent(dom, "change"));
412
+
413
+ await sleep(1000);
414
+ expect([...select_seq.options].map((o) => o.text)).toStrictEqual([
415
+ "",
416
+ "Leo Tolstoy",
417
+ "Peter Kropotkin",
418
+ ]);
419
+
420
+ expect(pubwarn.style.display).toBe("");
421
+ const jf = dom.window.document.querySelector(
422
+ "div.pubwarn div[data-source-url]"
423
+ );
424
+ expect(jf.innerHTML).toBe("AK Press");
425
+ });
426
+ });
@@ -391,6 +391,27 @@ describe("Field Endpoints", () => {
391
391
  })
392
392
  .expect(toBeTrue((r) => +r.text > 2));
393
393
  });
394
+ it("should show calculated field with two single joinfields", async () => {
395
+ const loginCookie = await getAdminLoginCookie();
396
+ const table = Table.findOne({ name: "patients" });
397
+ await Field.create({
398
+ table,
399
+ label: "pagesp12",
400
+ type: "Integer",
401
+ calculated: true,
402
+ stored: true,
403
+ expression: "favbook.pages+1+favbook.id",
404
+ });
405
+ const app = await getApp({ disableCsrf: true });
406
+
407
+ await request(app)
408
+ .post("/field/show-calculated/patients/pagesp12/show")
409
+ .set("Cookie", loginCookie)
410
+ .send({
411
+ id: 1,
412
+ })
413
+ .expect(toBeTrue((r) => +r.text > 2));
414
+ });
394
415
  it("should show calculated field with double joinfield", async () => {
395
416
  const loginCookie = await getAdminLoginCookie();
396
417
  const table = Table.findOne({ name: "readings" });
@@ -0,0 +1,68 @@
1
+ const request = require("supertest");
2
+ const getApp = require("../app");
3
+ const { resetToFixtures, load_url_dom } = require("../auth/testhelp");
4
+ const db = require("@saltcorn/data/db");
5
+ const { getState } = require("@saltcorn/data/db/state");
6
+ const View = require("@saltcorn/data/models/view");
7
+ const Table = require("@saltcorn/data/models/table");
8
+ const { plugin_with_routes, sleep } = require("@saltcorn/data/tests/mocks");
9
+
10
+ afterAll(db.close);
11
+ beforeAll(async () => {
12
+ await resetToFixtures();
13
+ });
14
+
15
+ jest.setTimeout(30000);
16
+
17
+ describe("JSDOM-E2E filter test", () => {
18
+ it("should load authorlist", async () => {
19
+ const dom = await load_url_dom("/view/authorlist");
20
+ //console.log("dom", dom);
21
+ });
22
+ it("should user filter to change url", async () => {
23
+ await View.create({
24
+ viewtemplate: "Filter",
25
+ description: "",
26
+ min_role: 100,
27
+ name: `authorfilter1`,
28
+ table_id: Table.findOne("books")?.id,
29
+ default_render_page: "",
30
+ slug: {},
31
+ attributes: {},
32
+ configuration: {
33
+ layout: {
34
+ type: "field",
35
+ block: false,
36
+ fieldview: "edit",
37
+ textStyle: "",
38
+ field_name: "author",
39
+ configuration: {},
40
+ },
41
+ columns: [
42
+ {
43
+ type: "Field",
44
+ block: false,
45
+ fieldview: "edit",
46
+ textStyle: "",
47
+ field_name: "author",
48
+ configuration: {},
49
+ },
50
+ ],
51
+ },
52
+ });
53
+ const dom = await load_url_dom("/view/authorfilter1");
54
+ expect(dom.window.location.href).toBe(
55
+ "http://localhost/view/authorfilter1"
56
+ );
57
+ //console.log(dom.serialize());
58
+ const input = dom.window.document.querySelector("input[name=author]");
59
+ input.value = "Leo";
60
+ input.dispatchEvent(new dom.window.Event("change"));
61
+ await sleep(1000);
62
+ expect(dom.window.location.href).toBe(
63
+ "http://localhost/view/authorfilter1?author=Leo"
64
+ );
65
+
66
+ //console.log("dom", dom);
67
+ });
68
+ });
@@ -76,7 +76,7 @@ describe("page create", () => {
76
76
  await request(app)
77
77
  .get("/pageedit/new")
78
78
  .set("Cookie", loginCookie)
79
- .expect(toInclude("A short name that will be in your URL"));
79
+ .expect(toInclude("A short name that will be in the page URL"));
80
80
  });
81
81
  it("shows new with html file selector", async () => {
82
82
  const app = await getApp({ disableCsrf: true });
@@ -237,7 +237,7 @@ describe("pageedit", () => {
237
237
  await request(app)
238
238
  .get("/pageedit/edit-properties/a_page")
239
239
  .set("Cookie", loginCookie)
240
- .expect(toInclude("A short name that will be in your URL"));
240
+ .expect(toInclude("A short name that will be in the page URL"));
241
241
 
242
242
  //TODO full context
243
243
  const ctx = encodeURIComponent(JSON.stringify({}));
@@ -137,8 +137,10 @@ describe("Plugin dependency resolution and upgrade", () => {
137
137
  .expect(toRedirect("/plugins"));
138
138
  const quill = await Plugin.findOne({ name: "quill-editor" });
139
139
  expect(quill.location).toBe("@saltcorn/quill-editor");
140
+ expect(quill.name).toBe("quill-editor");
140
141
  const html = await Plugin.findOne({ location: "@saltcorn/html" });
141
142
  expect(html.location).toBe("@saltcorn/html");
143
+ expect(html.name).toBe("html");
142
144
  const html_type = getState().types.HTML;
143
145
  expect(!!html_type.fieldviews.Quill).toBe(true);
144
146
  });