@saltcorn/data 0.5.5-beta.1 → 0.5.6-beta.2

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.
@@ -522,6 +522,9 @@ const authorise_post = async ({ body, table_id, req }) => {
522
522
  const field_name = await table.owner_fieldname();
523
523
  return field_name && `${body[field_name]}` === `${user_id}`;
524
524
  }
525
+ if (table.ownership_formula && user_id) {
526
+ return await table.is_owner(req.user, body);
527
+ }
525
528
  if (table.name === "users" && `${body.id}` === `${user_id}`) return true;
526
529
  return false;
527
530
  };
@@ -103,6 +103,10 @@ const run = async (table_id, viewname, { columns, layout }, state, extra) => {
103
103
  if (kpath.length === 3) {
104
104
  const [jtNm, jFieldNm, lblField] = kpath;
105
105
  const jtable = await Table.findOne({ name: jtNm });
106
+ if (!jtable)
107
+ throw new InvalidConfiguration(
108
+ `View ${viewname} incorrectly configured: cannot find join table ${jtNm}`
109
+ );
106
110
  const jfields = await jtable.getFields();
107
111
  const jfield = jfields.find((f) => f.name === lblField);
108
112
  if (jfield)
@@ -344,6 +344,13 @@ const renderRows = async (
344
344
  segment.contents = await view.run(state, extra);
345
345
  }
346
346
  });
347
+ const user_id = extra.req.user ? extra.req.user.id : null;
348
+
349
+ const is_owner =
350
+ table.ownership_formula && user_id
351
+ ? await table.is_owner(extra.req.user, row)
352
+ : owner_field && user_id && row[owner_field] === user_id;
353
+
347
354
  return render(
348
355
  row,
349
356
  fields,
@@ -352,7 +359,7 @@ const renderRows = async (
352
359
  table,
353
360
  role,
354
361
  extra.req,
355
- owner_field
362
+ is_owner
356
363
  );
357
364
  });
358
365
  };
@@ -393,18 +400,7 @@ const runMany = async (
393
400
  return rendered.map((html, ix) => ({ html, row: rows[ix] }));
394
401
  };
395
402
 
