@saltcorn/server 0.7.1 → 0.7.2-beta.3

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/routes/admin.js CHANGED
@@ -787,7 +787,7 @@ router.get(
787
787
  ? div(
788
788
  { class: "alert alert-success", role: "alert" },
789
789
  i({ class: "fas fa-check-circle fa-lg me-2" }),
790
- h5({ class: "d-inline" }, "No errors detected")
790
+ h5({ class: "d-inline" }, req.__("No errors detected during configuration check"))
791
791
  )
792
792
  : errors.map(mkError)
793
793
  ),
package/routes/api.js CHANGED
@@ -20,6 +20,7 @@ const { error_catcher } = require("./utils.js");
20
20
  //const { mkTable, renderForm, link, post_btn } = require("@saltcorn/markup");
21
21
  const { getState } = require("@saltcorn/data/db/state");
22
22
  const Table = require("@saltcorn/data/models/table");
23
+ const View = require("@saltcorn/data/models/view");
23
24
  //const Field = require("@saltcorn/data/models/field");
24
25
  const Trigger = require("@saltcorn/data/models/trigger");
25
26
  //const load_plugins = require("../load_plugins");
@@ -111,6 +112,40 @@ function accessAllowed(req, user, trigger) {
111
112
  return role <= trigger.min_role;
112
113
  }
113
114
 
115
+ router.post(
116
+ "/viewQuery/:viewName/:queryName",
117
+ error_catcher(async (req, res, next) => {
118
+ let { viewName, queryName } = req.params;
119
+ const view = await View.findOne({ name: viewName });
120
+ if (!view) {
121
+ res.status(404).json({ error: req.__("Not found") });
122
+ return;
123
+ }
124
+ await passport.authenticate(
125
+ "jwt",
126
+ { session: false },
127
+ async function (err, user, info) {
128
+ const role = user && user.id ? user.role_id : 10;
129
+ if (
130
+ role <= view.min_role ||
131
+ (await view.authorise_get({ req, ...view })) // TODO set query to state
132
+ ) {
133
+ const queries = view.queries(false, req);
134
+ if (queries[queryName]) {
135
+ const { args } = req.body;
136
+ const resp = await queries[queryName](...args, true);
137
+ res.json({ success: resp });
138
+ } else {
139
+ res.status(404).json({ error: req.__("Not found") });
140
+ }
141
+ } else {
142
+ res.status(401).json({ error: req.__("Not authorized") });
143
+ }
144
+ }
145
+ )(req, res, next);
146
+ })
147
+ );
148
+
114
149
  router.get(
115
150
  "/:tableName/distinct/:fieldName",
116
151
  //passport.authenticate("api-bearer", { session: false }),
@@ -180,7 +215,7 @@ router.get(
180
215
  }
181
216
 
182
217
  await passport.authenticate(
183
- "api-bearer",
218
+ ["api-bearer", "jwt"],
184
219
  { session: false },
185
220
  async function (err, user, info) {
186
221
  if (accessAllowedRead(req, user, table)) {
package/routes/edit.js CHANGED
@@ -44,7 +44,8 @@ router.post(
44
44
  "error",
45
45
  req.__("Not allowed to write to table %s", table.name)
46
46
  );
47
- if (req.get("referer")) res.redirect(req.get("referer"));
47
+ if (req.xhr) res.send("OK");
48
+ else if (req.get("referer")) res.redirect(req.get("referer"));
48
49
  else res.redirect(redirect || `/list/${table.name}`);
49
50
  })
50
51
  );
package/routes/fields.js CHANGED
@@ -181,6 +181,7 @@ const fieldFlow = (req) =>
181
181
  var attributes = context.attributes || {};
182
182
  attributes.default = context.default;
183
183
  attributes.summary_field = context.summary_field;
184
+ attributes.include_fts = context.include_fts;
184
185
  attributes.on_delete_cascade = context.on_delete_cascade;
185
186
  const {
186
187
  table_id,
@@ -370,6 +371,11 @@ const fieldFlow = (req) =>
370
371
  value: f.name,
371
372
  label: f.label,
372
373
  }));
