@saltcorn/server 1.0.0-beta.8 → 1.0.0-rc.1
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 +4 -7
- package/auth/admin.js +1 -0
- package/help/Aggregation where formula.tmd +11 -0
- package/help/Calculated fields.tmd +28 -0
- package/help/Date format.tmd +55 -0
- package/help/Protected fields.tmd +4 -0
- package/help/Snapshots.tmd +19 -0
- package/help/Table history.tmd +42 -0
- package/help/index.js +2 -0
- package/load_plugins.js +89 -8
- package/locales/en.json +13 -1
- package/locales/pl.json +407 -85
- package/package.json +11 -11
- package/public/saltcorn-common.js +195 -100
- package/public/saltcorn.css +17 -0
- package/public/saltcorn.js +69 -12
- package/routes/actions.js +18 -24
- package/routes/admin.js +256 -54
- package/routes/eventlog.js +2 -1
- package/routes/fields.js +21 -5
- package/routes/infoarch.js +40 -2
- package/routes/menu.js +3 -0
- package/routes/pageedit.js +7 -1
- package/routes/plugins.js +118 -23
- package/routes/scapi.js +19 -0
- package/routes/search.js +6 -1
- package/routes/sync.js +2 -1
- package/routes/tables.js +14 -4
- package/serve.js +17 -3
- package/tests/fields.test.js +32 -0
- package/tests/plugin_install.test.js +235 -0
- package/tests/plugins.test.js +140 -0
package/app.js
CHANGED
|
@@ -306,13 +306,7 @@ const getApp = async (opts = {}) => {
|
|
|
306
306
|
? new Date(u.last_mobile_login).valueOf()
|
|
307
307
|
: u.last_mobile_login) <= jwt_payload.iat
|
|
308
308
|
) {
|
|
309
|
-
return done(null,
|
|
310
|
-
email: u.email,
|
|
311
|
-
id: u.id,
|
|
312
|
-
role_id: u.role_id,
|
|
313
|
-
language: u.language,
|
|
314
|
-
tenant: db.getTenantSchema(),
|
|
315
|
-
});
|
|
309
|
+
return done(null, u.session_object);
|
|
316
310
|
} else {
|
|
317
311
|
return done(null, { role_id: 100 });
|
|
318
312
|
}
|
|
@@ -447,6 +441,9 @@ Sitemap: ${base}sitemap.xml
|
|
|
447
441
|
app.get("*", function (req, res) {
|
|
448
442
|
res.status(404).sendWrap(req.__("Not found"), h1(req.__("Page not found")));
|
|
449
443
|
});
|
|
444
|
+
|
|
445
|
+
//prevent prototype pollution
|
|
446
|
+
delete Object.prototype.__proto__;
|
|
450
447
|
return app;
|
|
451
448
|
};
|
|
452
449
|
module.exports = getApp;
|
package/auth/admin.js
CHANGED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
The where formula allows you to restrict the rows included in the aggregations.
|
|
2
|
+
|
|
3
|
+
The formula should take the form of a JavaScript boolean expression such as
|
|
4
|
+
`a === b` or `x>y && w==1` when is based on the row values in the rows in the target table for
|
|
5
|
+
inclusion in the aggregation. For instance, if you are displaying aggregations about
|
|
6
|
+
a row from a Projects table, and a Task table has a key to projects field and a Boolean
|
|
7
|
+
field called `completed`, you may want to show the number of not-completed tasks for the Project.
|
|
8
|
+
In this case use the where expression `completed == false`.
|
|
9
|
+
|
|
10
|
+
You cannot here use view state or values from the row about which you are displaying aggregations. However,
|
|
11
|
+
if a user is logged in, the `user` variable can be used to access user fields, such as `user.id`.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
A calculated field is a column that holds values that are not entered
|
|
2
|
+
or changed directly but are instead calculated automatically from other
|
|
3
|
+
values in that row of from rows in other tables related by a key field.
|
|
4
|
+
|
|
5
|
+
There are two types of calculated field values:
|
|
6
|
+
|
|
7
|
+
*Not stored*: calculated fields that are not stored in the database are
|
|
8
|
+
recalculated every time the row is accessed. They are defined by a
|
|
9
|
+
JavaScript expression where the values of the other fields are in scope.
|
|
10
|
+
They are suitable for quick calculations that depend only on other values
|
|
11
|
+
in the same row. Because they are recalculated they are guaranteed to be
|
|
12
|
+
up to date. Non-stored fields cannot access data in related rows by join
|
|
13
|
+
fields or aggregations and cannot use asynchronous functions provided by
|
|
14
|
+
modules. They can use synchronous functions.
|
|
15
|
+
|
|
16
|
+
*Stored*: the values of stored fields are stored in the database table.
|
|
17
|
+
They are more flexible in the way they can be defined. Stored fields can
|
|
18
|
+
be defined either by a JavaScript expression or by an aggregation. When
|
|
19
|
+
defined by a JavaScript expression, join fields can be accessed by the
|
|
20
|
+
dot notation. Stored fields may also use both synchronous and asynchronous f
|
|
21
|
+
unctions provided by modules. Asynchronous functions can in principle do
|
|
22
|
+
input/output including calling APIs. This expression will be evaluated
|
|
23
|
+
every time the row changes.
|
|
24
|
+
|
|
25
|
+
Stored fields that are defined by an aggregation are automatically updated
|
|
26
|
+
when one of the child rows is updated. Stored fields that are based on
|
|
27
|
+
JavaScript expressions involving join felds are not automatically updated.
|
|
28
|
+
You need to run the recalculate stored fields action to update them
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
You can define how a date value should be shown by specifying a date format string
|
|
2
|
+
which supports a range of formatting options. The formatting is provided by the
|
|
3
|
+
[Day.js](https://day.js.org/docs/en/display/format) library with the
|
|
4
|
+
[AdvancedFormat](https://day.js.org/docs/en/plugin/advanced-format) extension. Those links
|
|
5
|
+
give the full definition for the available format options.
|
|
6
|
+
|
|
7
|
+
When a date is rendered with a format string, certain specific character sequences are
|
|
8
|
+
substituted for parts of the date value. Any unrecognized characters are repeated verbatim
|
|
9
|
+
in the output and you can used that for separator charcters such as hyphens (-) or slashes (/).
|
|
10
|
+
|
|
11
|
+
For example, the timepoint 11.15am on Friday, 4 October 2024 is rendered, according to the
|
|
12
|
+
format string:
|
|
13
|
+
|
|
14
|
+
{{# const d = new Date('2024-10-04T11:15:00.000')}}
|
|
15
|
+
|
|
16
|
+
| Format string | Output |
|
|
17
|
+
| ------------------- | ------------ |
|
|
18
|
+
| YYYY-MM-DD | {{ moment(d).format("YYYY-MM-DD")}} |
|
|
19
|
+
| H:mm on dddd, D MMMM YYYY | {{ moment(d).format("H:mm on dddd, D MMMM YYYY")}} |
|
|
20
|
+
|
|
21
|
+
The full list of supported format sequences are:
|
|
22
|
+
|
|
23
|
+
| Format | Output | Description |
|
|
24
|
+
| --- | --- | --- |
|
|
25
|
+
| `YY` | 18 | Two-digit year |
|
|
26
|
+
| `YYYY` | 2018 | Four-digit year |
|
|
27
|
+
| `M` | 1-12 | The month, beginning at 1 |
|
|
28
|
+
| `MM` | 01-12 | The month, 2-digits |
|
|
29
|
+
| `MMM` | Jan-Dec | The abbreviated month name |
|
|
30
|
+
| `MMMM` | January-December | The full month name |
|
|
31
|
+
| `D` | 1-31 | The day of the month |
|
|
32
|
+
| `DD` | 01-31 | The day of the month, 2-digits |
|
|
33
|
+
| `d` | 0-6 | The day of the week, with Sunday as 0 |
|
|
34
|
+
| `dd` | Su-Sa | The min name of the day of the week |
|
|
35
|
+
| `ddd` | Sun-Sat | The short name of the day of the week |
|
|
36
|
+
| `dddd` | Sunday-Saturday | The name of the day of the week |
|
|
37
|
+
| `H` | 0-23 | The hour |
|
|
38
|
+
| `HH` | 00-23 | The hour, 2-digits |
|
|
39
|
+
| `h` | 1-12 | The hour, 12-hour clock |
|
|
40
|
+
| `hh` | 01-12 | The hour, 12-hour clock, 2-digits |
|
|
41
|
+
| `m` | 0-59 | The minute |
|
|
42
|
+
| `mm` | 00-59 | The minute, 2-digits |
|
|
43
|
+
| `s` | 0-59 | The second |
|
|
44
|
+
| `ss` | 00-59 | The second, 2-digits |
|
|
45
|
+
| `SSS` | 000-999 | The millisecond, 3-digits |
|
|
46
|
+
| `Z` | +05:00 | The offset from UTC, ±HH:mm |
|
|
47
|
+
| `ZZ` | +0500 | The offset from UTC, ±HHmm |
|
|
48
|
+
| `A` | AM PM | |
|
|
49
|
+
| `a` | am pm | |
|
|
50
|
+
| `Q` | 1-4 | Quarter |
|
|
51
|
+
| `Do` | 1st 2nd ... 31st | Day of Month with ordinal |
|
|
52
|
+
| `k` | 1-24 | The hour, beginning at 1 |
|
|
53
|
+
| `kk` | 01-24 | The hour, 2-digits, beginning at 1 |
|
|
54
|
+
| `X` | 1360013296 | Unix Timestamp in second |
|
|
55
|
+
| `x` | 1360013296123 | Unix Timestamp in millisecond |
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
A snapshot is a saved format of your application that includes all of
|
|
2
|
+
your application structure (table and fields definitions, views pages
|
|
3
|
+
and configuration settings) but excludes all of your data, that is,
|
|
4
|
+
table rows and files. A snapshot is the same format as a pack - a snapshot
|
|
5
|
+
is in fact merely a full pack of the application. (To create a partial
|
|
6
|
+
pack of the application, use tags or the pack export in the module store).
|
|
7
|
+
|
|
8
|
+
Snapshots have several possible uses:
|
|
9
|
+
|
|
10
|
+
* If you have two different servers one for production and one for development
|
|
11
|
+
then you can make changes to the development server and use a snapshot to
|
|
12
|
+
move those changes to the production instance
|
|
13
|
+
|
|
14
|
+
* By enabling periodic snapshots Saltcorn allows you to restore versions of
|
|
15
|
+
views, pages and triggers after you have made changes. This allows you to
|
|
16
|
+
undo mistakes that you have made in building.
|
|
17
|
+
|
|
18
|
+
* If your application contains a large amount of data that can quickly be
|
|
19
|
+
regenerated you may want to use snapshots instead of backups to save your application.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
When version history on a database table is enabled in Saltcorn,
|
|
2
|
+
changes to row values are recorded in a secondary, hidden table.
|
|
3
|
+
This allows you to restore previous versions of individual rows
|
|
4
|
+
and undelete deleted rows.
|
|
5
|
+
|
|
6
|
+
The downside to enabling version history on tables is that this
|
|
7
|
+
can lead to a lot of data being retained which may mean your
|
|
8
|
+
database disk requirements will grow. In addition it will impact
|
|
9
|
+
performance when making changes to rows. There should be no
|
|
10
|
+
performance impact on reading data from the database.
|
|
11
|
+
|
|
12
|
+
Note that when the history is first enabled the existing rows in
|
|
13
|
+
the table are not copied into the history table in their present
|
|
14
|
+
state. Only when a change is made to a row (or a row is created)
|
|
15
|
+
is the new row copied into the history table.
|
|
16
|
+
|
|
17
|
+
Enabling version history gives the administrator access to the
|
|
18
|
+
version history in the administrator's data edit facility (Edit
|
|
19
|
+
button on the table page). Each row will show the number of versions
|
|
20
|
+
recorded and by clicking this you will see the list of versions.
|
|
21
|
+
Each version shows the value for each field, the user who made the
|
|
22
|
+
change, the time at which the change was made and an option to
|
|
23
|
+
restore. This is the only facility for interacting with the version
|
|
24
|
+
history in core Saltcorn.
|
|
25
|
+
|
|
26
|
+
The history-control module provides additional facilities for
|
|
27
|
+
interacting with the history. The history-control module provides:
|
|
28
|
+
|
|
29
|
+
* Actions to undo and redo row changes that will move the current
|
|
30
|
+
row backwards and forwards in the table history
|
|
31
|
+
|
|
32
|
+
* A history field difference view pattern that allows users to
|
|
33
|
+
compare different versions of a field value
|
|
34
|
+
|
|
35
|
+
* A "History for database table" table provider that can be used to create a "virtual"
|
|
36
|
+
table based on the history of an existing table. This table will have
|
|
37
|
+
all the fields of the original table and fields for the user making
|
|
38
|
+
the change and the time of the change being made
|
|
39
|
+
|
|
40
|
+
* A restore_from_history action that can be used on a view of the table
|
|
41
|
+
history (on the provided table) to restore or undelete any version of
|
|
42
|
+
a specific row
|
package/help/index.js
CHANGED
|
@@ -5,6 +5,7 @@ const _ = require("underscore");
|
|
|
5
5
|
const fs = require("fs").promises;
|
|
6
6
|
const MarkdownIt = require("markdown-it"),
|
|
7
7
|
md = new MarkdownIt();
|
|
8
|
+
const moment = require("moment");
|
|
8
9
|
|
|
9
10
|
const { pre } = require("@saltcorn/markup/tags");
|
|
10
11
|
const path = require("path");
|
|
@@ -33,6 +34,7 @@ const get_help_markup = async (topic, query, req) => {
|
|
|
33
34
|
scState: getState(),
|
|
34
35
|
query,
|
|
35
36
|
oneOf,
|
|
37
|
+
moment,
|
|
36
38
|
};
|
|
37
39
|
const mdTemplate = await get_md_file(topic);
|
|
38
40
|
if (!mdTemplate) return { markup: "Topic not found" };
|
package/load_plugins.js
CHANGED
|
@@ -12,6 +12,73 @@ const { isRoot } = require("@saltcorn/data/utils");
|
|
|
12
12
|
const { eachTenant } = require("@saltcorn/admin-models/models/tenant");
|
|
13
13
|
|
|
14
14
|
const PluginInstaller = require("@saltcorn/plugins-loader/plugin_installer");
|
|
15
|
+
const npmFetch = require("npm-registry-fetch");
|
|
16
|
+
const packagejson = require("./package.json");
|
|
17
|
+
const {
|
|
18
|
+
supportedVersion,
|
|
19
|
+
resolveLatest,
|
|
20
|
+
} = require("@saltcorn/plugins-loader/stable_versioning");
|
|
21
|
+
|
|
22
|
+
const isFixedPlugin = (plugin) =>
|
|
23
|
+
plugin.location === "@saltcorn/sbadmin2" ||
|
|
24
|
+
plugin.location === "@saltcorn/base-plugin";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* return the cached engine infos or fetch them from npm and update the cache
|
|
28
|
+
* @param plugin plugin to load
|
|
29
|
+
*/
|
|
30
|
+
const getEngineInfos = async (plugin, forceFetch) => {
|
|
31
|
+
const rootState = getRootState();
|
|
32
|
+
const cached = rootState.getConfig("engines_cache", {}) || {};
|
|
33
|
+
if (cached[plugin.location] && !forceFetch) {
|
|
34
|
+
return cached[plugin.location];
|
|
35
|
+
} else {
|
|
36
|
+
getState().log(5, `Fetching versions for '${plugin.location}'`);
|
|
37
|
+
const pkgInfo = await npmFetch.json(
|
|
38
|
+
`https://registry.npmjs.org/${plugin.location}`
|
|
39
|
+
);
|
|
40
|
+
const versions = pkgInfo.versions;
|
|
41
|
+
const newCached = {};
|
|
42
|
+
for (const [k, v] of Object.entries(versions)) {
|
|
43
|
+
newCached[k] = v.engines?.saltcorn
|
|
44
|
+
? { engines: { saltcorn: v.engines.saltcorn } }
|
|
45
|
+
: {};
|
|
46
|
+
}
|
|
47
|
+
cached[plugin.location] = newCached;
|
|
48
|
+
await rootState.setConfig("engines_cache", { ...cached });
|
|
49
|
+
return newCached;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* checks the saltcorn engine property and changes the plugin version if necessary
|
|
55
|
+
* @param plugin plugin to load
|
|
56
|
+
*/
|
|
57
|
+
const ensurePluginSupport = async (plugin, forceFetch) => {
|
|
58
|
+
let versions = await getEngineInfos(plugin, forceFetch);
|
|
59
|
+
if (
|
|
60
|
+
plugin.version &&
|
|
61
|
+
plugin.version !== "latest" &&
|
|
62
|
+
!versions[plugin.version] &&
|
|
63
|
+
!forceFetch
|
|
64
|
+
) {
|
|
65
|
+
versions = await getEngineInfos(plugin, true);
|
|
66
|
+
}
|
|
67
|
+
const supported = supportedVersion(
|
|
68
|
+
plugin.version || "latest",
|
|
69
|
+
versions,
|
|
70
|
+
packagejson.version
|
|
71
|
+
);
|
|
72
|
+
if (!supported)
|
|
73
|
+
throw new Error(
|
|
74
|
+
`Unable to find a supported version for '${plugin.location}'`
|
|
75
|
+
);
|
|
76
|
+
else if (
|
|
77
|
+
supported !== plugin.version ||
|
|
78
|
+
(plugin.version === "latest" && supported !== resolveLatest(versions))
|
|
79
|
+
)
|
|
80
|
+
plugin.version = supported;
|
|
81
|
+
};
|
|
15
82
|
|
|
16
83
|
/**
|
|
17
84
|
* Load one plugin
|
|
@@ -19,7 +86,16 @@ const PluginInstaller = require("@saltcorn/plugins-loader/plugin_installer");
|
|
|
19
86
|
* @param plugin - plugin to load
|
|
20
87
|
* @param force - force flag
|
|
21
88
|
*/
|
|
22
|
-
const loadPlugin = async (plugin, force) => {
|
|
89
|
+
const loadPlugin = async (plugin, force, forceFetch) => {
|
|
90
|
+
if (plugin.source === "npm" && !isFixedPlugin(plugin)) {
|
|
91
|
+
try {
|
|
92
|
+
await ensurePluginSupport(plugin, forceFetch);
|
|
93
|
+
} catch (e) {
|
|
94
|
+
console.log(
|
|
95
|
+
`Warning: Unable to find a supported version for '${plugin.location}' Continuing with the installed version`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
23
99
|
// load plugin
|
|
24
100
|
const loader = new PluginInstaller(plugin);
|
|
25
101
|
const res = await loader.install(force);
|
|
@@ -105,7 +181,7 @@ const loadAllPlugins = async (force) => {
|
|
|
105
181
|
}
|
|
106
182
|
}
|
|
107
183
|
await getState().refreshUserLayouts();
|
|
108
|
-
await getState().refresh(true);
|
|
184
|
+
await getState().refresh(true, true);
|
|
109
185
|
if (!isRoot()) reloadAuthFromRoot();
|
|
110
186
|
};
|
|
111
187
|
|
|
@@ -151,11 +227,14 @@ const loadAndSaveNewPlugin = async (
|
|
|
151
227
|
return;
|
|
152
228
|
}
|
|
153
229
|
}
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
const
|
|
230
|
+
if (plugin.source === "npm") await ensurePluginSupport(plugin);
|
|
231
|
+
const loadMsgs = [];
|
|
232
|
+
const loader = new PluginInstaller(plugin, {
|
|
233
|
+
scVersion: packagejson.version,
|
|
234
|
+
});
|
|
235
|
+
const { version, plugin_module, location, loadedWithReload, msgs } =
|
|
157
236
|
await loader.install(force);
|
|
158
|
-
|
|
237
|
+
if (msgs) loadMsgs.push(...msgs);
|
|
159
238
|
// install dependecies
|
|
160
239
|
for (const loc of plugin_module.dependencies || []) {
|
|
161
240
|
const existing = await Plugin.findOne({ location: loc });
|
|
@@ -201,7 +280,7 @@ const loadAndSaveNewPlugin = async (
|
|
|
201
280
|
}
|
|
202
281
|
}
|
|
203
282
|
if (loadedWithReload || registeredWithReload) {
|
|
204
|
-
|
|
283
|
+
loadMsgs.push(
|
|
205
284
|
__(
|
|
206
285
|
"The plugin was corrupted and had to be repaired. We recommend restarting your server.",
|
|
207
286
|
plugin.name
|
|
@@ -228,7 +307,7 @@ const loadAndSaveNewPlugin = async (
|
|
|
228
307
|
force: false, // okay ??
|
|
229
308
|
});
|
|
230
309
|
}
|
|
231
|
-
return
|
|
310
|
+
return loadMsgs;
|
|
232
311
|
};
|
|
233
312
|
|
|
234
313
|
module.exports = {
|
|
@@ -236,4 +315,6 @@ module.exports = {
|
|
|
236
315
|
loadAllPlugins,
|
|
237
316
|
loadPlugin,
|
|
238
317
|
requirePlugin,
|
|
318
|
+
getEngineInfos,
|
|
319
|
+
ensurePluginSupport,
|
|
239
320
|
};
|
package/locales/en.json
CHANGED
|
@@ -1464,5 +1464,17 @@
|
|
|
1464
1464
|
"Zip compression level": "Zip compression level",
|
|
1465
1465
|
"1=Fast, larger file, 9=Slow, smaller files": "1=Fast, larger file, 9=Slow, smaller files",
|
|
1466
1466
|
"Use system zip": "Use system zip",
|
|
1467
|
-
"Recommended. Executable <code>zip</code> must be installed": "Recommended. Executable <code>zip</code> must be installed"
|
|
1467
|
+
"Recommended. Executable <code>zip</code> must be installed": "Recommended. Executable <code>zip</code> must be installed",
|
|
1468
|
+
"Time to run": "Time to run",
|
|
1469
|
+
"Mobile": "Mobile",
|
|
1470
|
+
"Plain password trigger row": "Plain password trigger row",
|
|
1471
|
+
"Send plaintext password changes to Users table triggers (Insert, Update and Validate).": "Send plaintext password changes to Users table triggers (Insert, Update and Validate).",
|
|
1472
|
+
"Minimum user role required to create a new tenant<div class=\"alert alert-danger fst-normal\" role=\"alert\" data-show-if=\"showIfFormulaInputs($('select[name=role_to_create_tenant]'), '+role_to_create_tenant>1')\">Giving non-trusted users access to create tenants is a security risk and not recommended.</div>": "Minimum user role required to create a new tenant<div class=\"alert alert-danger fst-normal\" role=\"alert\" data-show-if=\"showIfFormulaInputs($('select[name=role_to_create_tenant]'), '+role_to_create_tenant>1')\">Giving non-trusted users access to create tenants is a security risk and not recommended.</div>",
|
|
1473
|
+
"Select tag": "Select tag",
|
|
1474
|
+
"Invalid build directory path": "Invalid build directory path",
|
|
1475
|
+
"Invalid build directory name": "Invalid build directory name",
|
|
1476
|
+
"clean node_modules": "clean node_modules",
|
|
1477
|
+
"After delete": "After delete",
|
|
1478
|
+
"Search only on exact match, not substring match. Useful for large tables": "Search only on exact match, not substring match. Useful for large tables",
|
|
1479
|
+
"Please select an entry point.": "Please select an entry point."
|
|
1468
1480
|
}
|