@saltcorn/server 0.9.0-beta.1 → 0.9.0-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.
- package/app.js +49 -3
- package/auth/routes.js +15 -19
- package/help/Extra state formula.tmd +62 -0
- package/help/Field views.tmd +22 -0
- package/help/Table formula constraint.tmd +2 -0
- package/help/View patterns.tmd +35 -0
- package/help/Where formula.tmd +30 -0
- package/help/index.js +11 -5
- package/locales/da.json +709 -709
- package/locales/de.json +1049 -1049
- package/locales/en.json +11 -3
- package/locales/pl.json +1155 -1155
- package/locales/ru.json +1101 -1101
- package/locales/si.json +1196 -1196
- package/locales/uk.json +1168 -1168
- package/locales/zh.json +886 -886
- package/package.json +10 -9
- package/public/saltcorn-builder.css +4 -0
- package/public/saltcorn-common.js +23 -2
- package/public/saltcorn.js +13 -0
- package/routes/menu.js +1 -1
- package/routes/plugins.js +186 -36
- package/routes/tables.js +4 -3
- package/routes/viewedit.js +8 -1
package/app.js
CHANGED
|
@@ -69,6 +69,39 @@ const disabledCsurf = (req, res, next) => {
|
|
|
69
69
|
next();
|
|
70
70
|
};
|
|
71
71
|
|
|
72
|
+
const noCsrfLookup = (state) => {
|
|
73
|
+
if (!state.plugin_routes) return null;
|
|
74
|
+
else {
|
|
75
|
+
const result = new Set();
|
|
76
|
+
for (const [plugin, routes] of Object.entries(state.plugin_routes)) {
|
|
77
|
+
for (const url of routes
|
|
78
|
+
.filter((r) => r.noCsrf === true)
|
|
79
|
+
.map((r) => r.url)) {
|
|
80
|
+
result.add(url);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const prepPluginRouter = (pluginRoutes) => {
|
|
88
|
+
const router = express.Router();
|
|
89
|
+
for (const [plugin, routes] of Object.entries(pluginRoutes)) {
|
|
90
|
+
for (const route of routes) {
|
|
91
|
+
switch (route.method) {
|
|
92
|
+
case "post":
|
|
93
|
+
router.post(route.url, error_catcher(route.callback));
|
|
94
|
+
break;
|
|
95
|
+
case "get":
|
|
96
|
+
default:
|
|
97
|
+
router.get(route.url, error_catcher(route.callback));
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return router;
|
|
103
|
+
};
|
|
104
|
+
|
|
72
105
|
// todo console.log app instance info when app stxarts - avoid to show secrets (password, etc)
|
|
73
106
|
|
|
74
107
|
/**
|
|
@@ -300,21 +333,34 @@ const getApp = async (opts = {}) => {
|
|
|
300
333
|
app.use("/scapi", scapi);
|
|
301
334
|
|
|
302
335
|
const csurf = csrf();
|
|
303
|
-
|
|
336
|
+
let noCsrf = null;
|
|
337
|
+
if (!opts.disableCsrf) {
|
|
338
|
+
noCsrf = noCsrfLookup(getState());
|
|
304
339
|
app.use(function (req, res, next) {
|
|
305
340
|
if (
|
|
341
|
+
noCsrf?.has(req.url) ||
|
|
306
342
|
(req.smr &&
|
|
307
343
|
(req.url.startsWith("/api/") ||
|
|
308
344
|
req.url === "/auth/login-with/jwt" ||
|
|
309
345
|
req.url === "/auth/signup")) ||
|
|
310
|
-
jwt_extractor(req)
|
|
346
|
+
jwt_extractor(req) ||
|
|
347
|
+
req.url === "/auth/callback/saml"
|
|
311
348
|
)
|
|
312
349
|
return disabledCsurf(req, res, next);
|
|
313
350
|
csurf(req, res, next);
|
|
314
351
|
});
|
|
315
|
-
else app.use(disabledCsurf);
|
|
352
|
+
} else app.use(disabledCsurf);
|
|
316
353
|
|
|
317
354
|
mountRoutes(app);
|
|
355
|
+
// mount plugin router with a callback for changes
|
|
356
|
+
let pluginRouter = prepPluginRouter(getState().plugin_routes || {});
|
|
357
|
+
getState().routesChangedCb = () => {
|
|
358
|
+
pluginRouter = prepPluginRouter(getState().plugin_routes || {});
|
|
359
|
+
noCsrf = noCsrfLookup(getState());
|
|
360
|
+
};
|
|
361
|
+
app.use((req, res, next) => {
|
|
362
|
+
pluginRouter(req, res, next);
|
|
363
|
+
});
|
|
318
364
|
// set tenant homepage as / root
|
|
319
365
|
app.get("/", error_catcher(homepage));
|
|
320
366
|
// /robots.txt
|
package/auth/routes.js
CHANGED
|
@@ -1202,25 +1202,21 @@ const loginCallback = (req, res) => () => {
|
|
|
1202
1202
|
}
|
|
1203
1203
|
};
|
|
1204
1204
|
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
);
|
|
1221
|
-
}
|
|
1222
|
-
})
|
|
1223
|
-
);
|
|
1205
|
+
const callbackFn = async (req, res, next) => {
|
|
1206
|
+
const { method } = req.params;
|
|
1207
|
+
const auth = getState().auth_methods[method];
|
|
1208
|
+
if (auth) {
|
|
1209
|
+
passport.authenticate(method, { failureRedirect: "/auth/login" })(
|
|
1210
|
+
req,
|
|
1211
|
+
res,
|
|
1212
|
+
loginCallback(req, res)
|
|
1213
|
+
);
|
|
1214
|
+
}
|
|
1215
|
+
};
|
|
1216
|
+
|
|
1217
|
+
router.get("/callback/:method", error_catcher(callbackFn));
|
|
1218
|
+
|
|
1219
|
+
router.post("/callback/:method", error_catcher(callbackFn));
|
|
1224
1220
|
|
|
1225
1221
|
/**
|
|
1226
1222
|
* @param {object} req
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{{# const srcTable = Table.findOne({ name: query.srcTable }) }}
|
|
2
|
+
{{# const view = View.findOne({ name: query.view_name }) }}
|
|
3
|
+
{{# const destTable = Table.findOne({ id: view.table_id }) }}
|
|
4
|
+
|
|
5
|
+
The extra state formula when embedding or linking to a view is used
|
|
6
|
+
to influence the row that will be included in that views display,
|
|
7
|
+
in addition to any selection based on a chosen relationship.
|
|
8
|
+
|
|
9
|
+
The formula takes the form of a JavaScript object where the object
|
|
10
|
+
keys are fields (variable names) on the table of the embedded view,
|
|
11
|
+
and the values are the JavaScript expressions for those fields which set the criteria
|
|
12
|
+
for the rows to be included. For example, if you would like to display
|
|
13
|
+
all rows where the Active field is true you would use this as the extra
|
|
14
|
+
state formula: `{ active: true }`. This is an object where the only
|
|
15
|
+
key is `active` and its value is `true`. You can have more than one
|
|
16
|
+
key, for example:
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
{ active: true, age: {gt: 21}, manager: user.id }
|
|
20
|
+
```
|
|
21
|
+
(See below for using the `user` variable and greater-than filters)
|
|
22
|
+
|
|
23
|
+
The keys you can use are the variable names on the {{destTable.name}} table:
|
|
24
|
+
|
|
25
|
+
| Field | Variable name | Type |
|
|
26
|
+
| ----- | ------------- | ---- |
|
|
27
|
+
{{# for (const field of destTable.fields) { }} | {{ field.label }} | `{{ field.name }}` | {{ field.pretty_type }} |
|
|
28
|
+
{{# } }}
|
|
29
|
+
|
|
30
|
+
In the value expressions (to the right of the colon) you can use
|
|
31
|
+
literals (e.g. 17 for numbers or true for booleans; string literals
|
|
32
|
+
must be enclosed in quotes (single, double or
|
|
33
|
+
[backtick](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals))).
|
|
34
|
+
You can also as the value use a more complex JavaScript expression.
|
|
35
|
+
|
|
36
|
+
You can refer to the logged in user using the variable name user.
|
|
37
|
+
This is an object and you must use the dot to access user fields,
|
|
38
|
+
e.g. user.id for the logged in users id. A very common scenario
|
|
39
|
+
is to restrict rows to show to those that belong to the current user.
|
|
40
|
+
If you have a key to users field called `theUser`, you would use this
|
|
41
|
+
as the extra state formula: `{theUser: user.id}`
|
|
42
|
+
|
|
43
|
+
The user object has other fields than the primary key identifier.
|
|
44
|
+
For your login session the user object is:
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
{{JSON.stringify(user, null, 2)}}
|
|
48
|
+
```
|
|
49
|
+
{{# const userkey = oneOf(Object.keys(user)) }}
|
|
50
|
+
|
|
51
|
+
For example to use the `{{userkey}}` field on the user object, write `user.{{userkey}}`
|
|
52
|
+
{{# if(srcTable) { }}
|
|
53
|
+
You can also use variables from the currently displayed row in the value expressions,
|
|
54
|
+
in this case the table {{srcTable.name}}:
|
|
55
|
+
|
|
56
|
+
| Field | Variable name | Type |
|
|
57
|
+
| ----- | ------------- | ---- |
|
|
58
|
+
{{# for (const field of srcTable.fields) { }} | {{ field.label }} | `{{ field.name }}` | {{ field.pretty_type }} |
|
|
59
|
+
{{# } }}
|
|
60
|
+
|
|
61
|
+
{{# } }}
|
|
62
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{{# const table = Table.findOne({ name: query.table_name }) }}
|
|
2
|
+
{{# const mode = query.mode }}
|
|
3
|
+
{{# const field = table.getField(query.field_name) }}
|
|
4
|
+
{{# const fvsObj = field.type === "Key" ? scState.keyFieldviews : field.type === "File" ? scState.fileviews : field.type?.fieldviews || {} }}
|
|
5
|
+
{{# const allFVs = Object.entries(fvsObj) }}
|
|
6
|
+
{{# const myFVs = mode==='filter' ? allFVs.filter(([nm,fv])=>fv.isFilter || fv.isEdit) : mode==='edit' ? allFVs.filter(([nm,fv])=>!fv.isFilter || fv.isEdit) : mode==='show'||mode==='list' ? allFVs.filter(([nm,fv])=>!fv.isFilter && !fv.isEdit) : allFVs }}
|
|
7
|
+
|
|
8
|
+
The field view determines how a field value is shown to the user and how the
|
|
9
|
+
user can interact with (or edit, in Edit views) that value. The available
|
|
10
|
+
field views are determined by the field type and the view pattern.
|
|
11
|
+
|
|
12
|
+
Some field views have configuration options which can be used to further
|
|
13
|
+
customize how the value is displayed or how the user interacts with it.
|
|
14
|
+
This configuration options will appear in the field settings below the
|
|
15
|
+
field view choice.
|
|
16
|
+
|
|
17
|
+
In the table {{ table.name }} the field {{ field.label }} of type
|
|
18
|
+
{{ field.pretty_type }} has the following available field views:
|
|
19
|
+
|
|
20
|
+
{{# for (const [name, fv] of myFVs) { }}
|
|
21
|
+
* {{name}}{{mode==='edit' && !fv.isEdit? ` [Not editable]`:''}}: {{fv.description||undefined}}
|
|
22
|
+
{{# } }}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
{{# const table = Table.findOne({ name: query.table }) }}
|
|
2
|
+
|
|
1
3
|
The constraint formula should be a JavaScript expression which evaluates to a
|
|
2
4
|
boolean (true or false). If the formula evaluates to false then the row is not
|
|
3
5
|
valid and will not be accepted in the table. If you are editing an existing row
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
The view pattern is a fundamental concept in Saltcorn that determines how
|
|
2
|
+
the view will relate to the database table and the overall behavior of the
|
|
3
|
+
view. A view will always follow one specific view pattern, and will require
|
|
4
|
+
further configuration according to which view pattern is chosen. This
|
|
5
|
+
configuration could be as simple as making a few choices, or it could be as
|
|
6
|
+
complicated as specifying a layout using drag and drop. In addition most view
|
|
7
|
+
patterns require choosing a single table, which it can use as a source or
|
|
8
|
+
destination of data, although in many cases data from other tables can be
|
|
9
|
+
brought in using table relationships.
|
|
10
|
+
|
|
11
|
+
There are fundamental view patterns in Saltcorn that you will use in
|
|
12
|
+
every application:
|
|
13
|
+
|
|
14
|
+
* A List view shows a tabular grid of multiple rows in a table. You can define
|
|
15
|
+
its columns which can be data from this or related tables, action buttons or
|
|
16
|
+
links to other views.
|
|
17
|
+
|
|
18
|
+
* An Edit view is a form that can be used to create a new row or edit an existing
|
|
19
|
+
row in a table.
|
|
20
|
+
|
|
21
|
+
* A Show view shows a single existing row in the table. The row must be specified
|
|
22
|
+
either by linking or embedding from another view or page.
|
|
23
|
+
|
|
24
|
+
* A Filter view does not display any data from the table but can be used to set
|
|
25
|
+
up user interface elements that determine which rows are shown in other views
|
|
26
|
+
on the same page.
|
|
27
|
+
|
|
28
|
+
* A Feed view is configured by another view which shows a single row and will
|
|
29
|
+
repeat this view for all available rows.
|
|
30
|
+
|
|
31
|
+
The additional view patterns available in your system are:
|
|
32
|
+
{{# const vts = Object.values(scState.viewtemplates).filter(vt=>!["List","Show","Edit","Filter","Feed",].includes(vt.name))}}
|
|
33
|
+
{{# for (const vt of vts) { }}
|
|
34
|
+
* {{ vt.name }}{{vt.description ? `: ${vt.description}` : ""}}
|
|
35
|
+
{{# } }}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{{# const srcTable = Table.findOne({ name: query.table_name }) }}
|
|
2
|
+
{{# const field = srcTable.getField(query.field_name) }}
|
|
3
|
+
{{# const refTable = Table.findOne(field.reftable_name) }}
|
|
4
|
+
|
|
5
|
+
The where formula allows you to restrict the options provided. This can be
|
|
6
|
+
done according to a static criterion, all according to a dynamic criterion
|
|
7
|
+
that depends on the value of another form field.
|
|
8
|
+
|
|
9
|
+
The formula should take the form of a JavaScript boolean expression such as
|
|
10
|
+
`a === b` or `x>y && w==1` etc.
|
|
11
|
+
|
|
12
|
+
In this expression you can use variables from the target table from which
|
|
13
|
+
you are selecting values, here {{ refTable.name }}. These can accessed by
|
|
14
|
+
their variable nmame. The fields on the table {{ refTable.name }} are:
|
|
15
|
+
|
|
16
|
+
| Field | Variable name | Type |
|
|
17
|
+
| ----- | ------------- | ---- |
|
|
18
|
+
{{# for (const field of refTable.fields) { }} | {{ field.label }} | `{{ field.name }}` | {{ field.pretty_type }} |
|
|
19
|
+
{{# } }}
|
|
20
|
+
|
|
21
|
+
You can also use variables from the current form. In that case the options will
|
|
22
|
+
respond dynamically to the state of the form fields. These form variables must be proceeded by
|
|
23
|
+
a `$`:
|
|
24
|
+
|
|
25
|
+
| Field | Variable name | Type |
|
|
26
|
+
| ----- | ------------- | ---- |
|
|
27
|
+
{{# for (const field of srcTable.fields) { }} | {{ field.label }} | `${{ field.name }}` | {{ field.pretty_type }} |
|
|
28
|
+
{{# } }}
|
|
29
|
+
|
|
30
|
+
You can mix the two types of variables, as in `a == $b`
|
package/help/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const Table = require("@saltcorn/data/models/table");
|
|
2
|
+
const View = require("@saltcorn/data/models/view");
|
|
2
3
|
const File = require("@saltcorn/data/models/file");
|
|
3
4
|
const _ = require("underscore");
|
|
4
5
|
const fs = require("fs").promises;
|
|
@@ -7,7 +8,8 @@ const MarkdownIt = require("markdown-it"),
|
|
|
7
8
|
|
|
8
9
|
const { pre } = require("@saltcorn/markup/tags");
|
|
9
10
|
const path = require("path");
|
|
10
|
-
|
|
11
|
+
const { getState } = require("@saltcorn/data/db/state");
|
|
12
|
+
const { oneOf } = require("@saltcorn/types/generators");
|
|
11
13
|
const get_md_file = async (topic) => {
|
|
12
14
|
try {
|
|
13
15
|
const fp = path.join(__dirname, `${File.normalise(topic)}.tmd`);
|
|
@@ -24,10 +26,14 @@ md.renderer.rules.table_open = function (tokens, idx) {
|
|
|
24
26
|
|
|
25
27
|
const get_help_markup = async (topic, query, req) => {
|
|
26
28
|
try {
|
|
27
|
-
const context = {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
const context = {
|
|
30
|
+
user: req.user,
|
|
31
|
+
Table,
|
|
32
|
+
View,
|
|
33
|
+
scState: getState(),
|
|
34
|
+
query,
|
|
35
|
+
oneOf,
|
|
36
|
+
};
|
|
31
37
|
const mdTemplate = await get_md_file(topic);
|
|
32
38
|
if (!mdTemplate) return { markup: "Topic not found" };
|
|
33
39
|
const template = _.template(mdTemplate, {
|