@saltcorn/server 0.8.9 → 0.9.0-beta.0

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.
@@ -0,0 +1,23 @@
1
+ Table constraints in Saltcorn are used to impose constraints on the data that
2
+ can be held in individual rows or to alter the performance characteristics of
3
+ the table.
4
+
5
+ There are three types of table constraints:
6
+
7
+ A __unique constraint__ can be used to impose a restriction that a combination
8
+ of fields is unique
9
+ across the whole table. For instance, if you have a table for people and you
10
+ mark `name` and `age` as jointly unique using a unique constraint then you cannot
11
+ have two rows with the same name and age; but you could have two rows with the
12
+ same name.
13
+
14
+ A __formula constraint__ is used to impose restrictions on the content of
15
+ each row which can use several Fields. For instance if you have from and to
16
+ date fields, you can use the formula constraint to ensure that the to date is
17
+ always after the from date.
18
+
19
+ An __index__ does not put any limitations or constraints
20
+ on the table data. It adds an index to the table to accelerate queries from
21
+ individual fields, at the expense of using more disk space. You should put an
22
+ index on fields in large tables that are frequently queried either through
23
+ filters or aggregations.
@@ -0,0 +1,43 @@
1
+ The constraint formula should be a JavaScript expression which evaluates to a
2
+ boolean (true or false). If the formula evaluates to false then the row is not
3
+ valid and will not be accepted in the table. If you are editing an existing row
4
+ then the edit will not be accepted. The supplied error message text will be shown
5
+ to the user. 
6
+
7
+ Some examples:
8
+
9
+ {{# for (const fml of table.getFormulaExamples("Bool")) { }} * `{{= fml}}`
10
+ {{# } }}
11
+
12
+ This formula can use any of the fields in table {{= table.name }} as variables:
13
+
14
+ | Field | Variable name | Type |
15
+ | ----- | ------------- | ---- |
16
+ {{# for (const field of table.fields) { }} | {{= field.label }} | `{{= field.name }}` | {{= field.pretty_type }} |
17
+ {{# } }}
18
+
19
+
20
+
21
+
22
+ {{# if(table.fields.some(f=>f.is_fkey)) { }}
23
+
24
+ You can also use join fields on related tables. These are accessed with the dot object access
25
+ notation, in the form:
26
+
27
+ ```
28
+ {key field}.{field on referenced table}
29
+ ```
30
+
31
+ The first-order join fields you can use in the constraint formula are:
32
+
33
+ {{# for (const field of table.fields.filter(f=>f.is_fkey && f.reftable_name)) { }}
34
+ {{# const reftable = Table.findOne( field.reftable_name); }}
35
+ * Through {{=field.label}} key field: {{= table.fields.map(jf=>`\`${field.name}.${jf.name}\``).join(", ") }}
36
+
37
+
38
+ {{# } }}
39
+
40
+ If you use join fields, Saltcorn is not able to create a constraint in the SQL database. In that case, it will not check existing
41
+ rows, and it will also not be able to enforce constraints involving the primary key value on newly created rows.
42
+
43
+ {{# } }}
package/help/index.js ADDED
@@ -0,0 +1,42 @@
1
+ const Table = require("@saltcorn/data/models/table");
2
+ const File = require("@saltcorn/data/models/file");
3
+ const _ = require("underscore");
4
+ const fs = require("fs").promises;
5
+ const MarkdownIt = require("markdown-it"),
6
+ md = new MarkdownIt();
7
+
8
+ const { pre } = require("@saltcorn/markup/tags");
9
+
10
+ const get_md_file = async (topic) => {
11
+ try {
12
+ const fp = require.resolve(`./${File.normalise(topic)}.tmd`);
13
+ const fileBuf = await fs.readFile(fp);
14
+ return fileBuf.toString();
15
+ } catch (e) {
16
+ return false;
17
+ }
18
+ };
19
+
20
+ _.templateSettings = {
21
+ evaluate: /\{\{#(.+?)\}\}/g,
22
+ interpolate: /\{\{=(.+?)\}\}/g,
23
+ };
24
+
25
+ const get_help_markup = async (topic, query, req) => {
26
+ try {
27
+ const context = { user: req.user, Table };
28
+ if (query.table) {
29
+ context.table = Table.findOne({ name: query.table });
30
+ }
31
+ const mdTemplate = await get_md_file(topic);
32
+ if (!mdTemplate) return { markup: "Topic not found" };
33
+ const template = _.template(mdTemplate);
34
+ const mdTopic = template(context);
35
+ const markup = md.render(mdTopic);
36
+ return { markup };
37
+ } catch (e) {
38
+ return { markup: pre(e.toString()) };
39
+ }
40
+ };
41
+
42
+ module.exports = { get_help_markup };
@@ -22,144 +22,21 @@ const toJsType = (type) =>
22
22
  Color: "string",
23
23
  }[type] || type);
24
24
 
25
- /**
26
- * @param {*} type
27
- * @param {object[]} fields
28
- * @returns {string[]}
29
- */
30
- const intExamples = (type, fields) => {
31
- const boolFields = fields.filter((f) => f.type && f.type.name === "Bool");
32
- const intFields = fields.filter((f) => f.type && f.type.name === "Integer");
33
- const exs = ["3"];
34
- if (boolFields.length > 0) {
35
- const b = is.one_of(boolFields).generate();
36
- exs.push(`${b.name} ? 6 : 9`);
37
- }
38
- if (intFields.length > 0) {
39
- const b = is.one_of(intFields).generate();
40
- exs.push(`${b.name} + 5`);
41
- }
42
- return exs;
43
- };
44
-
45
- /**
46
- * @param {*} type
47
- * @param {object[]} fields
48
- * @returns {string[]}
49
- */
50
- const colorExamples = (type, fields) => {
51
- const boolFields = fields.filter((f) => f.type && f.type.name === "Bool");
52
- const exs = [`"#06ab6d1"`];
53
- if (boolFields.length > 0) {
54
- const b = is.one_of(boolFields).generate();
55
- exs.push(`${b.name} ? "#000000" : "#ffffff"`);
56
- }
57
- return exs;
58
- };
59
-
60
- /**
61
- * @param {*} type
62
- * @param {object[]} fields
63
- * @returns {string[]}
64
- */
65
- const stringExamples = (type, fields) => {
66
- const boolFields = fields.filter((f) => f.type && f.type.name === "Bool");
67
- const strFields = fields.filter((f) => f.type && f.type.name === "String");
68
- const exs = [`"Hello world!"`];
69
- if (boolFields.length > 0) {
70
- const b = is.one_of(boolFields).generate();
71
- exs.push(`${b.name} ? "Squish" : "Squash"`);
72
- }
73
- if (strFields.length > 0) {
74
- const b = is.one_of(strFields).generate();
75
- exs.push(`${b.name}.toUpperCase()`);
76
- }
77
- return exs;
78
- };
79
-
80
- /**
81
- * @param {*} type
82
- * @param {object[]} fields
83
- * @returns {string[]}
84
- */
85
- const floatExamples = (type, fields) => {
86
- const boolFields = fields.filter((f) => f.type && f.type.name === "Bool");
87
- const numFields = fields.filter(
88
- (f) => f.type && (f.type.name === "Integer" || f.type.name === "Float")
89
- );
90
- const exs = ["3.14"];
91
- if (boolFields.length > 0) {
92
- const b = is.one_of(boolFields).generate();
93
- exs.push(`${b.name} ? 2.78 : 99`);
94
- }
95
- if (numFields.length > 0) {
96
- const b = is.one_of(numFields).generate();
97
- exs.push(`Math.pow(${b.name}, 2)`);
98
- }
99
- if (numFields.length > 1) {
100
- const n1 = numFields[0];
101
- const n2 = numFields[1];
102
- exs.push(
103
- `${n1.name}>${n2.name} ? Math.sqrt(${n1.name}) : ${n1.name}*${n2.name}`
104
- );
105
- }
106
- return exs;
107
- };
108
-
109
- /**
110
- * @param {*} type
111
- * @param {object[]} fields
112
- * @returns {string[]}
113
- */
114
- const boolExamples = (type, fields) => {
115
- const boolFields = fields.filter((f) => f.type && f.type.name === "Bool");
116
- const numFields = fields.filter(
117
- (f) => (f.type && f.type.name === "Integer") || f.type.name === "Float"
118
- );
119
- const exs = ["true"];
120
- if (boolFields.length > 0) {
121
- const b = is.one_of(boolFields).generate();
122
- exs.push(`!${b.name}`);
123
- }
124
- if (boolFields.length > 1) {
125
- const b1 = boolFields[0];
126
- const b2 = boolFields[1];
127
- exs.push(`${b1.name} && ${b2.name}`);
128
- }
129
- if (numFields.length > 0) {
130
- const b = is.one_of(numFields).generate();
131
- exs.push(`${b.name}<3`);
132
- }
133
- if (numFields.length > 1) {
134
- const n1 = numFields[0];
135
- const n2 = numFields[1];
136
- exs.push(`${n1.name}>${n2.name}`);
137
- }
138
- return exs;
139
- };
140
-
141
25
  /**
142
26
  * @param {string} type
143
27
  * @param {*} stored
144
- * @param {object[]} allFields
28
+ * @param {Table} table
145
29
  * @param {object} req
146
30
  * @returns {p[]}
147
31
  */
148
- const expressionBlurb = (type, stored, allFields, req) => {
32
+ const expressionBlurb = (type, stored, table, req) => {
33
+ const allFields = table.fields;
149
34
  const fields = allFields.filter((f) => !f.calculated);
150
35
  const funs = getState().functions;
151
36
  const funNames = Object.entries(funs)
152
37
  .filter(([k, v]) => !(!stored && v.isAsync))
153
38
  .map(([k, v]) => k);
154
- const examples = (
155
- {
156
- Integer: () => intExamples(type, fields),
157
- Float: () => floatExamples(type, fields),
158
- Bool: () => boolExamples(type, fields),
159
- Color: () => colorExamples(type, fields),
160
- String: () => stringExamples(type, fields),
161
- }[type] || (() => [])
162
- )();
39
+ const examples = table.getFormulaExamples(type);
163
40
  return [
164
41
  p(
165
42
  req.__(
package/package.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "@saltcorn/server",
3
- "version": "0.8.9",
3
+ "version": "0.9.0-beta.0",
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.9",
10
- "@saltcorn/builder": "0.8.9",
11
- "@saltcorn/data": "0.8.9",
12
- "@saltcorn/admin-models": "0.8.9",
13
- "@saltcorn/filemanager": "0.8.9",
14
- "@saltcorn/markup": "0.8.9",
15
- "@saltcorn/sbadmin2": "0.8.9",
9
+ "@saltcorn/base-plugin": "0.9.0-beta.0",
10
+ "@saltcorn/builder": "0.9.0-beta.0",
11
+ "@saltcorn/data": "0.9.0-beta.0",
12
+ "@saltcorn/admin-models": "0.9.0-beta.0",
13
+ "@saltcorn/filemanager": "0.9.0-beta.0",
14
+ "@saltcorn/markup": "0.9.0-beta.0",
15
+ "@saltcorn/sbadmin2": "0.9.0-beta.0",
16
16
  "@socket.io/cluster-adapter": "^0.2.1",
17
17
  "@socket.io/sticky": "^1.0.1",
18
18
  "adm-zip": "0.5.10",
@@ -32,11 +32,13 @@
32
32
  "express-session": "^1.17.1",
33
33
  "greenlock": "^4.0.4",
34
34
  "greenlock-express": "^4.0.3",
35
+ "underscore": "1.13.6",
35
36
  "helmet": "^3.23.3",
36
37
  "i18n": "^0.15.1",
37
38
  "imapflow": "1.0.123",
38
39
  "jsonwebtoken": "^9.0.0",
39
40
  "live-plugin-manager": "^0.17.1",
41
+ "markdown-it": "^13.0.2",
40
42
  "moment": "^2.29.4",
41
43
  "multer": "1.4.5-lts.1",
42
44
  "multer-s3": "^2.10.0",
package/routes/admin.js CHANGED
@@ -56,6 +56,7 @@ const {
56
56
  li,
57
57
  ol,
58
58
  script,
59
+ text,
59
60
  domReady,
60
61
  } = require("@saltcorn/markup/tags");
61
62
  const db = require("@saltcorn/data/db");
@@ -104,6 +105,7 @@ const Page = require("@saltcorn/data/models/page");
104
105
  const { getSafeSaltcornCmd } = require("@saltcorn/data/utils");
105
106
  const stream = require("stream");
106
107
  const Crash = require("@saltcorn/data/models/crash");
108
+ const { get_help_markup } = require("../help/index.js");
107
109
 
108
110
  const router = new Router();
109
111
  module.exports = router;
@@ -240,6 +242,22 @@ router.get(
240
242
  })
241
243
  );
242
244
 
245
+ /**
246
+ * @name get/send-test-email
247
+ * @function
248
+ * @memberof module:routes/admin~routes/adminRouter
249
+ */
250
+ router.get(
251
+ "/help/:topic",
252
+ isAdmin,
253
+ error_catcher(async (req, res) => {
254
+ const { topic } = req.params;
255
+ const { markup } = await get_help_markup(topic, req.query, req);
256
+
257
+ res.sendWrap(`Help: ${topic}`, { above: [markup] });
258
+ })
259
+ );
260
+
243
261
  /**
244
262
  * @name get/backup
245
263
  * @function
@@ -538,6 +556,7 @@ router.get(
538
556
  error_catcher(async (req, res) => {
539
557
  const { type, name } = req.params;
540
558
  const snaps = await Snapshot.entity_history(type, name);
559
+ res.set("Page-Title", `Restore ${text(name)}`);
541
560
  res.send(
542
561
  mkTable(
543
562
  [
package/routes/api.js CHANGED
@@ -140,7 +140,7 @@ router.post(
140
140
  const view = await View.findOne({ name: viewName });
141
141
  const db = require("@saltcorn/data/db");
142
142
  if (!view) {
143
- getState().log(3, `API viewQuery ${view.name} not found`);
143
+ getState().log(3, `API viewQuery ${viewName} not found`);
144
144
  res.status(404).json({
145
145
  error: req.__("View %s not found", viewName),
146
146
  view: viewName,
package/routes/fields.js CHANGED
@@ -165,6 +165,7 @@ const fieldForm = async (req, fkey_opts, existing_names, id, hasData) => {
165
165
  name: "protected",
166
166
  sublabel: req.__("Set role to access"),
167
167
  type: "Bool",
168
+ showIf: { calculated: false },
168
169
  }),
169
170
  {
170
171
  label: req.__("Minimum role to write"),
@@ -460,7 +461,7 @@ const fieldFlow = (req) =>
460
461
  html: expressionBlurb(
461
462
  context.type,
462
463
  context.stored,
463
- fields,
464
+ table,
464
465
  req
465
466
  ),
466
467
  },
package/routes/tables.js CHANGED
@@ -1378,6 +1378,12 @@ router.get(
1378
1378
  link(`/table/add-constraint/${id}/Formula`, req.__("Formula")),
1379
1379
  " | ",
1380
1380
  link(`/table/add-constraint/${id}/Index`, req.__("Index")),
1381
+ a(
1382
+ {
1383
+ href: `javascript:ajax_modal('/admin/help/Table%20constraints?table=${table.name}')`,
1384
+ },
1385
+ i({ class: "fas fa-question-circle ms-1" })
1386
+ ),
1381
1387
  ],
1382
1388
  },
1383
1389
  ],
@@ -1392,11 +1398,11 @@ router.get(
1392
1398
  * @param {object[]} fields
1393
1399
  * @returns {Form}
1394
1400
  */
1395
- const constraintForm = (req, table_id, fields, type) => {
1401
+ const constraintForm = (req, table, fields, type) => {
1396
1402
  switch (type) {
1397
1403
  case "Formula":
1398
1404
  return new Form({
1399
- action: `/table/add-constraint/${table_id}/${type}`,
1405
+ action: `/table/add-constraint/${table.id}/${type}`,
1400
1406
 
1401
1407
  fields: [
1402
1408
  {
@@ -1405,6 +1411,10 @@ const constraintForm = (req, table_id, fields, type) => {
1405
1411
  validator: expressionValidator,
1406
1412
  type: "String",
1407
1413
  class: "validate-expression",
1414
+ help: {
1415
+ topic: "Table formula constraint",
1416
+ context: { table: table.name },
1417
+ },
1408
1418
  sublabel:
1409
1419
  req.__(
1410
1420
  "Formula must evaluate to true for valid rows. In scope: "
@@ -1417,14 +1427,14 @@ const constraintForm = (req, table_id, fields, type) => {
1417
1427
  {
1418
1428
  name: "errormsg",
1419
1429
  label: "Error message",
1420
- sublabel: "Shown the user if formula is false",
1430
+ sublabel: "Shown to the user if formula is false",
1421
1431
  type: "String",
1422
1432
  },
1423
1433
  ],
1424
1434
  });
1425
1435
  case "Unique":
1426
1436
  return new Form({
1427
- action: `/table/add-constraint/${table_id}/${type}`,
1437
+ action: `/table/add-constraint/${table.id}/${type}`,
1428
1438
  blurb: req.__(
1429
1439
  "Tick the boxes for the fields that should be jointly unique"
1430
1440
  ),
@@ -1437,14 +1447,14 @@ const constraintForm = (req, table_id, fields, type) => {
1437
1447
  {
1438
1448
  name: "errormsg",
1439
1449
  label: "Error message",
1440
- sublabel: "Shown the user if joint uniqueness is violated",
1450
+ sublabel: "Shown to the user if joint uniqueness is violated",
1441
1451
  type: "String",
1442
1452
  },
1443
1453
  ],
1444
1454
  });
1445
1455
  case "Index":
1446
1456
  return new Form({
1447
- action: `/table/add-constraint/${table_id}/${type}`,
1457
+ action: `/table/add-constraint/${table.id}/${type}`,
1448
1458
  blurb: req.__(
1449
1459
  "Choose the field to be indexed. This make searching the table faster."
1450
1460
  ),
@@ -1483,7 +1493,7 @@ router.get(
1483
1493
  return;
1484
1494
  }
1485
1495
  const fields = table.getFields();
1486
- const form = constraintForm(req, table.id, fields, type);
1496
+ const form = constraintForm(req, table, fields, type);
1487
1497
  res.sendWrap(req.__(`Add constraint to %s`, table.name), {
1488
1498
  above: [
1489
1499
  {
@@ -1527,7 +1537,7 @@ router.post(
1527
1537
  return;
1528
1538
  }
1529
1539
  const fields = table.getFields();
1530
- const form = constraintForm(req, table.id, fields, type);
1540
+ const form = constraintForm(req, table, fields, type);
1531
1541
  form.validate(req.body);
1532
1542
  if (form.hasErrors) req.flash("error", req.__("An error occurred"));
1533
1543
  else {