@saltcorn/server 0.8.6-beta.3 → 0.8.6-beta.5

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
@@ -1166,5 +1166,6 @@
1166
1166
  "Event logs": "Event logs",
1167
1167
  "Migrations": "Migrations",
1168
1168
  "Tag Entries": "Tag Entries",
1169
- "Not a valid field name": "Not a valid field name"
1170
- }
1169
+ "Not a valid field name": "Not a valid field name",
1170
+ "Set a default value for missing data": "Set a default value for missing data"
1171
+ }
package/package.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "@saltcorn/server",
3
- "version": "0.8.6-beta.3",
3
+ "version": "0.8.6-beta.5",
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.8.6-beta.3",
10
- "@saltcorn/builder": "0.8.6-beta.3",
11
- "@saltcorn/data": "0.8.6-beta.3",
12
- "@saltcorn/admin-models": "0.8.6-beta.3",
13
- "@saltcorn/filemanager": "0.8.6-beta.3",
14
- "@saltcorn/markup": "0.8.6-beta.3",
15
- "@saltcorn/sbadmin2": "0.8.6-beta.3",
9
+ "@saltcorn/base-plugin": "0.8.6-beta.5",
10
+ "@saltcorn/builder": "0.8.6-beta.5",
11
+ "@saltcorn/data": "0.8.6-beta.5",
12
+ "@saltcorn/admin-models": "0.8.6-beta.5",
13
+ "@saltcorn/filemanager": "0.8.6-beta.5",
14
+ "@saltcorn/markup": "0.8.6-beta.5",
15
+ "@saltcorn/sbadmin2": "0.8.6-beta.5",
16
16
  "@socket.io/cluster-adapter": "^0.2.1",
17
17
  "@socket.io/sticky": "^1.0.1",
18
18
  "adm-zip": "0.5.10",
@@ -435,6 +435,7 @@ function reload_on_init() {
435
435
  localStorage.setItem("reload_on_init", true);
436
436
  }
437
437
  function initialize_page() {
438
+ const isNode = typeof parent?.saltcorn?.data?.state === "undefined";
438
439
  //console.log("init page");
439
440
  $(".blur-on-enter-keypress").bind("keyup", function (e) {
440
441
  if (e.keyCode === 13) e.target.blur();
@@ -480,7 +481,9 @@ function initialize_page() {
480
481
  if ($(this).find(".editicon").length === 0) {
481
482
  var current = $(this).html();
482
483
  $(this).html(
483
- `<span class="current">${current}</span><i class="editicon fas fa-edit ms-1"></i>`
484
+ `<span class="current">${current}</span><i class="editicon ${
485
+ !isNode ? "visible" : ""
486
+ } fas fa-edit ms-1"></i>`
484
487
  );
485
488
  }
486
489
  });
@@ -546,12 +549,20 @@ function initialize_page() {
546
549
  } else
547
550
  $(this).replaceWith(
548
551
  `<form method="post" action="${url}" ${
549
- ajax ? `onsubmit="inline_ajax_submit(event, '${opts}')"` : ""
552
+ ajax
553
+ ? `onsubmit="inline_${
554
+ isNode ? "ajax" : "local"
555
+ }_submit(event, '${opts}')"`
556
+ : ""
550
557
  }>
551
- <input type="hidden" name="_csrf" value="${_sc_globalCsrf}">
552
- <input type="${
553
- type === "Integer" || type === "Float" ? "number" : "text"
554
- }" name="${key}" value="${escapeHtml(current)}">
558
+ ${
559
+ isNode
560
+ ? `<input type="hidden" name="_csrf" value="${_sc_globalCsrf}"></input>`
561
+ : ""
562
+ }
563
+ <input type="${
564
+ type === "Integer" || type === "Float" ? "number" : "text"
565
+ }" name="${key}" value="${escapeHtml(current)}">
555
566
  <button type="submit" class="btn btn-sm btn-primary">OK</button>
556
567
  <button onclick="cancel_inline_edit(event, '${opts}')" type="button" class="btn btn-sm btn-danger"><i class="fas fa-times"></i></button>