374
+ const textfields = orderedFields
375
+ .filter(
376
+ (f) => (!f.calculated || f.stored) && f.type?.sql_name === "text"
377
+ )
378
+ .map((f) => f.name);
373
379
  return new Form({
374
380
  fields: [
375
381
  new Field({
@@ -378,6 +384,12 @@ const fieldFlow = (req) =>
378
384
  input_type: "select",
379
385
  options: keyfields,
380
386
  }),
387
+ new Field({
388
+ name: "include_fts",
389
+ label: req.__("Include in full-text search"),
390
+ type: "Bool",
391
+ showIf: { summary_field: textfields },
392
+ }),
381
393
  new Field({
382
394
  name: "on_delete_cascade",
383
395
  label: req.__("On delete cascade"),
package/routes/files.js CHANGED
@@ -9,6 +9,7 @@ const File = require("@saltcorn/data/models/file");
9
9
  const User = require("@saltcorn/data/models/user");
10
10
  const { getState } = require("@saltcorn/data/db/state");
11
11
  const s3storage = require("../s3storage");
12
+ const sharp = require("sharp");
12
13
 
13
14
  const {
14
15
  mkTable,
@@ -43,6 +44,8 @@ const {
43
44
  config_fields_form,
44
45
  save_config_from_form,
45
46
  } = require("../markup/admin");
47
+ const fsp = require("fs").promises;
48
+ const fs = require("fs");
46
49
 
47
50
  /**
48
51
  * @type {object}
@@ -187,6 +190,54 @@ router.get(
187
190
  })
188
191
  );
189
192
 
193
+ /**
194
+ * @name get/resize/:id
195
+ * @function
196
+ * @memberof module:routes/files~filesRouter
197
+ * @function
198
+ */
199
+ router.get(
200
+ "/resize/:id/:width_str",
201
+ error_catcher(async (req, res) => {
202
+ const role = req.user && req.user.id ? req.user.role_id : 10;
203
+ const user_id = req.user && req.user.id;
204
+ const { id, width_str } = req.params;
205
+ let file;
206
+ if (typeof strictParseInt(id) !== "undefined")
207
+ file = await File.findOne({ id });
208
+ else file = await File.findOne({ filename: id });
209
+
210
+ if (!file) {
211
+ res
212
+ .status(404)
213
+ .sendWrap(req.__("Not found"), h1(req.__("File not found")));
214
+ return;
215
+ }
216
+ if (role <= file.min_role_read || (user_id && user_id === file.user_id)) {
217
+ res.type(file.mimetype);
218
+ const cacheability = file.min_role_read === 10 ? "public" : "private";
219
+ res.set("Cache-Control", `${cacheability}, max-age=86400`);
220
+ //TODO s3
221
+ if (file.s3_store) s3storage.serveObject(file, res, false);
222
+ else {
223
+ const width = strictParseInt(width_str);
224
+ if (!width) {
225
+ res.sendFile(file.location);
226
+ return;
227
+ }
228
+ const fnm = `${file.location}_w${width}`;
229
+ if (!fs.existsSync(fnm)) {
230
+ await sharp(file.location).resize({ width }).toFile(fnm);
231
+ }
232
+ res.sendFile(fnm);
233
+ }
234
+ } else {
235
+ req.flash("warning", req.__("Not authorized"));
236
+ res.redirect("/");
237
+ }
238
+ })
239
+ );
240
+
190
241
  /**
191
242
  * @name post/setrole/:id
192
243
  * @function
@@ -582,8 +582,11 @@ router.get(
582
582
  return;
583
583
  }
584
584
  const configFlow = await view.get_config_flow(req);
585
+ const hasConfig =
586
+ view.configuration && Object.keys(view.configuration).length > 0;
585
587
  const wfres = await configFlow.run(
586
588
  {
589
+ id: hasConfig ? view.id : undefined,
587
590
  table_id: view.table_id,
588
591
  exttable_name: view.exttable_name,
589
592
  viewname: name,
@@ -704,6 +707,48 @@ router.post(
704
707
  })
705
708
  );
706
709
 
710
+ /**
711
+ * @name post/saveconfig/:id
712
+ * @function
713
+ * @memberof module:routes/viewedit~vieweditRouter
714
+ * @function
715
+ */
716
+ router.post(
717
+ "/saveconfig/:viewname",
718
+ isAdmin,
719
+ error_catcher(async (req, res) => {
720
+ const { viewname } = req.params;
721
+
722
+ if (viewname && req.body) {
723
+ const view = await View.findOne({ name: viewname });
724
+ const configFlow = await view.get_config_flow(req);
725
+ const step = await configFlow.singleStepForm(req.body, req);
726
+ if (step?.renderForm) {
727
+ if (!step.renderForm.hasErrors) {
728
+ let newcfg;
729
+ if (step.contextField)
730
+ newcfg = {
731
+ ...view.configuration,
732
+ [step.contextField]: {
733
+ ...view.configuration?.[step.contextField],
734
+ ...step.renderForm.values,
735
+ },
736
+ };
737
+ else newcfg = { ...view.configuration, ...step.renderForm.values };
738
+ await View.update({ configuration: newcfg }, view.id);
739
+ res.json({ success: "ok" });
740
+ } else {
741
+ res.json({ error: step.renderForm.errorSummary });
742
+ }
743
+ } else {
744
+ res.json({ error: "no form" });
745
+ }
746
+ } else {
747
+ res.json({ error: "no view" });
748
+ }
749
+ })
750
+ );
751
+
707
752
  /**
708
753
  * @name post/setrole/:id
709
754
  * @function
@@ -730,3 +775,12 @@ router.post(
730
775
  res.redirect("/viewedit");
731
776
  })
732
777
  );
778
+
779
+ router.post(
780
+ "/test/inserter",
781
+ isAdmin,
782
+ error_catcher(async (req, res) => {
783
+ const view = await View.create(req.body);
784
+ res.json({ view });
785
+ })
786
+ );
@@ -12,6 +12,7 @@ const {
12
12
  respondJsonWith,
13
13
  } = require("../auth/testhelp");
14
14
  const db = require("@saltcorn/data/db");
15
+ const { sleep } = require("@saltcorn/data/tests/mocks");
15
16
  const fs = require("fs").promises;
16
17
  const File = require("@saltcorn/data/models/file");
17
18
  const User = require("@saltcorn/data/models/user");
@@ -30,7 +31,12 @@ beforeAll(async () => {
30
31
  4
31
32
  );
32
33
  });
33
- afterAll(db.close);
34
+
35
+ afterAll(async () => {
36
+ await sleep(200);
37
+ db.close();
38
+ });
39
+
34
40
  const adminPageContains = (specs) =>
35
41
  it("adminPageContains " + specs.map((s) => s[1]).join(","), async () => {
36
42
  const app = await getApp({ disableCsrf: true });
@@ -456,6 +462,71 @@ describe("actions", () => {
456
462
  .expect(toRedirect("/actions/"));
457
463
  });
458
464
  });
465
+ describe("localizer", () => {
466
+ itShouldRedirectUnauthToLogin("/site-structure/localizer");
467
+ itShouldRedirectUnauthToLogin("/site-structure/localizer/add-lang");
468
+ it("redirects site struct to menu", async () => {
469
+ const app = await getApp({ disableCsrf: true });
470
+ const loginCookie = await getAdminLoginCookie();
471
+ await request(app)
472
+ .get("/site-structure")
473
+ .set("Cookie", loginCookie)
474
+ .expect(toRedirect("/menu"));
475
+ });
476
+ it("shows languages", async () => {
477
+ const app = await getApp({ disableCsrf: true });
478
+ const loginCookie = await getAdminLoginCookie();
479
+ await request(app)
480
+ .get("/site-structure/localizer")
481
+ .set("Cookie", loginCookie)
482
+ .expect(toInclude("Languages"));
483
+ });
484
+ it("shows add language form", async () => {
485
+ const app = await getApp({ disableCsrf: true });
486
+ const loginCookie = await getAdminLoginCookie();
487
+ await request(app)
488
+ .get("/site-structure/localizer/add-lang")
489
+ .set("Cookie", loginCookie)
490
+ .expect(toInclude("Locale identifier short code"));
491
+ });
492
+ it("add language", async () => {
493
+ const app = await getApp({ disableCsrf: true });
494
+ const loginCookie = await getAdminLoginCookie();
495
+ await request(app)
496
+ .post("/site-structure/localizer/save-lang")
497
+ .set("Cookie", loginCookie)
498
+ .send("name=dansk")
499
+ .send("locale=da")
500
+ .expect(toRedirect("/site-structure/localizer/edit/da"));
501
+ });
502
+ it("shows new in languages", async () => {
503
+ const app = await getApp({ disableCsrf: true });
504
+ const loginCookie = await getAdminLoginCookie();
505
+ await request(app)
506
+ .get("/site-structure/localizer")
507
+ .set("Cookie", loginCookie)
508
+ .expect(toInclude("dansk"));
509
+ });
510
+
511
+ it("shows edit language form", async () => {
512
+ const app = await getApp({ disableCsrf: true });
513
+ const loginCookie = await getAdminLoginCookie();
514
+ await request(app)
515
+ .get("/site-structure/localizer/edit/da")
516
+ .set("Cookie", loginCookie)
517
+ .expect(toInclude("Hello world"));
518
+ });
519
+ it("set string language", async () => {
520
+ const app = await getApp({ disableCsrf: true });
521
+ const loginCookie = await getAdminLoginCookie();
522
+ await request(app)
523
+ .post("/site-structure/localizer/save-string/da/Hello%20world")
524
+ .set("Cookie", loginCookie)
525
+ .send("value=Hej+verden")
526
+ .expect(toRedirect("/site-structure/localizer/edit/da"));
527
+ });
528
+ });
529
+
459
530
  /**
460
531
  * Pages tests
461
532
  */
@@ -14,6 +14,7 @@ const load_script = (fnm) => {
14
14
  };
15
15
 
16
16
  load_script("jquery-3.6.0.min.js");
17
+ load_script("saltcorn-common.js");
17
18
  load_script("saltcorn.js");
18
19
 
19
20
  test("updateQueryStringParameter", () => {
@@ -90,7 +90,7 @@ describe("Plugin Endpoints", () => {
90
90
  .expect(toInclude("testfilecontents"));
91
91
  await request(app)
92
92
  .get(
93
- "/plugins/pubdeps/sbadmin2/startbootstrap-sb-admin-2-bs5/4.1.5-beta.0/css/sb-admin-2.min.css"
93
+ "/plugins/pubdeps/sbadmin2/startbootstrap-sb-admin-2-bs5/4.1.5-beta.4/css/sb-admin-2.min.css"
94
94
  )
95
95
  .expect(toInclude("Start Bootstrap"));
96
96
 
@@ -100,7 +100,7 @@ describe("Plugin Endpoints", () => {
100
100
  .expect(toRedirect("/plugins"));
101
101
  await request(app)
102
102
  .get(
103
- "/plugins/pubdeps/sbadmin2/startbootstrap-sb-admin-2-bs5/4.1.5-beta.0/css/sb-admin-2.min.css"
103
+ "/plugins/pubdeps/sbadmin2/startbootstrap-sb-admin-2-bs5/4.1.5-beta.4/css/sb-admin-2.min.css"
104
104
  )
105
105
  .expect(toInclude("Start Bootstrap"));
106
106
  });
@@ -8,6 +8,7 @@ const {
8
8
  toInclude,
9
9
  toNotInclude,
10
10
  resetToFixtures,
11
+ succeedJsonWith,
11
12
  } = require("../auth/testhelp");
12
13
  const db = require("@saltcorn/data/db");
13
14
  const View = require("@saltcorn/data/models/view");
@@ -371,3 +372,96 @@ describe("viewedit new Show", () => {
371
372
  .expect(toRedirect("/viewedit"));
372
373
  });
373
374
  });
375
+ describe("Library", () => {
376
+ it("should save new from builder", async () => {
377
+ const loginCookie = await getAdminLoginCookie();
378
+ const app = await getApp({ disableCsrf: true });
379
+ await request(app)
380
+ .post("/library/savefrombuilder/")
381
+ .set("Cookie", loginCookie)
382
+ .send({
383
+ layout: {
384
+ columns: [],
385
+ layout: {
386
+ type: "card",
387
+ contents: {
388
+ above: [
389
+ null,
390
+ {
391
+ besides: [
392
+ {
393
+ above: [
394
+ null,
395
+ {
396
+ type: "blank",
397
+ contents: "Hello world",
398
+ block: false,
399
+ inline: false,
400
+ textStyle: "",
401
+ isFormula: {},
402
+ labelFor: "",
403
+ style: {},
404
+ font: "",
405
+ },
406
+ ],
407
+ },
408
+ {
409
+ above: [
410
+ null,
411
+ {
412
+ type: "blank",
413
+ contents: "Bye bye",
414
+ block: false,
415
+ inline: false,
416
+ textStyle: "",
417
+ isFormula: {},
418
+ labelFor: "",
419
+ style: {},
420
+ font: "",
421
+ },
422
+ ],
423
+ },
424
+ ],
425
+ breakpoints: ["", ""],
426
+ style: {},
427
+ widths: [6, 6],
428
+ },
429
+ ],
430
+ },
431
+ title: "header",
432
+ style: {},
433
+ },
434
+ },
435
+ icon: "far fa-angry",
436
+ name: "ShinyCard",
437
+ })
438
+ .set("Content-Type", "application/json")
439
+ .set("Accept", "application/json")
440
+ .expect(succeedJsonWith(() => true));
441
+ });
442
+ it("shows library with item", async () => {
443
+ const app = await getApp({ disableCsrf: true });
444
+ const loginCookie = await getAdminLoginCookie();
445
+ await request(app)
446
+ .get("/library/list")
447
+ .set("Cookie", loginCookie)
448
+ .expect(toInclude("ShinyCard"));
449
+ });
450
+ it("deletes in library", async () => {
451
+ const app = await getApp({ disableCsrf: true });
452
+ const loginCookie = await getAdminLoginCookie();
453
+ await request(app)
454
+ .post("/library/delete/1")
455
+ .set("Cookie", loginCookie)
456
+ .expect(toRedirect("/library/list"));
457
+ });
458
+ it("shows empty library", async () => {
459
+ const app = await getApp({ disableCsrf: true });
460
+ const loginCookie = await getAdminLoginCookie();
461
+ await request(app)
462
+ .get("/library/list")
463
+ .set("Cookie", loginCookie)
464
+ .expect(toInclude("Library"))
465
+ .expect(toNotInclude("ShinyCard"))
466
+ });
467
+ });
package/wrapper.js CHANGED
@@ -167,6 +167,7 @@ const get_headers = (req, version_tag, description, extras = []) => {
167
167
  headerTag: `<script>var _sc_globalCsrf = "${req.csrfToken()}"; var _sc_version_tag = "${version_tag}";</script>`,
168
168
  },
169
169
  { css: `/static_assets/${version_tag}/saltcorn.css` },
170
+ { script: `/static_assets/${version_tag}/saltcorn-common.js` },
170
171
  { script: `/static_assets/${version_tag}/saltcorn.js` },
171
172
  ];
172
173
  let from_cfg = [];