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

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.4",
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.4",
10
+ "@saltcorn/builder": "0.8.6-beta.4",
11
+ "@saltcorn/data": "0.8.6-beta.4",
12
+ "@saltcorn/admin-models": "0.8.6-beta.4",
13
+ "@saltcorn/filemanager": "0.8.6-beta.4",
14
+ "@saltcorn/markup": "0.8.6-beta.4",
15
+ "@saltcorn/sbadmin2": "0.8.6-beta.4",
16
16
  "@socket.io/cluster-adapter": "^0.2.1",
17
17
  "@socket.io/sticky": "^1.0.1",
18
18
  "adm-zip": "0.5.10",
@@ -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/api.js CHANGED
@@ -410,7 +410,8 @@ router.post(
410
410
  if (
411
411
  field.required &&
412
412
  !field.primary_key &&
413
- typeof row[field.name] === "undefined"
413
+ typeof row[field.name] === "undefined" &&
414
+ !field.attributes.default
414
415
  ) {
415
416
  hasErrors = true;
416
417
  errors.push(`${field.name}: required`);
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
  });
@@ -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
  });