396
- const render = (
397
- row,
398
- fields,
399
- layout0,
400
- viewname,
401
- table,
402
- role,
403
- req,
404
- owner_field
405
- ) => {
406
- const user_id = req.user ? req.user.id : null;
407
- const is_owner = owner_field && user_id && row[owner_field] === user_id;
403
+ const render = (row, fields, layout0, viewname, table, role, req, is_owner) => {
408
404
  const evalMaybeExpr = (segment, key, fmlkey) => {
409
405
  if (segment.isFormula && segment.isFormula[fmlkey || key]) {
410
406
  const f = get_expression_function(segment[key], fields);
@@ -261,11 +261,7 @@ const get_viewable_fields = contract(
261
261
  label: column.header_label ? text(__(column.header_label)) : "",
262
262
  key: (r) => {
263
263
  if (action_requires_write(column.action_name)) {
264
- const owner_field = table.owner_fieldname_from_fields(fields);
265
- if (
266
- table.min_role_write < role &&
267
- (!owner_field || r[owner_field] !== user_id)
268
- )
264
+ if (table.min_role_write < role && !table.is_owner(req.user, r))
269
265
  return "";
270
266
  }
271
267
  const url = action_url(
@@ -524,9 +520,11 @@ const fill_presets = async (table, req, fixed) => {
524
520
  if (fixed[k]) {
525
521
  const fldnm = k.replace("preset_", "");
526
522
  const fld = fields.find((f) => f.name === fldnm);
527
- if (table.name === "users" && fld.primary_key)
528
- fixed[fldnm] = req.user ? req.user.id : null;
529
- else fixed[fldnm] = fld.presets[fixed[k]]({ user: req.user, req });
523
+ if (fld) {
524
+ if (table.name === "users" && fld.primary_key)
525
+ fixed[fldnm] = req.user ? req.user.id : null;
526
+ else fixed[fldnm] = fld.presets[fixed[k]]({ user: req.user, req });
527
+ }
530
528
  }
531
529
  delete fixed[k];
532
530
  } else {
package/db/state.js CHANGED
@@ -46,6 +46,7 @@ class State {
46
46
  this.viewtemplates = {};
47
47
  this.tables = [];
48
48
  this.types = {};
49
+ this.stashed_fieldviews = {};
49
50
  this.files = {};
50
51
  this.pages = [];
51
52
  this.fields = [];
@@ -355,6 +356,10 @@ class State {
355
356
  if (type) {
356
357
  if (type.fieldviews) type.fieldviews[k] = v;
357
358
  else type.fieldviews = { [k]: v };
359
+ } else {
360
+ if (!this.stashed_fieldviews[v.type])
361
+ this.stashed_fieldviews[v.type] = {};
362
+ this.stashed_fieldviews[v.type][k] = v;
358
363
  }
359
364
  });
360
365
  const layout = withCfg("layout");
@@ -383,7 +388,13 @@ class State {
383
388
  * @param t
384
389
  */
385
390
  addType(t) {
386
- this.types[t.name] = { ...t, fieldviews: { ...t.fieldviews } };
391
+ this.types[t.name] = {
392
+ ...t,
393
+ fieldviews: {
394
+ ...t.fieldviews,
395
+ ...(this.stashed_fieldviews[t.name] || {}),
396
+ },
397
+ };
387
398
  }
388
399
 
389
400
  /**
@@ -405,6 +416,7 @@ class State {
405
416
  async refresh_plugins(noSignal) {
406
417
  this.viewtemplates = {};
407
418
  this.types = {};
419
+ this.stashed_fieldviews = {};
408
420
  this.fields = [];
409
421
  this.fileviews = {};
410
422
  this.actions = {};
@@ -431,7 +443,9 @@ class State {
431
443
  this.pages.forEach((p) => strings.push(...p.getStringsForI18n()));
432
444
  const menu = this.getConfig("menu_items", []);
433
445
  strings.push(...menu.map(({ label }) => label));
434
- return Array.from(new Set(strings)).filter(removeAllWhiteSpace);
446
+ return Array.from(new Set(strings)).filter(
447
+ (s) => s && removeAllWhiteSpace(s)
448
+ );
435
449
  }
436
450
 
437
451
  setRoomEmitter(f) {
@@ -0,0 +1,3 @@
1
+ const sql = "alter table _sc_tables add column ownership_formula text";
2
+
3
+ module.exports = { sql };
package/models/backup.js CHANGED
@@ -6,6 +6,7 @@ const View = require("./view");
6
6
  const File = require("./file");
7
7
  const Plugin = require("./plugin");
8
8
  const User = require("./user");
9
+ const Role = require("./role");
9
10
  const Page = require("./page");
10
11
  const Zip = require("adm-zip");
11
12
  const tmp = require("tmp-promise");
@@ -26,6 +27,7 @@ const {
26
27
  const { is_plugin } = require("../contracts");
27
28
 
28
29
  const { asyncMap } = require("../utils");
30
+ const Trigger = require("./trigger");
29
31
 
30
32
  const create_pack = contract(
31
33
  is.fun(is.str, is.promise(is.undefined)),
@@ -46,7 +48,9 @@ const create_pack = contract(
46
48
  await Page.find({}),
47
49
  async (v) => await page_pack(v.name)
48
50
  );
49
- var pack = { tables, views, plugins, pages };
51
+ const triggers = (await Trigger.find({})).map((tr) => tr.toJson);
52
+ const roles = await Role.find({});
53
+ var pack = { tables, views, plugins, pages, triggers, roles };
50
54
 
51
55
  await fs.writeFile(path.join(dirpath, "pack.json"), JSON.stringify(pack));
52
56
  }
@@ -175,7 +179,8 @@ const restore_files = contract(
175
179
  //set location
176
180
  file.location = newPath;
177
181
  //insert in db
178
- await db.insert("_sc_files", file);
182
+ const { user_id, ...file_row } = file;
183
+ await db.insert("_sc_files", file_row);
179
184
  }
180
185
  }
181
186
  }
package/models/pack.js CHANGED
@@ -10,6 +10,8 @@ const { contract, is } = require("contractis");
10
10
  const Page = require("./page");
11
11
  const { is_pack, is_plugin } = require("../contracts");
12
12
  const TableConstraint = require("./table_constraints");
13
+ const { tr } = require("@saltcorn/markup/tags");
14
+ const Role = require("./role");
13
15
 
14
16
  const pack_fun = is.fun(is.str, is.promise(is.obj()));
15
17
 
@@ -21,7 +23,7 @@ const table_pack = contract(pack_fun, async (name) => {
21
23
  delete o.table_id;
22
24
  return o;
23
25
  };
24
- const triggers = await Trigger.find({ table_id: table.id });
26
+ //const triggers = await Trigger.find({ table_id: table.id });
25
27
  const constraints = await TableConstraint.find({ table_id: table.id });
26
28
 
27
29
  return {
@@ -29,8 +31,9 @@ const table_pack = contract(pack_fun, async (name) => {
29
31
  min_role_read: table.min_role_read,
30
32
  min_role_write: table.min_role_write,
31
33
  versioned: table.versioned,
34
+ ownership_formula: table.ownership_formula,
32
35
  fields: fields.map((f) => strip_ids(f.toJson)),
33
- triggers: triggers.map((tr) => tr.toJson),
36
+ //triggers: triggers.map((tr) => tr.toJson),
34
37
  constraints: constraints.map((c) => c.toJson),
35
38
  ownership_field_name: table.owner_fieldname_from_fields(fields),
36
39
  };
@@ -200,6 +203,9 @@ const install_pack = contract(
200
203
  await loadAndSaveNewPlugin(p);
201
204
  }
202
205
  }
206
+ for (const role of pack.roles || []) {
207
+ await Role.create(role);
208
+ }
203
209
  for (const tableSpec of pack.tables) {
204
210
  if (tableSpec.name !== "users") {
205
211
  const table = await Table.create(tableSpec.name, tableSpec);
@@ -226,7 +232,7 @@ const install_pack = contract(
226
232
  }
227
233
  }
228
234
  for (const trigger of tableSpec.triggers || [])
229
- await Trigger.create({ table, ...trigger });
235
+ await Trigger.create({ table, ...trigger }); //legacy, not in new packs
230
236
  for (const constraint of tableSpec.constraints || [])
231
237
  await TableConstraint.create({ table, ...constraint });
232
238
  if (tableSpec.ownership_field_name) {
@@ -258,6 +264,10 @@ const install_pack = contract(
258
264
  min_role: viewSpec.min_role || 10,
259
265
  });
260
266
  }
267
+ for (const triggerSpec of pack.triggers || []) {
268
+ await Trigger.create(triggerSpec);
269
+ }
270
+
261
271
  for (const pageFullSpec of pack.pages || []) {
262
272
  const { root_page_for_roles, menu_label, ...pageSpec } = pageFullSpec;
263
273
  await Page.create(pageSpec);
package/models/table.js CHANGED
@@ -10,6 +10,7 @@ const {
10
10
  apply_calculated_fields,
11
11
  apply_calculated_fields_stored,
12
12
  recalculate_for_stored,
13
+ get_expression_function,
13
14
  } = require("./expression");
14
15
  const { contract, is } = require("contractis");
15
16
  const { is_table_query } = require("../contracts");
@@ -87,6 +88,7 @@ class Table {
87
88
  this.min_role_read = o.min_role_read;
88
89
  this.min_role_write = o.min_role_write;
89
90
  this.ownership_field_id = o.ownership_field_id;
91
+ this.ownership_formula = o.ownership_formula;
90
92
  this.versioned = !!o.versioned;
91
93
  this.external = false;
92
94
  this.description = o.description;
@@ -178,11 +180,10 @@ class Table {
178
180
  * Get owner column name
179
181
  * @returns {Promise<string|null|*>}
180
182
  */
181
- async owner_fieldname() {
183
+ owner_fieldname() {
182
184
  if (this.name === "users") return "id";
183
185
  if (!this.ownership_field_id) return null;
184
- const fields = await this.getFields();
185
- return this.owner_fieldname_from_fields(fields);
186
+ return this.owner_fieldname_from_fields(this.fields);
186
187
  }
187
188
 
188
189
  /**
@@ -191,9 +192,13 @@ class Table {
191
192
  * @param row - table row
192
193
  * @returns {Promise<string|null|*|boolean>}
193
194
  */
194
- async is_owner(user, row) {
195
+ is_owner(user, row) {
195
196
  if (!user) return false;
196
- const field_name = await this.owner_fieldname();
197
+ if (this.ownership_formula) {
198
+ const f = get_expression_function(this.ownership_formula, this.fields);
199
+ return f(row, user);
200
+ }
201
+ const field_name = this.owner_fieldname();
197
202
  return field_name && row[field_name] === user.id;
198
203
  }
199
204
 
@@ -218,6 +223,7 @@ class Table {
218
223
  min_role_read: options.min_role_read || 1,
219
224
  min_role_write: options.min_role_write || 1,
220
225
  ownership_field_id: options.ownership_field_id,
226
+ ownership_formula: options.ownership_formula,
221
227
  description: options.description || "",
222
228
  };
223
229
  // insert table defintion into _sc_tables
@@ -990,10 +996,18 @@ class Table {
990
996
  const throughTable = await Table.findOne({
991
997
  name: reffield.reftable_name,
992
998
  });
999
+ if (!throughTable)
1000
+ throw new InvalidConfiguration(
1001
+ `Join-through table ${reffield.reftable_name} not found`
1002
+ );
993
1003
  const throughTableFields = await throughTable.getFields();
994
1004
  const throughRefField = throughTableFields.find(
995
1005
  (f) => f.name === through
996
1006
  );
1007
+ if (!throughRefField)
1008
+ throw new InvalidConfiguration(
1009
+ `Reference field field ${through} not found in table ${throughTable.name}`
1010
+ );
997
1011
  const finalTable = throughRefField.reftable_name;
998
1012
  const jtNm1 = `${sqlsanitize(reftable)}_jt_${sqlsanitize(
999
1013
  through
package/models/trigger.js CHANGED
@@ -39,13 +39,19 @@ class Trigger {
39
39
  * @returns {{when_trigger, configuration: any, name, description, action}}
40
40
  */
41
41
  get toJson() {
42
+ let table_name = this.table_name;
43
+ if (!table_name && this.table_id) {
44
+ const Table = require("./table");
45
+ const table = Table.find(+this.table_id);
46
+ table_name = table.name;
47
+ }
42
48
  return {
43
49
  name: this.name,
44
50
  description: this.description,
45
51
  action: this.action,
46
52
  when_trigger: this.when_trigger,
47
53
  configuration: this.configuration,
48
- table_id: this.table_id,
54
+ table_name,
49
55
  channel: this.channel,
50
56
  min_role: this.min_role,
51
57
  };
@@ -116,6 +122,11 @@ class Trigger {
116
122
  static async create(f) {
117
123
  const trigger = new Trigger(f);
118
124
  const { id, table_name, ...rest } = trigger;
125
+ if (table_name && !rest.table_id) {
126
+ const Table = require("./table");
127
+ const table = Table.find(table_name);
128
+ rest.table_id = table.id;
129
+ }
119
130
  const fid = await db.insert("_sc_triggers", rest);
120
131
  trigger.id = fid;
121
132
  await require("../db/state").getState().refresh_triggers();
package/models/user.js CHANGED
@@ -5,6 +5,24 @@ const { v4: uuidv4 } = require("uuid");
5
5
  const dumbPasswords = require("dumb-passwords");
6
6
  const validator = require("email-validator");
7
7
 
8
+ const safeUserFields = (o) => {
9
+ const {
10
+ email,
11
+ password,
12
+ language,
13
+ _attributes,
14
+ api_token,
15
+ verification_token,
16
+ verified_on,
17
+ disabled,
18
+ id,
19
+ reset_password_token,
20
+ reset_password_expiry,
21
+ role_id,
22
+ ...rest
23
+ } = o;
24
+ return rest;
25
+ };
8
26
  /**
9
27
  * User
10
28
  */
@@ -33,22 +51,7 @@ class User {
33
51
  : o.reset_password_expiry || null;
34
52
  this.role_id = o.role_id ? +o.role_id : 8;
35
53
 
36
- const {
37
- email,
38
- password,
39
- language,
40
- _attributes,
41
- api_token,
42
- verification_token,
43
- verified_on,
44
- disabled,
45
- id,
46
- reset_password_token,
47
- reset_password_expiry,
48
- role_id,
49
- ...rest
50
- } = o;
51
- Object.assign(this, rest);
54
+ Object.assign(this, safeUserFields(o));
52
55
 
53
56
  contract.class(this);
54
57
  }
@@ -149,13 +152,15 @@ class User {
149
152
  * @returns {{role_id: number, language, id, email, tenant: *}}
150
153
  */
151
154
  get session_object() {
152
- return {
155
+ const so = {
153
156
  email: this.email,
154
157
  id: this.id,
155
158
  role_id: this.role_id,
156
159
  language: this.language,
157
160
  tenant: db.getTenantSchema(),
158
161
  };
162
+ Object.assign(so, safeUserFields(this));
163
+ return so;
159
164
  }
160
165
 
161
166
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saltcorn/data",
3
- "version": "0.5.5-beta.1",
3
+ "version": "0.5.6-beta.2",
4
4
  "description": "Data models for Saltcorn, open-source no-code platform",
5
5
  "homepage": "https://saltcorn.com",
6
6
  "scripts": {
@@ -10,7 +10,7 @@
10
10
  "license": "MIT",
11
11
  "main": "index.js",
12
12
  "dependencies": {
13
- "@saltcorn/markup": "0.5.5-beta.1",
13
+ "@saltcorn/markup": "0.5.6-beta.2",
14
14
  "acorn": "^8.0.3",
15
15
  "adm-zip": "0.5.5",
16
16
  "astring": "^1.4.3",
@@ -30,7 +30,7 @@
30
30
  "latest-version": "^5.1.0",
31
31
  "moment": "^2.27.0",
32
32
  "moment-timezone": "^0.5.33",
33
- "node-fetch": "^2.6.1",
33
+ "node-fetch": "2.6.2",
34
34
  "nodemailer": "^6.4.17",
35
35
  "pg": "^8.2.1",
36
36
  "pg-copy-streams": "^5.1.1",
package/plugin-helper.js CHANGED
@@ -891,7 +891,7 @@ const stateFieldsToWhere = contract(
891
891
  if (kpath.length === 3) {
892
892
  const [jtNm, jFieldNm, lblField] = kpath;
893
893
  qstate.id = [
894
- ...(qstate.id || []),
894
+ ...(qstate.id ? [qstate.id] : []),
895
895
  {
896
896
  // where id in (select jFieldNm from jtnm where lblField=v)
897
897
  inSelect: {
@@ -882,6 +882,42 @@ describe("Table with row ownership", () => {
882
882
  await persons.delete();
883
883
  });
884
884
  });
885
+ describe("Table with row ownership", () => {
886
+ it("should create and delete table", async () => {
887
+ const persons = await Table.create("TableOwnedFml");
888
+ const name = await Field.create({
889
+ table: persons,
890
+ name: "name",
891
+ type: "String",
892
+ });
893
+ const age = await Field.create({
894
+ table: persons,
895
+ name: "age",
896
+ type: "String",
897
+ });
898
+ const owner = await Field.create({
899
+ table: persons,
900
+ name: "owner",
901
+ type: "Key to users",
902
+ });
903
+ await persons.update({ ownership_formula: "user.id===owner" });
904
+ if (!db.isSQLite) {
905
+ await age.update({ type: "Integer" });
906
+ await name.update({ name: "lastname" });
907
+ await persons.insertRow({ lastname: "Joe", age: 12 });
908
+ await persons.insertRow({ lastname: "Sam", age: 13, owner: 1 });
909
+ const row = await persons.getRow({ age: 12 });
910
+ expect(row.lastname).toBe("Joe");
911
+ expect(row.age).toBe(12);
912
+ const is_owner = await persons.is_owner({ id: 6 }, row);
913
+ expect(is_owner).toBe(false);
914
+ const row1 = await persons.getRow({ age: 13 });
915
+ const is_owner1 = await persons.is_owner({ id: 1 }, row1);
916
+ expect(is_owner1).toBe(true);
917
+ }
918
+ await persons.delete();
919
+ });
920
+ });
885
921
  describe("Table with UUID pks", () => {
886
922
  if (!db.isSQLite) {
887
923
  it("should select uuid", async () => {