@saltcorn/server 0.9.0-beta.0 → 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 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
- if (!opts.disableCsrf)
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
- * @name get/callback/:method
1207
- * @function
1208
- * @memberof module:auth/routes~routesRouter
1209
- */
1210
- router.get(
1211
- "/callback/:method",
1212
- error_catcher(async (req, res, next) => {
1213
- const { method } = req.params;
1214
- const auth = getState().auth_methods[method];
1215
- if (auth) {
1216
- passport.authenticate(method, { failureRedirect: "/auth/login" })(
1217
- req,
1218
- res,
1219
- loginCallback(req, res)
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
@@ -6,14 +8,14 @@ to the user. 
6
8
 
7
9
  Some examples:
8
10
 
9
- {{# for (const fml of table.getFormulaExamples("Bool")) { }} * `{{= fml}}`
11
+ {{# for (const fml of table.getFormulaExamples("Bool")) { }} * `{{ fml}}`
10
12
  {{# } }}
11
13
 
12
- This formula can use any of the fields in table {{= table.name }} as variables:
14
+ This formula can use any of the fields in table {{ table.name }} as variables:
13
15
 
14
16
  | Field | Variable name | Type |
15
17
  | ----- | ------------- | ---- |
16
- {{# for (const field of table.fields) { }} | {{= field.label }} | `{{= field.name }}` | {{= field.pretty_type }} |
18
+ {{# for (const field of table.fields) { }} | {{ field.label }} | `{{ field.name }}` | {{ field.pretty_type }} |
17
19
  {{# } }}
18
20
 
19
21
 
@@ -32,7 +34,7 @@ The first-order join fields you can use in the constraint formula are:
32
34
 
33
35
  {{# for (const field of table.fields.filter(f=>f.is_fkey && f.reftable_name)) { }}
34
36
  {{# const reftable = Table.findOne( field.reftable_name); }}
35
- * Through {{=field.label}} key field: {{= table.fields.map(jf=>`\`${field.name}.${jf.name}\``).join(", ") }}
37
+ * Through {{field.label}} key field: {{ reftable.fields.map(jf=>`\`${field.name}.${jf.name}\``).join(", ") }}
36
38
 
37
39
 
38
40
  {{# } }}
@@ -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;
@@ -6,10 +7,12 @@ const MarkdownIt = require("markdown-it"),
6
7
  md = new MarkdownIt();
7
8
 
8
9
  const { pre } = require("@saltcorn/markup/tags");
9
-
10
+ const path = require("path");
11
+ const { getState } = require("@saltcorn/data/db/state");
12
+ const { oneOf } = require("@saltcorn/types/generators");
10
13
  const get_md_file = async (topic) => {
11
14
  try {
12
- const fp = require.resolve(`./${File.normalise(topic)}.tmd`);
15
+ const fp = path.join(__dirname, `${File.normalise(topic)}.tmd`);
13
16
  const fileBuf = await fs.readFile(fp);
14
17
  return fileBuf.toString();
15
18
  } catch (e) {
@@ -17,20 +20,26 @@ const get_md_file = async (topic) => {
17
20
  }
18
21
  };
19
22
 
20
- _.templateSettings = {
21
- evaluate: /\{\{#(.+?)\}\}/g,
22
- interpolate: /\{\{=(.+?)\}\}/g,
23
+ md.renderer.rules.table_open = function (tokens, idx) {
24
+ return '<table class="help-md">';
23
25
  };
24
26
 
25
27
  const get_help_markup = async (topic, query, req) => {
26
28
  try {
27
- const context = { user: req.user, Table };
28
- if (query.table) {
29
- context.table = Table.findOne({ name: query.table });
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
- const template = _.template(mdTemplate);
39
+ const template = _.template(mdTemplate, {
40
+ evaluate: /\{\{#(.+?)\}\}/g,
41
+ interpolate: /\{\{([^#].+?)\}\}/g,
42
+ });
34
43
  const mdTopic = template(context);
35
44
  const markup = md.render(mdTopic);
36
45
  return { markup };
package/load_plugins.js CHANGED
@@ -176,9 +176,10 @@ const loadAllPlugins = async () => {
176
176
  * @param plugin
177
177
  * @param force
178
178
  * @param noSignalOrDB
179
+ * @param manager - optional plugin manager
179
180
  * @returns {Promise<void>}
180
181
  */
181
- const loadAndSaveNewPlugin = async (plugin, force, noSignalOrDB) => {
182
+ const loadAndSaveNewPlugin = async (plugin, force, noSignalOrDB, manager) => {
182
183
  const tenants_unsafe_plugins = getRootState().getConfig(
183
184
  "tenants_unsafe_plugins",
184
185
  false
@@ -200,7 +201,8 @@ const loadAndSaveNewPlugin = async (plugin, force, noSignalOrDB) => {
200
201
  }
201
202
  const { version, plugin_module, location } = await requirePlugin(
202
203
  plugin,
203
- force
204
+ force,
205
+ manager,
204
206
  );
205
207
 
206
208
  // install dependecies