557
568
  </form>`
@@ -641,6 +652,7 @@ function initialize_page() {
641
652
  $(initialize_page);
642
653
 
643
654
  function cancel_inline_edit(e, opts1) {
655
+ const isNode = typeof parent?.saltcorn?.data?.state === "undefined";
644
656
  var opts = JSON.parse(decodeURIComponent(opts1 || "") || "{}");
645
657
  var form = $(e.target).closest("form");
646
658
  var json_fk_opt;
@@ -668,17 +680,47 @@ function cancel_inline_edit(e, opts1) {
668
680
  <span class="current">${
669
681
  json_fk_opt || opts.current_label || opts.current
670
682
  }</span>
671
- <i class="editicon fas fa-edit ms-1"></i>
683
+ <i class="editicon ${!isNode ? "visible" : ""} fas fa-edit ms-1"></i>
672
684
  </div>`);
673
685
  initialize_page();
674
686
  }
675
687
 
688
+ function inline_submit_success(e, form, opts) {
689
+ const isNode = typeof parent?.saltcorn?.data?.state === "undefined";
690
+ const formDataArray = form.serializeArray();
691
+ if (opts) {
692
+ let rawVal = formDataArray.find((f) => f.name == opts.key).value;
693
+ let val =
694
+ opts.is_key || (opts.schema && opts.schema.type.startsWith("Key to "))
695
+ ? form.find("select").find("option:selected").text()
696
+ : rawVal;
697
+
698
+ $(e.target).replaceWith(`<div
699
+ data-inline-edit-field="${opts.key}"
700
+ ${opts.ajax ? `data-inline-edit-ajax="true"` : ""}
701
+ ${opts.type ? `data-inline-edit-type="${opts.type}"` : ""}
702
+ ${opts.current ? `data-inline-edit-current="${rawVal}"` : ""}
703
+ ${
704
+ opts.schema
705
+ ? `data-inline-edit-schema="${encodeURIComponent(
706
+ JSON.stringify(opts.schema)
707
+ )}"`
708
+ : ""
709
+ }
710
+ ${opts.current_label ? `data-inline-edit-current-label="${val}"` : ""}
711
+ data-inline-edit-dest-url="${opts.url}">
712
+ <span class="current">${val}</span>
713
+ <i class="editicon ${!isNode ? "visible" : ""} fas fa-edit ms-1"></i>
714
+ </div>`);
715
+ initialize_page();
716
+ } else location.reload();
717
+ }
718
+
676
719
  function inline_ajax_submit(e, opts1) {
677
720
  var opts = JSON.parse(decodeURIComponent(opts1 || "") || "{}");
678
721
  e.preventDefault();
679
722
  var form = $(e.target).closest("form");
680
723
  var form_data = form.serialize();
681
- var formDataArray = form.serializeArray();
682
724
  var url = form.attr("action");
683
725
  $.ajax(url, {
684
726
  type: "POST",
@@ -687,32 +729,7 @@ function inline_ajax_submit(e, opts1) {
687
729
  },
688
730
  data: form_data,
689
731
  success: function (res) {
690
- if (opts) {
691
- let rawVal = formDataArray.find((f) => f.name == opts.key).value;
692
- let val =
693
- opts.is_key || (opts.schema && opts.schema.type.startsWith("Key to "))
694
- ? form.find("select").find("option:selected").text()
695
- : rawVal;
696
-
697
- $(e.target).replaceWith(`<div
698
- data-inline-edit-field="${opts.key}"
699
- ${opts.ajax ? `data-inline-edit-ajax="true"` : ""}
700
- ${opts.type ? `data-inline-edit-type="${opts.type}"` : ""}
701
- ${opts.current ? `data-inline-edit-current="${rawVal}"` : ""}
702
- ${
703
- opts.schema
704
- ? `data-inline-edit-schema="${encodeURIComponent(
705
- JSON.stringify(opts.schema)
706
- )}"`
707
- : ""
708
- }
709
- ${opts.current_label ? `data-inline-edit-current-label="${val}"` : ""}
710
- data-inline-edit-dest-url="${opts.url}">
711
- <span class="current">${val}</span>
712
- <i class="editicon fas fa-edit ms-1"></i>
713
- </div>`);
714
- initialize_page();
715
- } else location.reload();
732
+ inline_submit_success(e, form, opts);
716
733
  },
717
734
  error: function (e) {
718
735
  ajax_done(
@@ -402,3 +402,20 @@ table.table-inner-grid td {
402
402
  div.unread-notify {
403
403
  border-left: 4px solid green;
404
404
  }
405
+
406
+ .mobile-data-inline-edit {
407
+ position: relative;
408
+ }
409
+
410
+ .mobile-data-inline-edit::after {
411
+ content: "";
412
+ position: absolute;
413
+ top: -25%;
414
+ left: 0%;
415
+ width: 100%;
416
+ height: 150%;
417
+ }
418
+
419
+ .mt-6 {
420
+ margin-top: 5rem;
421
+ }
@@ -79,7 +79,7 @@ function set_state_field(key, value) {
79
79
  function check_state_field(that) {
80
80
  const checked = that.checked;
81
81
  const name = that.name;
82
- const value = that.value;
82
+ const value = encodeURIComponent(that.value);
83
83
  var separator = window.location.href.indexOf("?") !== -1 ? "&" : "?";
84
84
  let dest;
85
85
  if (checked) dest = get_current_state_url() + `${separator}${name}=${value}`;
package/routes/admin.js CHANGED
@@ -990,6 +990,9 @@ router.post(
990
990
  child.stdout.on("data", (data) => {
991
991
  res.write(data);
992
992
  });
993
+ child.stderr.on("data", (data) => {
994
+ res.write(data);
995
+ });
993
996
  child.on("exit", function (code, signal) {
994
997
  res.end(
995
998
  req.__(
package/routes/api.js CHANGED
@@ -19,6 +19,7 @@ const Router = require("express-promise-router");
19
19
  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
+ const { prepare_update_row } = require("@saltcorn/data/web-mobile-commons");
22
23
  const Table = require("@saltcorn/data/models/table");
23
24
  const View = require("@saltcorn/data/models/view");
24
25
  //const Field = require("@saltcorn/data/models/field");
@@ -410,7 +411,8 @@ router.post(
410
411
  if (
411
412
  field.required &&
412
413
  !field.primary_key &&
413
- typeof row[field.name] === "undefined"
414
+ typeof row[field.name] === "undefined" &&
415
+ !field.attributes.default
414
416
  ) {
415
417
  hasErrors = true;
416
418
  errors.push(`${field.name}: required`);
@@ -459,39 +461,15 @@ router.post(
459
461
  return;
460
462
  }
461
463
  await passport.authenticate(
462
- "api-bearer",
464
+ ["api-bearer", "jwt"],
463
465
  { session: false },
464
466
  async function (err, user, info) {
465
467
  if (accessAllowedWrite(req, user, table)) {
466
468
  const { _versions, ...row } = req.body;
467
469
  const fields = table.getFields();
468
470
  readState(row, fields, req);
469
- let errors = [];
470
- let hasErrors = false;
471
- for (const k of Object.keys(row)) {
472
- const field = fields.find((f) => f.name === k);
473
- if (!field && k.includes(".")) {
474
- const [fnm, jkey] = k.split(".");
475
- const jfield = fields.find((f) => f.name === fnm);
476
- if (jfield?.type?.name === "JSON") {
477
- if (typeof row[fnm] === "undefined") {
478
- const dbrow = await table.getRow({ [table.pk_name]: id });
479
- row[fnm] = dbrow[fnm] || {};
480
- }
481
- row[fnm][jkey] = row[k];
482
- delete row[k];
483
- }
484
- } else if (!field || field.calculated) {
485
- delete row[k];
486
- } else if (field?.type && field.type.validate) {
487
- const vres = field.type.validate(field.attributes || {})(row[k]);
488
- if (vres.error) {
489
- hasErrors = true;
490
- errors.push(`${k}: ${vres.error}`);
491
- }
492
- }
493
- }
494
- if (hasErrors) {
471
+ const errors = await prepare_update_row(table, row, id);
472
+ if (errors.length > 0) {
495
473
  getState().log(
496
474
  2,
497
475
  `API POST ${table.name} error: ${errors.join(", ")}`
package/routes/fields.js CHANGED
@@ -86,7 +86,7 @@ const fieldForm = async (req, fkey_opts, existing_names, id, hasData) => {
86
86
  if (Field.labelToName(s) === "row")
87
87
  return req.__("Not a valid field name");
88
88
  try {
89
- new Function(s, "return;");
89
+ new Function(Field.labelToName(s), "return;");
90
90
  } catch {
91
91
  return req.__("Not a valid field name");
92
92
  }
@@ -471,14 +471,11 @@ const fieldFlow = (req) =>
471
471
  },
472
472
  {
473
473
  name: req.__("Default"),
474
- onlyWhen: async (context) => {
475
- if (!context.required || context.id || context.calculated)
476
- return false;
474
+ onlyWhen: async (context) => context.required && !context.calculated,
475
+
476
+ form: async (context) => {
477
477
  const table = await Table.findOne({ id: context.table_id });
478
478
  const nrows = await table.countRows();
479
- return nrows > 0;
480
- },
481
- form: async (context) => {
482
479
  const formfield = new Field({
483
480
  name: "default",
484
481
  label: req.__("Default"),
@@ -491,12 +488,28 @@ const fieldFlow = (req) =>
491
488
  },
492
489
  });
493
490
  await formfield.fill_fkey_options();
494
- return new Form({
495
- blurb: req.__(
496
- "A default value is required when adding required fields to nonempty tables"
497
- ),
498
- fields: [formfield],
491
+ const defaultOptional = nrows === 0 || context.id;
492
+ if (defaultOptional) formfield.showIf = { set_default: true };
493
+
494
+ const form = new Form({
495
+ blurb: defaultOptional
496
+ ? req.__("Set a default value for missing data")
497
+ : req.__(
498
+ "A default value is required when adding required fields to nonempty tables"
499
+ ),
500
+ fields: [
501
+ ...(defaultOptional
502
+ ? [{ name: "set_default", label: "Set Default", type: "Bool" }]
503
+ : []),
504
+ formfield,
505
+ ],
499
506
  });
507
+ if (
508
+ typeof context.default !== "undefined" &&
509
+ context.default !== null
510
+ )
511
+ form.values.set_default = true;
512
+ return form;
500
513
  },
501
514
  },
502
515
  ],
package/routes/sync.js CHANGED
@@ -1,53 +1,16 @@
1
1
  const { error_catcher } = require("./utils.js");
2
- const Table = require("@saltcorn/data/models/table");
3
2
  const Router = require("express-promise-router");
4
3
  const db = require("@saltcorn/data/db");
5
4
  const { getState } = require("@saltcorn/data/db/state");
5
+ const Table = require("@saltcorn/data/models/table");
6
6
 
7
7
  const router = new Router();
8
8
  module.exports = router;
9
9
 
10
- /**
11
- * Send all rows from a user, so that they can be used in an offline session with the mobile app
12
- */
13
- router.get(
14
- "/table_data",
15
- error_catcher(async (req, res) => {
16
- // TODO optimsie: hash over all rows or dynamic user specific
17
- // TODO public user
18
- // TODO split large data 10 000 rows?
19
- getState().log(
20
- 4,
21
- `GET /sync/table_data user: '${req.user ? req.user.id : "public"}'`
22
- );
23
- const allTables = await Table.find();
24
- const result = {};
25
- const selectOpts = req.user ? { forUser: req.user } : { forPublic: true };
26
- for (const table of allTables) {
27
- const rows = await table.getRows({}, selectOpts);
28
- if (
29
- req.user &&
30
- table.name === "users" &&
31
- !rows.find((row) => row.id === req.user.id)
32
- ) {
33
- rows.push(await table.getRow({ id: req.user.id }));
34
- }
35
- result[table.name] = {
36
- rows:
37
- table.name !== "users"
38
- ? rows
39
- : rows.map(({ id, email, role_id, language, disabled }) => {
40
- return { id, email, role_id, language, disabled };
41
- }),
42
- };
43
- }
44
- res.json(result);
45
- })
46
- );
47
-
48
10
  const pickFields = (table, row) => {
49
11
  const result = {};
50
12
  for (const { name, type } of table.getFields()) {
13
+ if (name === "id") continue;
51
14
  if (type?.name === "Date") {
52
15
  result[name] = row[name] ? new Date(row[name]) : undefined;
53
16
  } else {
@@ -57,114 +20,65 @@ const pickFields = (table, row) => {
57
20
  return result;
58
21
  };
59
22
 
60
- const getChanges = (table, dbRow, appRow) => {
61
- const changes = {};
62
- for (const { name, type } of table.getFields()) {
63
- if (name !== "id") {
64
- const dbVal = dbRow[name];
65
- const appVal = appRow[name];
66
- let valHasChanged = false;
67
- if (type?.name === "Date") {
68
- valHasChanged = dbVal?.valueOf() !== appVal?.valueOf();
69
- } else {
70
- valHasChanged = dbVal !== appVal;
71
- }
72
- // TODO Float with decimal_places
73
- if (valHasChanged) {
74
- changes[name] = appRow[name];
75
- }
76
- }
77
- }
78
- return changes;
79
- };
80
-
81
- const allowUpdate = (table, row, user) => {
82
- const role = user?.role_id || 100;
83
- return table.min_role_write >= role || table.is_owner(user, row);
84
- };
85
-
86
- const allowInsert = (table, row, user) => {
23
+ const allowInsert = (table, user) => {
87
24
  const role = user?.role_id || 100;
88
25
  return table.min_role_write >= role;
89
26
  };
90
27
 
91
- const syncRows = async (table, dbRows, appRows, user, dbClient) => {
92
- const dbRowsLookup = {};
93
- for (const row of dbRows) {
94
- dbRowsLookup[row.id] = row;
95
- }
96
- const translatedIds = [];
97
- for (const appRow of appRows.map((row) => pickFields(table, row))) {
98
- if (!appRow.id) continue;
99
- const dbRow = dbRowsLookup[appRow.id];
100
- if (dbRow) {
101
- const changes = getChanges(table, dbRow, appRow);
102
- if (Object.keys(changes).length > 0 && allowUpdate(table, dbRow, user)) {
103
- await db.update(table.name, changes, dbRow.id, { client: dbClient });
104
- }
105
- } else if (allowInsert(table, appRow, user)) {
106
- const idFromApp = appRow.id;
107
- delete appRow.id;
108
- const newId = await db.insert(table.name, appRow, { client: dbClient });
109
- if (newId !== idFromApp)
110
- translatedIds.push({ from: idFromApp, to: newId });
111
- } else {
112
- getState().log(
113
- 3,
114
- `Skipping id: '${appRow.id}' from app of table '${table.name}'`
115
- );
116
- }
117
- }
118
- return translatedIds;
28
+ const throwWithCode = (message, code) => {
29
+ const err = new Error(message);
30
+ err.statusCode = code;
31
+ throw err;
119
32
  };
120
33
 
121
34
  /**
122
- * Sync the database to the state of an offline session with the mobile app
35
+ * insert the offline data uploaded by the mobile-app
123
36
  */
124
37
  router.post(
125
38
  "/table_data",
126
39
  error_catcher(async (req, res) => {
127
- // TODO public user
128
40
  // TODO sqlite
129
41
  getState().log(
130
42
  4,
131
43
  `POST /sync/table_data user: '${req.user ? req.user.id : "public"}'`
132
44
  );
133
- const role = req.user ? req.user.role_id : 100;
45
+ let aborted = false;
46
+ req.socket.on("close", () => {
47
+ aborted = true;
48
+ });
49
+ req.socket.on("timeout", () => {
50
+ aborted = true;
51
+ });
134
52
  const client = db.isSQLite ? db : await db.getClient();
135
- const selectOpts = req.user ? { forUser: req.user } : { forPublic: true };
136
53
  try {
137
54
  await client.query("BEGIN");
138
55
  await client.query("SET CONSTRAINTS ALL DEFERRED");
139
- const translateIds = {};
140
- for (const [tblName, appRows] of Object.entries(req.body.data) || []) {
56
+ for (const [tblName, offlineRows] of Object.entries(req.body.data) ||
57
+ []) {
58
+ const table = Table.findOne({ name: tblName });
59
+ if (!table) throw new Error(`The table '${tblName}' does not exist.`);
60
+ if (!allowInsert(table, req.user))
61
+ throwWithCode(req.__("Not authorized"), 401);
141
62
  if (tblName !== "users") {
142
- const table = Table.findOne({ name: tblName });
143
- if (table) {
144
- const dbRows =
145
- role <= table.min_role_write
146
- ? await table.getRows({}, selectOpts)
147
- : (await table.getRows({}, selectOpts)).filter((row) =>
148
- table.is_owner(req.user, row)
149
- );
150
- const translated = await syncRows(
151
- table,
152
- dbRows,
153
- appRows,
154
- req.user,
155
- client
156
- );
157
- if (translated.length > 0) translateIds[tblName] = translated;
63
+ for (const newRow of offlineRows.map((row) =>
64
+ pickFields(table, row)
65
+ )) {
66
+ if (aborted) throw new Error("connection closed by client");
67
+ await db.insert(table.name, newRow, { client: client });
158
68
  }
159
69
  }
160
70
  }
71
+ if (aborted) throw new Error("connection closed by client");
161
72
  await client.query("COMMIT");
162
- if (!db.isSQLite) await client.release(true);
163
- res.json({ translateIds });
73
+ res.json({ success: true });
164
74
  } catch (error) {
165
75
  await client.query("ROLLBACK");
166
76
  getState().log(2, `POST /sync/table_data error: '${error.message}'`);
167
- res.status(400).json({ error: error.message || error });
77
+ res
78
+ .status(error.statusCode || 400)
79
+ .json({ error: error.message || error });
80
+ } finally {
81
+ if (!db.isSQLite) await client.release(true);
168
82
  }
169
83
  })
170
84
  );
package/routes/tables.js CHANGED
@@ -541,11 +541,7 @@ const attribBadges = (f) => {
541
541
  let s = "";
542
542
  if (f.attributes) {
543
543
  Object.entries(f.attributes).forEach(([k, v]) => {
544
- if (
545
- ["summary_field", "default", "on_delete_cascade", "on_delete"].includes(
546
- k
547
- )
548
- )
544
+ if (["summary_field", "on_delete_cascade", "on_delete"].includes(k))
549
545
  return;
550
546
  if (v || v === 0) s += badge("secondary", k);
551
547
  });
@@ -493,14 +493,22 @@ router.post(
493
493
  * @param {object} res
494
494
  * @returns {void}
495
495
  */
496
- const respondWorkflow = (view, wf, wfres, req, res) => {
496
+ const respondWorkflow = (view, wf, wfres, req, res, table) => {
497
497
  const wrap = (contents, noCard, previewURL) => ({
498
498
  above: [
499
499
  {
500
500
  type: "breadcrumbs",
501
501
  crumbs: [
502
502
  { text: req.__("Views"), href: "/viewedit" },
503
- { href: `/view/${view.name}`, text: view.name },
503
+ {
504
+ href: `/view/${view.name}`,
505
+ text: view.name,
506
+ postLinkText: `[${view.viewtemplate}${
507
+ table
508
+ ? ` on ${a({ href: `/table/` + table.name }, table.name)}`
509
+ : ""
510
+ }]`,
511
+ },
504
512
  { workflow: wf, step: wfres },
505
513
  ],
506
514
  },
@@ -584,6 +592,9 @@ router.get(
584
592
  (view.configuration?.columns || []).forEach((c) => {
585
593
  c._columndef = JSON.stringify(c);
586
594
  });
595
+ let table;
596
+ if (view.table_id) table = Table.findOne({ id: view.table_id });
597
+ if (view.exttable_name) table = Table.findOne({ name: view.exttable_name });
587
598
  const configFlow = await view.get_config_flow(req);
588
599
  const hasConfig =
589
600
  view.configuration && Object.keys(view.configuration).length > 0;
@@ -598,7 +609,7 @@ router.get(
598
609
  },
599
610
  req
600
611
  );
601
- respondWorkflow(view, configFlow, wfres, req, res);
612
+ respondWorkflow(view, configFlow, wfres, req, res, table);
602
613
  })
603
614
  );
604
615
 
@@ -617,7 +628,11 @@ router.post(
617
628
  const view = await View.findOne({ name });
618
629
  const configFlow = await view.get_config_flow(req);
619
630
  const wfres = await configFlow.run(req.body, req);
620
- respondWorkflow(view, configFlow, wfres, req, res);
631
+
632
+ let table;
633
+ if (view.table_id) table = Table.findOne({ id: view.table_id });
634
+ if (view.exttable_name) table = Table.findOne({ name: view.exttable_name });
635
+ respondWorkflow(view, configFlow, wfres, req, res, table);
621
636
  })
622
637
  );
623
638
 
@@ -2,9 +2,10 @@ const request = require("supertest");
2
2
  const getApp = require("../app");
3
3
  const {
4
4
  getUserLoginCookie,
5
- getStaffLoginCookie,
6
5
  getAdminLoginCookie,
7
6
  resetToFixtures,
7
+ notAuthorized,
8
+ respondJsonWith,
8
9
  } = require("../auth/testhelp");
9
10
  const db = require("@saltcorn/data/db");
10
11
 
@@ -15,42 +16,16 @@ beforeAll(async () => {
15
16
  });
16
17
  afterAll(db.close);
17
18
 
18
- describe("Load offline data", () => {
19
- it("public request", async () => {
20
- const app = await getApp({ disableCsrf: true });
21
- const resp = await request(app).get("/sync/table_data");
22
- for (const [k, v] of Object.entries(resp._body)) {
23
- expect(v.rows.length).toBe(k === "books" ? 2 : 0);
24
- }
25
- });
26
-
27
- it("user request", async () => {
28
- const app = await getApp({ disableCsrf: true });
29
- const loginCookie = await getUserLoginCookie();
30
- const resp = await request(app)
31
- .get("/sync/table_data")
32
- .set("Cookie", loginCookie);
33
- const data = resp._body;
34
- expect(data.patients.rows.length).toBe(0);
35
- });
36
-
37
- it("admin request", async () => {
38
- const app = await getApp({ disableCsrf: true });
39
- const loginCookie = await getAdminLoginCookie();
40
- const resp = await request(app)
41
- .get("/sync/table_data")
42
- .set("Cookie", loginCookie);
43
- const data = resp._body;
44
- expect(data.patients.rows.length).toBe(2);
45
- });
46
- });
47
-
48
19
  describe("Synchronise with mobile offline data", () => {
49
- if (!db.isSQLite) {
50
- it("not permitted", async () => {
20
+ it("not permitted", async () => {
21
+ if (!db.isSQLite) {
22
+ const patients = Table.findOne({ name: "patients" });
23
+ const books = Table.findOne({ name: "books" });
24
+ const patientsBefore = await patients.countRows();
25
+ const booksBefore = await books.countRows();
51
26
  const app = await getApp({ disableCsrf: true });
52
27
  const loginCookie = await getUserLoginCookie();
53
- const uploadResp = await request(app)
28
+ await request(app)
54
29
  .post("/sync/table_data")
55
30
  .set("Cookie", loginCookie)
56
31
  .send({
@@ -68,26 +43,35 @@ describe("Synchronise with mobile offline data", () => {
68
43
  parent: 1,
69
44
  },
70
45
  ],
46
+ books: [
47
+ {
48
+ id: 3,
49
+ author: "foo",
50
+ pages: 20,
51
+ publisher: 1,
52
+ },
53
+ ],
71
54
  },
72
- });
73
- const translateIds = uploadResp._body.translateIds;
74
- expect(translateIds).toBeDefined();
75
- expect(Object.keys(translateIds).length).toBe(0);
76
-
77
- const adminCookie = await getAdminLoginCookie();
78
- const downloadResp = await request(app)
79
- .get("/sync/table_data")
80
- .set("Cookie", adminCookie);
81
- const data = downloadResp._body;
82
- expect(data.patients.rows.length).toBe(2);
83
- });
55
+ })
56
+ .expect(notAuthorized);
57
+ const patientsAfter = await patients.countRows();
58
+ const booksAfter = await books.countRows();
59
+ expect(patientsAfter).toBe(patientsBefore);
60
+ expect(booksAfter).toBe(booksBefore);
61
+ }
62
+ });
84
63
 
85
- it("upload patients and books", async () => {
64
+ it("upload patients and books", async () => {
65
+ if (!db.isSQLite) {
66
+ const patients = Table.findOne({ name: "patients" });
67
+ const books = Table.findOne({ name: "books" });
68
+ const patientsBefore = await patients.countRows();
69
+ const booksBefore = await books.countRows();
86
70
  const app = await getApp({ disableCsrf: true });
87
- const adminCookie = await getAdminLoginCookie();
88
- const uploadResp = await request(app)
71
+ const loginCookie = await getAdminLoginCookie();
72
+ await request(app)
89
73
  .post("/sync/table_data")
90
- .set("Cookie", adminCookie)
74
+ .set("Cookie", loginCookie)
91
75
  .send({
92
76
  data: {
93
77
  patients: [
@@ -97,7 +81,7 @@ describe("Synchronise with mobile offline data", () => {
97
81
  parent: 1,
98
82
  },
99
83
  {
100
- id: 84, // will be translated to 3
84
+ id: 84,
101
85
  name: "Pitt Brad",
102
86
  favbook: 2,
103
87
  parent: 1,
@@ -105,83 +89,20 @@ describe("Synchronise with mobile offline data", () => {
105
89
  ],
106
90
  books: [
107
91
  {
108
- id: 3, // stays at 3
92
+ id: 3,
109
93
  author: "foo",
110
94
  pages: 20,
111
95
  publisher: 1,
112
96
  },
113
97
  ],
114
98
  },
115
- });
116
- const translateIds = uploadResp._body.translateIds;
117
- expect(translateIds).toBeDefined();
118
- expect(Object.keys(translateIds).length).toBe(1);
119
- expect(translateIds.patients.length).toBe(1);
120
- expect(translateIds.patients[0]).toEqual({ from: 84, to: 3 });
121
-
122
- const staffCookie = await getStaffLoginCookie();
123
- const downloadResp = await request(app)
124
- .get("/sync/table_data")
125
- .set("Cookie", staffCookie);
126
- const data = downloadResp._body;
127
- expect(data.patients.rows.length).toBe(3);
128
- expect(data.books.rows.length).toBe(3);
129
- });
130
-
131
- it("upload with ownership_field", async () => {
132
- const messagesTbl = Table.findOne({ name: "messages" });
133
- const userField = messagesTbl
134
- .getFields()
135
- .find((field) => field.name === "user");
136
- await messagesTbl.update({
137
- min_role_read: 1,
138
- min_role_write: 1,
139
- ownership_field_id: userField.id,
140
- });
141
- const staffMsgId = await db.insert("messages", {
142
- content: "message from staff",
143
- user: 2,
144
- room: 1,
145
- });
146
- const userMsgId = await db.insert("messages", {
147
- content: "message from user",
148
- user: 3,
149
- room: 1,
150
- });
151
-
152
- const app = await getApp({ disableCsrf: true });
153
- const userCookie = await getUserLoginCookie();
154
- const uploadResp = await request(app)
155
- .post("/sync/table_data")
156
- .set("Cookie", userCookie)
157
- .send({
158
- data: {
159
- messages: [
160
- {
161
- id: staffMsgId, // will be skipped
162
- user: 3,
163
- room: 1,
164
- content: "offline change",
165
- },
166
- {
167
- id: userMsgId, // will be updated because user is the owner
168
- user: 2,
169
- room: 1,
170
- content: "offline change",
171
- },
172
- ],
173
- },
174
- });
175
- const translateIds = uploadResp._body.translateIds;
176
- expect(translateIds).toBeDefined();
177
- expect(Object.keys(translateIds).length).toBe(0);
178
- // load the admin data
179
- const adminCookie = await getAdminLoginCookie();
180
- const resp = await request(app)
181
- .get("/sync/table_data")
182
- .set("Cookie", adminCookie);
183
- const data = resp._body;
184
- expect(data.messages.rows.length).toBe(4);
185
- });
186
- }
99
+ })
100
+ .expect(respondJsonWith(200, ({ success }) => success));
101
+ const patientsAfter = await patients.countRows();
102
+ const booksAfter = await books.countRows();
103
+ expect(patientsAfter).toBe(patientsBefore + 2);
104
+ expect(booksAfter).toBe(booksBefore + 1);
105
+ expect((await patients.getRows({ id: 84 })).length).toBe(0);
106
+ }
107
+ });
187
108
  });