@saltcorn/server 0.9.0-beta.6 → 0.9.0-beta.8
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 +9 -3
- package/errors.js +15 -4
- package/help/Actions.tmd +9 -0
- package/help/JavaScript action code.tmd +159 -0
- package/locales/en.json +6 -1
- package/package.json +8 -8
- package/public/saltcorn-common.js +17 -2
- package/public/saltcorn.js +11 -2
- package/routes/actions.js +5 -3
- package/routes/admin.js +12 -0
- package/routes/fields.js +15 -3
- package/routes/packs.js +134 -9
- package/routes/sync.js +4 -1
- package/routes/viewedit.js +13 -0
- package/tests/admin.test.js +2 -2
- package/tests/sync.test.js +91 -6
package/app.js
CHANGED
|
@@ -128,17 +128,23 @@ const getApp = async (opts = {}) => {
|
|
|
128
128
|
app.use(helmet());
|
|
129
129
|
// TODO ch find a better solution
|
|
130
130
|
app.use(cors());
|
|
131
|
+
const bodyLimit = getState().getConfig("body_limit");
|
|
131
132
|
app.use(
|
|
132
133
|
express.json({
|
|
133
|
-
limit: "5mb",
|
|
134
|
+
limit: bodyLimit ? `${bodyLimit}kb` : "5mb",
|
|
134
135
|
verify: (req, res, buf) => {
|
|
135
136
|
req.rawBody = buf;
|
|
136
137
|
},
|
|
137
138
|
})
|
|
138
139
|
);
|
|
139
|
-
|
|
140
|
+
const urlencodedLimit = getState().getConfig("url_encoded_limit");
|
|
141
|
+
// extended url encoding in use
|
|
140
142
|
app.use(
|
|
141
|
-
express.urlencoded({
|
|
143
|
+
express.urlencoded({
|
|
144
|
+
limit: urlencodedLimit ? `${urlencodedLimit}kb` : "5mb",
|
|
145
|
+
extended: true,
|
|
146
|
+
parameterLimit: 50000,
|
|
147
|
+
})
|
|
142
148
|
);
|
|
143
149
|
|
|
144
150
|
// cookies
|
package/errors.js
CHANGED
|
@@ -46,10 +46,10 @@ module.exports =
|
|
|
46
46
|
? text(err.message)
|
|
47
47
|
: req.__("An error occurred")
|
|
48
48
|
);
|
|
49
|
-
} else
|
|
50
|
-
res
|
|
51
|
-
|
|
52
|
-
.sendWrap(
|
|
49
|
+
} else {
|
|
50
|
+
const _res = res.status(code);
|
|
51
|
+
if (_res.sendWrap)
|
|
52
|
+
_res.sendWrap(
|
|
53
53
|
req.__(headline),
|
|
54
54
|
devmode ? pre(text(err.stack)) : h3(req.__(headline)),
|
|
55
55
|
role === 1 && !devmode ? pre(text(err.message)) : "",
|
|
@@ -61,4 +61,15 @@ module.exports =
|
|
|
61
61
|
)
|
|
62
62
|
: ""
|
|
63
63
|
);
|
|
64
|
+
else
|
|
65
|
+
_res.send(
|
|
66
|
+
`<h2>${
|
|
67
|
+
err.message
|
|
68
|
+
? err.message
|
|
69
|
+
: req.__
|
|
70
|
+
? req.__("An error occurred")
|
|
71
|
+
: "An error occurred"
|
|
72
|
+
}</h2>`
|
|
73
|
+
);
|
|
74
|
+
}
|
|
64
75
|
};
|
package/help/Actions.tmd
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
The action chosen will determine what happens when the trigger occurs.
|
|
2
|
+
After you have chosen an action, there will be further configuration of that action.
|
|
3
|
+
|
|
4
|
+
The available actions, some of which are only accessible when the trigger is
|
|
5
|
+
table-related, are:
|
|
6
|
+
|
|
7
|
+
{{# for (const [name, action] of Object.entries(scState.actions)) { }}
|
|
8
|
+
* `{{name}}`: {{action.description||""}}
|
|
9
|
+
{{# } }}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
Here you can enter the code that needs to run when this trigger occurs.
|
|
2
|
+
The action can manipulate rows in the database, manipulate files, interact
|
|
3
|
+
with remote APIs, or issue directives for the user's display.
|
|
4
|
+
|
|
5
|
+
Your code can use await at the top level, and should do so whenever calling
|
|
6
|
+
database queries or other aynchronous code (see example below)
|
|
7
|
+
|
|
8
|
+
The variable `table` is the associated table (if any; note lowercase). If you want to access a different table,
|
|
9
|
+
use the `Table` variable (note uppercase) to access the Table class of tables (see
|
|
10
|
+
[documentation for Table class](https://saltcorn.github.io/saltcorn/classes/_saltcorn_data.models.Table-1.html))
|
|
11
|
+
|
|
12
|
+
Example:
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
await table.insertRow({name: "Alex", age: 43})
|
|
16
|
+
const otherTable = Table.findOne({name: "Orders"})
|
|
17
|
+
await otherTable.deleteRows({id: order})
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
In addition to `table` and `Table`, you can use other functions/variables:
|
|
21
|
+
|
|
22
|
+
#### `console`
|
|
23
|
+
|
|
24
|
+
Use this to print to the terminal.
|
|
25
|
+
|
|
26
|
+
Example: `console.log("Hello world")`
|
|
27
|
+
|
|
28
|
+
#### `Actions`
|
|
29
|
+
|
|
30
|
+
Use `Actions.{ACTION NAME}` to run an action.
|
|
31
|
+
|
|
32
|
+
Your available action names are: {{ Object.keys(scState.actions).join(", ") }}
|
|
33
|
+
|
|
34
|
+
Example:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
await Actions.set_user_language({language: "fr"})
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
#### `sleep`
|
|
41
|
+
|
|
42
|
+
A small utility function to sleep for certain number of milliseconds. Use this with await
|
|
43
|
+
|
|
44
|
+
Example: `await sleep(1000)`
|
|
45
|
+
|
|
46
|
+
#### `require`
|
|
47
|
+
|
|
48
|
+
Use require to access NPM packages listed under your [Development settings](/admin/dev)
|
|
49
|
+
|
|
50
|
+
Example: `const _ = require("underscore")`
|
|
51
|
+
|
|
52
|
+
#### `fetch` and `fetchJSON`
|
|
53
|
+
|
|
54
|
+
Use these to make HTTP API calls. `fetch` is the standard JavaScript `fetch` (provided by
|
|
55
|
+
[node-fetch](https://www.npmjs.com/package/node-fetch#common-usage)). `fetchJSON` performs a fetch
|
|
56
|
+
and then reads its reponse to JSON
|
|
57
|
+
|
|
58
|
+
Example:
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
const response = await fetch('https://api.github.com/users/github');
|
|
62
|
+
const data = await response.json();
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
which is the same as
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
const data = await fetchJSON('https://api.github.com/users/github');
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Return directives
|
|
72
|
+
|
|
73
|
+
Your code can with its return value give directives to the current page.
|
|
74
|
+
Valid return values are:
|
|
75
|
+
|
|
76
|
+
#### `notify`
|
|
77
|
+
|
|
78
|
+
Send a pop-up notification indicating success to the user
|
|
79
|
+
|
|
80
|
+
Example: `return { notify: "Order completed!" }`
|
|
81
|
+
|
|
82
|
+
#### `error`
|
|
83
|
+
|
|
84
|
+
Send a pop-up notification indicating error to the user.
|
|
85
|
+
|
|
86
|
+
Example: `return { error: "Invalid command!" }`
|
|
87
|
+
|
|
88
|
+
If this is triggered by an Edit view with the SubmitWithAjax,
|
|
89
|
+
halt navigation and stay on page. This can be used for complex validation logic,
|
|
90
|
+
When added as an Insert or Update trigger. If you delete the inserted row, You
|
|
91
|
+
may also need to clear the returned id in order to allow the user to continue editing.
|
|
92
|
+
|
|
93
|
+
Example:
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
if(amount>cash_on_hand) {
|
|
97
|
+
await table.deleteRows({ id })
|
|
98
|
+
return {
|
|
99
|
+
error: "Invalid order!",
|
|
100
|
+
id: null
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
#### `goto`
|
|
106
|
+
|
|
107
|
+
Navigate to a different URL:
|
|
108
|
+
|
|
109
|
+
Example: `return { goto: "https://saltcorn.com" }`
|
|
110
|
+
|
|
111
|
+
#### `reload_page`
|
|
112
|
+
|
|
113
|
+
Request a page reload with the existing URL.
|
|
114
|
+
|
|
115
|
+
Example: `return { reload_page: true }`
|
|
116
|
+
|
|
117
|
+
#### `popup`
|
|
118
|
+
|
|
119
|
+
Open a URL in a popup:
|
|
120
|
+
|
|
121
|
+
Example:
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
return { popup: `/view/Orders?id=${parent}` }
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
#### `download`
|
|
128
|
+
|
|
129
|
+
Download a file to the client browser.
|
|
130
|
+
|
|
131
|
+
Example:
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
return { download: {
|
|
135
|
+
mimetype: "text/csv",
|
|
136
|
+
blob: filecontents
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
#### `set_fields`
|
|
142
|
+
|
|
143
|
+
If triggered from an edit view, set fields dynamically in the form. The
|
|
144
|
+
value should be an object with keys that are field variable names.
|
|
145
|
+
|
|
146
|
+
Example:
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
return { set_fields: {
|
|
150
|
+
zidentifier: `${name.toUpperCase()}-${id}`
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
#### `eval_js`
|
|
156
|
+
|
|
157
|
+
Execute JavaScript in the browser.
|
|
158
|
+
|
|
159
|
+
Example: `return { eval_js: 'alert("Hello world")' }`
|
package/locales/en.json
CHANGED
|
@@ -1261,5 +1261,10 @@
|
|
|
1261
1261
|
"Module up-to-date": "Module up-to-date",
|
|
1262
1262
|
"Module '%s' not found": "Module '%s' not found",
|
|
1263
1263
|
"Module %s not found": "Module %s not found",
|
|
1264
|
-
"
|
|
1264
|
+
"Include Event Logs": "Include Event Logs",
|
|
1265
|
+
"Backup with event logs": "Backup with event logs",
|
|
1266
|
+
"Initially open": "Initially open",
|
|
1267
|
+
"Not a valid pack": "Not a valid pack",
|
|
1268
|
+
"Pack file": "Pack file",
|
|
1269
|
+
"Upload a pack file": "Upload a pack file"
|
|
1265
1270
|
}
|
package/package.json
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saltcorn/server",
|
|
3
|
-
"version": "0.9.0-beta.
|
|
3
|
+
"version": "0.9.0-beta.8",
|
|
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.9.0-beta.
|
|
10
|
-
"@saltcorn/builder": "0.9.0-beta.
|
|
11
|
-
"@saltcorn/data": "0.9.0-beta.
|
|
12
|
-
"@saltcorn/admin-models": "0.9.0-beta.
|
|
13
|
-
"@saltcorn/filemanager": "0.9.0-beta.
|
|
14
|
-
"@saltcorn/markup": "0.9.0-beta.
|
|
15
|
-
"@saltcorn/sbadmin2": "0.9.0-beta.
|
|
9
|
+
"@saltcorn/base-plugin": "0.9.0-beta.8",
|
|
10
|
+
"@saltcorn/builder": "0.9.0-beta.8",
|
|
11
|
+
"@saltcorn/data": "0.9.0-beta.8",
|
|
12
|
+
"@saltcorn/admin-models": "0.9.0-beta.8",
|
|
13
|
+
"@saltcorn/filemanager": "0.9.0-beta.8",
|
|
14
|
+
"@saltcorn/markup": "0.9.0-beta.8",
|
|
15
|
+
"@saltcorn/sbadmin2": "0.9.0-beta.8",
|
|
16
16
|
"@socket.io/cluster-adapter": "^0.2.1",
|
|
17
17
|
"@socket.io/sticky": "^1.0.1",
|
|
18
18
|
"adm-zip": "0.5.10",
|
|
@@ -51,6 +51,7 @@ const nubBy = (prop, xs) => {
|
|
|
51
51
|
});
|
|
52
52
|
};
|
|
53
53
|
function apply_showif() {
|
|
54
|
+
const isNode = typeof parent?.saltcorn?.data?.state === "undefined";
|
|
54
55
|
$("[data-show-if]").each(function (ix, element) {
|
|
55
56
|
var e = $(element);
|
|
56
57
|
try {
|
|
@@ -272,10 +273,12 @@ function apply_showif() {
|
|
|
272
273
|
|
|
273
274
|
if (typeof cache[recS] !== "undefined") {
|
|
274
275
|
e.html(cache[recS]);
|
|
276
|
+
e.prop("data-source-url-current", recS);
|
|
275
277
|
activate_onchange_coldef();
|
|
276
278
|
return;
|
|
277
279
|
}
|
|
278
|
-
|
|
280
|
+
|
|
281
|
+
const cb = {
|
|
279
282
|
success: (data) => {
|
|
280
283
|
e.html(data);
|
|
281
284
|
const cacheNow = e.prop("data-source-url-cache") || {};
|
|
@@ -295,7 +298,11 @@ function apply_showif() {
|
|
|
295
298
|
});
|
|
296
299
|
e.html("");
|
|
297
300
|
},
|
|
298
|
-
}
|
|
301
|
+
};
|
|
302
|
+
if (isNode) ajax_post_json(e.attr("data-source-url"), rec, cb);
|
|
303
|
+
else {
|
|
304
|
+
local_post_json(e.attr("data-source-url"), rec, cb);
|
|
305
|
+
}
|
|
299
306
|
});
|
|
300
307
|
const locale =
|
|
301
308
|
navigator.userLanguage ||
|
|
@@ -518,6 +525,11 @@ function initialize_page() {
|
|
|
518
525
|
});
|
|
519
526
|
|
|
520
527
|
$("form").change(apply_showif);
|
|
528
|
+
// also change if we select same
|
|
529
|
+
$("form select").on("blur", (e) => {
|
|
530
|
+
if (!e || !e.target) return;
|
|
531
|
+
$(e.target).closest("form").trigger("change");
|
|
532
|
+
});
|
|
521
533
|
apply_showif();
|
|
522
534
|
apply_showif();
|
|
523
535
|
$("[data-inline-edit-dest-url]").each(function () {
|
|
@@ -678,10 +690,13 @@ function initialize_page() {
|
|
|
678
690
|
setTimeout(() => {
|
|
679
691
|
codes.forEach((el) => {
|
|
680
692
|
//console.log($(el).attr("mode"), el);
|
|
693
|
+
if ($(el).hasClass("codemirror-enabled")) return;
|
|
694
|
+
|
|
681
695
|
const cm = CodeMirror.fromTextArea(el, {
|
|
682
696
|
lineNumbers: true,
|
|
683
697
|
mode: $(el).attr("mode"),
|
|
684
698
|
});
|
|
699
|
+
$(el).addClass("codemirror-enabled");
|
|
685
700
|
cm.on(
|
|
686
701
|
"change",
|
|
687
702
|
$.debounce(() => {
|
package/public/saltcorn.js
CHANGED
|
@@ -356,6 +356,15 @@ function selectVersionError(res, btnId) {
|
|
|
356
356
|
restore_old_button(btnId);
|
|
357
357
|
}
|
|
358
358
|
|
|
359
|
+
function submitWithAjax(e) {
|
|
360
|
+
saveAndContinue(e, (res) => {
|
|
361
|
+
if (res && res.responseJSON && res.responseJSON.url_when_done)
|
|
362
|
+
window.location.href = res.responseJSON.url_when_done;
|
|
363
|
+
if (res && res.responseJSON && res.responseJSON.error)
|
|
364
|
+
notifyAlert({ type: "danger", text: res.responseJSON.error });
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
|
|
359
368
|
function saveAndContinue(e, k) {
|
|
360
369
|
var form = $(e).closest("form");
|
|
361
370
|
const valres = form[0].reportValidity();
|
|
@@ -404,8 +413,8 @@ function saveAndContinue(e, k) {
|
|
|
404
413
|
}
|
|
405
414
|
ajax_indicate_error(e, request);
|
|
406
415
|
},
|
|
407
|
-
complete: function () {
|
|
408
|
-
if (k) k();
|
|
416
|
+
complete: function (res) {
|
|
417
|
+
if (k) k(res);
|
|
409
418
|
},
|
|
410
419
|
});
|
|
411
420
|
|
package/routes/actions.js
CHANGED
|
@@ -174,8 +174,9 @@ const triggerForm = async (req, trigger) => {
|
|
|
174
174
|
attributes: {
|
|
175
175
|
explainers: {
|
|
176
176
|
Often: req.__("Every 5 minutes"),
|
|
177
|
-
Never:
|
|
178
|
-
|
|
177
|
+
Never: req.__(
|
|
178
|
+
"Not scheduled but can be run as an action from a button click"
|
|
179
|
+
),
|
|
179
180
|
},
|
|
180
181
|
},
|
|
181
182
|
},
|
|
@@ -201,6 +202,7 @@ const triggerForm = async (req, trigger) => {
|
|
|
201
202
|
label: req.__("Action"),
|
|
202
203
|
type: "String",
|
|
203
204
|
required: true,
|
|
205
|
+
help: { topic: "Actions" },
|
|
204
206
|
attributes: {
|
|
205
207
|
calcOptions: ["when_trigger", action_options],
|
|
206
208
|
},
|
|
@@ -402,7 +404,7 @@ router.get(
|
|
|
402
404
|
form.values = trigger.configuration;
|
|
403
405
|
const events = Trigger.when_options;
|
|
404
406
|
const actions = Trigger.find({
|
|
405
|
-
when_trigger: {or: ["API call", "Never"]},
|
|
407
|
+
when_trigger: { or: ["API call", "Never"] },
|
|
406
408
|
});
|
|
407
409
|
const tables = (await Table.find({})).map((t) => ({
|
|
408
410
|
name: t.name,
|
package/routes/admin.js
CHANGED
|
@@ -285,6 +285,9 @@ router.get(
|
|
|
285
285
|
backupForm.values.auto_backup_expire_days = getState().getConfig(
|
|
286
286
|
"auto_backup_expire_days"
|
|
287
287
|
);
|
|
288
|
+
backupForm.values.backup_with_event_log = getState().getConfig(
|
|
289
|
+
"backup_with_event_log"
|
|
290
|
+
);
|
|
288
291
|
//
|
|
289
292
|
const aSnapshotForm = snapshotForm(req);
|
|
290
293
|
aSnapshotForm.values.snapshots_enabled =
|
|
@@ -721,6 +724,15 @@ const autoBackupForm = (req) =>
|
|
|
721
724
|
auto_backup_destination: "Local directory",
|
|
722
725
|
},
|
|
723
726
|
},
|
|
727
|
+
{
|
|
728
|
+
type: "Bool",
|
|
729
|
+
label: req.__("Include Event Logs"),
|
|
730
|
+
sublabel: req.__("Backup with event logs"),
|
|
731
|
+
name: "backup_with_event_log",
|
|
732
|
+
showIf: {
|
|
733
|
+
auto_backup_frequency: ["Daily", "Weekly"],
|
|
734
|
+
},
|
|
735
|
+
},
|
|
724
736
|
],
|
|
725
737
|
});
|
|
726
738
|
|
package/routes/fields.js
CHANGED
|
@@ -409,7 +409,7 @@ const fieldFlow = (req) =>
|
|
|
409
409
|
instance_options[model.name].push(...instances.map((i) => i.name));
|
|
410
410
|
|
|
411
411
|
const outputs = await applyAsync(
|
|
412
|
-
model.templateObj
|
|
412
|
+
model.templateObj?.prediction_outputs || [], // unit tests can have templateObj undefined
|
|
413
413
|
{ table, configuration: model.configuration }
|
|
414
414
|
);
|
|
415
415
|
output_options[model.name] = outputs.map((o) => o.name);
|
|
@@ -840,6 +840,11 @@ router.post(
|
|
|
840
840
|
const table = Table.findOne({ name: tableName });
|
|
841
841
|
const role = req.user && req.user.id ? req.user.role_id : 100;
|
|
842
842
|
|
|
843
|
+
getState().log(
|
|
844
|
+
5,
|
|
845
|
+
`Route /fields/show-calculated/${tableName}/${fieldName}/${fieldview} user=${req.user?.id}`
|
|
846
|
+
);
|
|
847
|
+
|
|
843
848
|
const fields = table.getFields();
|
|
844
849
|
let row = { ...req.body };
|
|
845
850
|
if (row && Object.keys(row).length > 0) readState(row, fields);
|
|
@@ -1018,6 +1023,13 @@ router.post(
|
|
|
1018
1023
|
const { tableName, fieldName, fieldview } = req.params;
|
|
1019
1024
|
const table = Table.findOne({ name: tableName });
|
|
1020
1025
|
const fields = table.getFields();
|
|
1026
|
+
const state = getState();
|
|
1027
|
+
|
|
1028
|
+
state.log(
|
|
1029
|
+
5,
|
|
1030
|
+
`Route /fields/preview/${tableName}/${fieldName}/${fieldview} user=${req.user?.id}`
|
|
1031
|
+
);
|
|
1032
|
+
|
|
1021
1033
|
let field, row, value;
|
|
1022
1034
|
if (fieldName.includes(".")) {
|
|
1023
1035
|
const [refNm, targetNm] = fieldName.split(".");
|
|
@@ -1048,9 +1060,9 @@ router.post(
|
|
|
1048
1060
|
}
|
|
1049
1061
|
const fieldviews =
|
|
1050
1062
|
field.type === "Key"
|
|
1051
|
-
?
|
|
1063
|
+
? state.keyFieldviews
|
|
1052
1064
|
: field.type === "File"
|
|
1053
|
-
?
|
|
1065
|
+
? state.fileviews
|
|
1054
1066
|
: field.type.fieldviews;
|
|
1055
1067
|
if (!field.type || !fieldviews) {
|
|
1056
1068
|
res.send("");
|
package/routes/packs.js
CHANGED
|
@@ -6,18 +6,19 @@
|
|
|
6
6
|
|
|
7
7
|
const Router = require("express-promise-router");
|
|
8
8
|
const { isAdmin, error_catcher } = require("./utils.js");
|
|
9
|
-
const {
|
|
10
|
-
const { getState } = require("@saltcorn/data/db/state");
|
|
9
|
+
const { renderForm } = require("@saltcorn/markup");
|
|
11
10
|
const Table = require("@saltcorn/data/models/table");
|
|
12
11
|
const Form = require("@saltcorn/data/models/form");
|
|
13
12
|
const View = require("@saltcorn/data/models/view");
|
|
14
|
-
const Field = require("@saltcorn/data/models/field");
|
|
15
13
|
const Plugin = require("@saltcorn/data/models/plugin");
|
|
16
14
|
const Page = require("@saltcorn/data/models/page");
|
|
15
|
+
const Tag = require("@saltcorn/data/models/tag");
|
|
16
|
+
const EventLog = require("@saltcorn/data/models/eventlog");
|
|
17
|
+
const Model = require("@saltcorn/data/models/model");
|
|
18
|
+
const ModelInstance = require("@saltcorn/data/models/model_instance");
|
|
17
19
|
const load_plugins = require("../load_plugins");
|
|
18
20
|
|
|
19
21
|
const { is_pack } = require("@saltcorn/data/contracts");
|
|
20
|
-
const { contract, is } = require("contractis");
|
|
21
22
|
const {
|
|
22
23
|
table_pack,
|
|
23
24
|
view_pack,
|
|
@@ -26,15 +27,20 @@ const {
|
|
|
26
27
|
role_pack,
|
|
27
28
|
library_pack,
|
|
28
29
|
trigger_pack,
|
|
30
|
+
tag_pack,
|
|
31
|
+
model_pack,
|
|
32
|
+
model_instance_pack,
|
|
29
33
|
install_pack,
|
|
30
34
|
fetch_pack_by_name,
|
|
31
35
|
can_install_pack,
|
|
32
36
|
uninstall_pack,
|
|
37
|
+
event_log_pack,
|
|
33
38
|
} = require("@saltcorn/admin-models/models/pack");
|
|
34
|
-
const {
|
|
39
|
+
const { pre, code, p, text, text_attr } = require("@saltcorn/markup/tags");
|
|
35
40
|
const Library = require("@saltcorn/data/models/library");
|
|
36
41
|
const Trigger = require("@saltcorn/data/models/trigger");
|
|
37
42
|
const Role = require("@saltcorn/data/models/role");
|
|
43
|
+
const fs = require("fs");
|
|
38
44
|
|
|
39
45
|
/**
|
|
40
46
|
* @type {object}
|
|
@@ -98,6 +104,52 @@ router.get(
|
|
|
98
104
|
name: `role.${l.role}`,
|
|
99
105
|
type: "Bool",
|
|
100
106
|
}));
|
|
107
|
+
const tags = await Tag.find({});
|
|
108
|
+
const tagFields = tags.map((t) => ({
|
|
109
|
+
label: `${t.name} tag`,
|
|
110
|
+
name: `tag.${t.name}`,
|
|
111
|
+
type: "Bool",
|
|
112
|
+
}));
|
|
113
|
+
const models = await Model.find({});
|
|
114
|
+
const modelFields = models.map((m) => {
|
|
115
|
+
const modelTbl = Table.findOne({ id: m.table_id });
|
|
116
|
+
return {
|
|
117
|
+
label: `${m.name} model, table: ${
|
|
118
|
+
modelTbl.name || req.__("Table not found")
|
|
119
|
+
}`,
|
|
120
|
+
name: `model.${m.name}.${modelTbl.name}`,
|
|
121
|
+
type: "Bool",
|
|
122
|
+
};
|
|
123
|
+
});
|
|
124
|
+
const modelInstances = await ModelInstance.find({});
|
|
125
|
+
const modelInstanceFields = (
|
|
126
|
+
await Promise.all(
|
|
127
|
+
modelInstances.map(async (instance) => {
|
|
128
|
+
const model = await Model.findOne({ id: instance.model_id });
|
|
129
|
+
if (!model) {
|
|
130
|
+
req.flash(
|
|
131
|
+
"warning",
|
|
132
|
+
req.__(`Model with '${instance.model_id}' not found`)
|
|
133
|
+
);
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
const mTable = await Table.findOne({ id: model.table_id });
|
|
137
|
+
if (!mTable) {
|
|
138
|
+
req.flash(
|
|
139
|
+
"warning",
|
|
140
|
+
req.__(`Table of model '${model.name}' not found`)
|
|
141
|
+
);
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
label: `${instance.name} model instance, model: ${model.name}, table: ${mTable.name}`,
|
|
146
|
+
name: `model_instance.${instance.name}.${model.name}.${mTable.name}`,
|
|
147
|
+
type: "Bool",
|
|
148
|
+
};
|
|
149
|
+
})
|
|
150
|
+
)
|
|
151
|
+
).filter((f) => f);
|
|
152
|
+
|
|
101
153
|
const form = new Form({
|
|
102
154
|
action: "/packs/create",
|
|
103
155
|
fields: [
|
|
@@ -108,6 +160,14 @@ router.get(
|
|
|
108
160
|
...trigFields,
|
|
109
161
|
...roleFields,
|
|
110
162
|
...libFields,
|
|
163
|
+
...tagFields,
|
|
164
|
+
...modelFields,
|
|
165
|
+
...modelInstanceFields,
|
|
166
|
+
{
|
|
167
|
+
name: "with_event_logs",
|
|
168
|
+
label: req.__("Include Event Logs"),
|
|
169
|
+
type: "Bool",
|
|
170
|
+
},
|
|
111
171
|
],
|
|
112
172
|
});
|
|
113
173
|
res.sendWrap(req.__(`Create Pack`), {
|
|
@@ -140,7 +200,7 @@ router.post(
|
|
|
140
200
|
"/create",
|
|
141
201
|
isAdmin,
|
|
142
202
|
error_catcher(async (req, res) => {
|
|
143
|
-
|
|
203
|
+
const pack = {
|
|
144
204
|
tables: [],
|
|
145
205
|
views: [],
|
|
146
206
|
plugins: [],
|
|
@@ -148,9 +208,13 @@ router.post(
|
|
|
148
208
|
roles: [],
|
|
149
209
|
library: [],
|
|
150
210
|
triggers: [],
|
|
211
|
+
tags: [],
|
|
212
|
+
models: [],
|
|
213
|
+
model_instances: [],
|
|
214
|
+
event_logs: [],
|
|
151
215
|
};
|
|
152
216
|
for (const k of Object.keys(req.body)) {
|
|
153
|
-
const [type, name] = k.split(".");
|
|
217
|
+
const [type, name, ...rest] = k.split(".");
|
|
154
218
|
switch (type) {
|
|
155
219
|
case "table":
|
|
156
220
|
pack.tables.push(await table_pack(name));
|
|
@@ -173,7 +237,32 @@ router.post(
|
|
|
173
237
|
case "trigger":
|
|
174
238
|
pack.triggers.push(await trigger_pack(name));
|
|
175
239
|
break;
|
|
176
|
-
|
|
240
|
+
case "tag":
|
|
241
|
+
pack.tags.push(await tag_pack(name));
|
|
242
|
+
break;
|
|
243
|
+
case "model": {
|
|
244
|
+
const table = rest[0];
|
|
245
|
+
if (!table) throw new Error(`Table for model '${name}' not found`);
|
|
246
|
+
pack.models.push(await model_pack(name, table));
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
case "model_instance": {
|
|
250
|
+
const model = rest[0];
|
|
251
|
+
if (!model)
|
|
252
|
+
throw new Error(`Model of Model Instance '${name}' not found`);
|
|
253
|
+
const table = rest[1];
|
|
254
|
+
if (!table) throw new Error(`Table of Model '${model}' not found`);
|
|
255
|
+
pack.model_instances.push(
|
|
256
|
+
await model_instance_pack(name, model, table)
|
|
257
|
+
);
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
case "with_event_logs":
|
|
261
|
+
const logs = await EventLog.find({});
|
|
262
|
+
pack.event_logs = await Promise.all(
|
|
263
|
+
logs.map(async (l) => await event_log_pack(l))
|
|
264
|
+
);
|
|
265
|
+
break;
|
|
177
266
|
default:
|
|
178
267
|
break;
|
|
179
268
|
}
|
|
@@ -217,11 +306,33 @@ const install_pack_form = (req) =>
|
|
|
217
306
|
action: "/packs/install",
|
|
218
307
|
submitLabel: req.__("Install"),
|
|
219
308
|
fields: [
|
|
309
|
+
{
|
|
310
|
+
name: "source",
|
|
311
|
+
label: req.__("Source"),
|
|
312
|
+
type: "String",
|
|
313
|
+
attributes: {
|
|
314
|
+
options: [
|
|
315
|
+
{ label: "from text", name: "from_text" },
|
|
316
|
+
{ label: "from file", name: "from_file" },
|
|
317
|
+
],
|
|
318
|
+
},
|
|
319
|
+
default: "from_text",
|
|
320
|
+
required: true,
|
|
321
|
+
},
|
|
220
322
|
{
|
|
221
323
|
name: "pack",
|
|
222
324
|
label: req.__("Pack"),
|
|
223
325
|
type: "String",
|
|
224
326
|
fieldview: "textarea",
|
|
327
|
+
showIf: { source: "from_text" },
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
name: "pack_file",
|
|
331
|
+
label: req.__("Pack file"),
|
|
332
|
+
class: "form-control",
|
|
333
|
+
type: "File",
|
|
334
|
+
sublabel: req.__("Upload a pack file"),
|
|
335
|
+
showIf: { source: "from_file" },
|
|
225
336
|
},
|
|
226
337
|
],
|
|
227
338
|
});
|
|
@@ -267,8 +378,22 @@ router.post(
|
|
|
267
378
|
isAdmin,
|
|
268
379
|
error_catcher(async (req, res) => {
|
|
269
380
|
var pack, error;
|
|
381
|
+
const source = req.body.source || "from_text";
|
|
270
382
|
try {
|
|
271
|
-
|
|
383
|
+
switch (source) {
|
|
384
|
+
case "from_text":
|
|
385
|
+
pack = JSON.parse(req.body.pack);
|
|
386
|
+
break;
|
|
387
|
+
case "from_file":
|
|
388
|
+
if (req.files?.pack_file?.tempFilePath)
|
|
389
|
+
pack = JSON.parse(
|
|
390
|
+
fs.readFileSync(req.files?.pack_file?.tempFilePath)
|
|
391
|
+
);
|
|
392
|
+
else throw new Error(req.__("No file uploaded"));
|
|
393
|
+
break;
|
|
394
|
+
default:
|
|
395
|
+
throw new Error(req.__("Invalid source"));
|
|
396
|
+
}
|
|
272
397
|
} catch (e) {
|
|
273
398
|
error = e.message;
|
|
274
399
|
}
|
package/routes/sync.js
CHANGED
|
@@ -312,7 +312,10 @@ router.get(
|
|
|
312
312
|
const translatedIds = JSON.parse(
|
|
313
313
|
await fs.readFile(path.join(syncDir, "translated-ids.json"))
|
|
314
314
|
);
|
|
315
|
-
|
|
315
|
+
const uniqueConflicts = JSON.parse(
|
|
316
|
+
await fs.readFile(path.join(syncDir, "unique-conflicts.json"))
|
|
317
|
+
);
|
|
318
|
+
res.json({ finished: true, translatedIds, uniqueConflicts });
|
|
316
319
|
} else if (entries.indexOf("error.json") >= 0) {
|
|
317
320
|
const error = JSON.parse(
|
|
318
321
|
await fs.readFile(path.join(syncDir, "error.json"))
|
package/routes/viewedit.js
CHANGED
|
@@ -632,6 +632,19 @@ router.post(
|
|
|
632
632
|
|
|
633
633
|
const view = await View.findOne({ name });
|
|
634
634
|
const configFlow = await view.get_config_flow(req);
|
|
635
|
+
configFlow.onStepSuccess = async (step, context) => {
|
|
636
|
+
let newcfg;
|
|
637
|
+
if (step.contextField)
|
|
638
|
+
newcfg = {
|
|
639
|
+
...view.configuration,
|
|
640
|
+
[step.contextField]: {
|
|
641
|
+
...view.configuration?.[step.contextField],
|
|
642
|
+
...context,
|
|
643
|
+
},
|
|
644
|
+
};
|
|
645
|
+
else newcfg = { ...view.configuration, ...context };
|
|
646
|
+
await View.update({ configuration: newcfg }, view.id);
|
|
647
|
+
};
|
|
635
648
|
const wfres = await configFlow.run(req.body, req);
|
|
636
649
|
|
|
637
650
|
let table;
|
package/tests/admin.test.js
CHANGED
|
@@ -568,11 +568,11 @@ describe("tags", () => {
|
|
|
568
568
|
.post("/tag")
|
|
569
569
|
.set("Cookie", loginCookie)
|
|
570
570
|
.send("name=MyNewTestTag")
|
|
571
|
-
.expect(toRedirect("/tag/
|
|
571
|
+
.expect(toRedirect("/tag/2?show_list=tables"));
|
|
572
572
|
});
|
|
573
573
|
|
|
574
574
|
itShouldIncludeTextForAdmin("/tag", "MyNewTestTag");
|
|
575
|
-
itShouldIncludeTextForAdmin("/tag/
|
|
575
|
+
itShouldIncludeTextForAdmin("/tag/2", "MyNewTestTag");
|
|
576
576
|
itShouldIncludeTextForAdmin("/tag-entries/add/tables/1", "Add entries");
|
|
577
577
|
itShouldIncludeTextForAdmin("/tag-entries/add/pages/1", "Add entries");
|
|
578
578
|
itShouldIncludeTextForAdmin("/tag-entries/add/views/1", "Add entries");
|
package/tests/sync.test.js
CHANGED
|
@@ -13,6 +13,7 @@ const db = require("@saltcorn/data/db");
|
|
|
13
13
|
const { sleep } = require("@saltcorn/data/tests/mocks");
|
|
14
14
|
|
|
15
15
|
const Table = require("@saltcorn/data/models/table");
|
|
16
|
+
const TableConstraint = require("@saltcorn/data/models/table_constraints");
|
|
16
17
|
const Field = require("@saltcorn/data/models/field");
|
|
17
18
|
const User = require("@saltcorn/data/models/user");
|
|
18
19
|
|
|
@@ -349,8 +350,9 @@ describe("Upload changes", () => {
|
|
|
349
350
|
.get(`/sync/upload_finished?dir_name=${encodeURIComponent(syncDir)}`)
|
|
350
351
|
.set("Cookie", loginCookie);
|
|
351
352
|
expect(resp.status).toBe(200);
|
|
352
|
-
const { finished, translatedIds, error } = resp._body;
|
|
353
|
-
if (finished)
|
|
353
|
+
const { finished, translatedIds, uniqueConflicts, error } = resp._body;
|
|
354
|
+
if (finished)
|
|
355
|
+
return translatedIds ? { translatedIds, uniqueConflicts } : error;
|
|
354
356
|
await sleep(1000);
|
|
355
357
|
}
|
|
356
358
|
return null;
|
|
@@ -401,7 +403,7 @@ describe("Upload changes", () => {
|
|
|
401
403
|
});
|
|
402
404
|
expect(resp.status).toBe(200);
|
|
403
405
|
const { syncDir } = resp._body;
|
|
404
|
-
const translatedIds = await getResult(app, loginCookie, syncDir);
|
|
406
|
+
const { translatedIds } = await getResult(app, loginCookie, syncDir);
|
|
405
407
|
await cleanSyncDir(app, loginCookie, syncDir);
|
|
406
408
|
expect(translatedIds).toBeDefined();
|
|
407
409
|
expect(translatedIds).toEqual({
|
|
@@ -414,6 +416,89 @@ describe("Upload changes", () => {
|
|
|
414
416
|
});
|
|
415
417
|
});
|
|
416
418
|
|
|
419
|
+
it("handles inserts with TableConstraint conflicts", async () => {
|
|
420
|
+
const books = Table.findOne({ name: "books" });
|
|
421
|
+
const oldCount = await books.countRows();
|
|
422
|
+
// unique constraint for author + pages
|
|
423
|
+
const constraint = await TableConstraint.create({
|
|
424
|
+
table: books,
|
|
425
|
+
type: "Unique",
|
|
426
|
+
configuration: {
|
|
427
|
+
fields: ["author", "pages"],
|
|
428
|
+
},
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
const app = await getApp({ disableCsrf: true });
|
|
432
|
+
const loginCookie = await getAdminLoginCookie();
|
|
433
|
+
const resp = await doUpload(app, loginCookie, new Date().valueOf(), {
|
|
434
|
+
books: {
|
|
435
|
+
inserts: [
|
|
436
|
+
{
|
|
437
|
+
author: "Herman Melville",
|
|
438
|
+
pages: 967,
|
|
439
|
+
publisher: 1,
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
author: "Leo Tolstoy",
|
|
443
|
+
pages: "728",
|
|
444
|
+
publisher: 2,
|
|
445
|
+
},
|
|
446
|
+
],
|
|
447
|
+
},
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
expect(resp.status).toBe(200);
|
|
451
|
+
const { syncDir } = resp._body;
|
|
452
|
+
const { uniqueConflicts } = await getResult(app, loginCookie, syncDir);
|
|
453
|
+
await constraint.delete();
|
|
454
|
+
await cleanSyncDir(app, loginCookie, syncDir);
|
|
455
|
+
expect(uniqueConflicts).toBeDefined();
|
|
456
|
+
expect(uniqueConflicts).toEqual({
|
|
457
|
+
books: [
|
|
458
|
+
{ id: 1, author: "Herman Melville", pages: 967, publisher: null },
|
|
459
|
+
{ id: 2, author: "Leo Tolstoy", pages: 728, publisher: 1 },
|
|
460
|
+
],
|
|
461
|
+
});
|
|
462
|
+
const newCount = await books.countRows();
|
|
463
|
+
expect(newCount).toBe(oldCount);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
it("denies updates with TableConstraint conflicts", async () => {
|
|
467
|
+
const books = Table.findOne({ name: "books" });
|
|
468
|
+
const oldCount = await books.countRows();
|
|
469
|
+
// unique constraint for author + pages
|
|
470
|
+
const constraint = await TableConstraint.create({
|
|
471
|
+
table: books,
|
|
472
|
+
type: "Unique",
|
|
473
|
+
configuration: {
|
|
474
|
+
fields: ["author", "pages"],
|
|
475
|
+
},
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
const app = await getApp({ disableCsrf: true });
|
|
479
|
+
const loginCookie = await getAdminLoginCookie();
|
|
480
|
+
const resp = await doUpload(app, loginCookie, new Date().valueOf(), {
|
|
481
|
+
books: {
|
|
482
|
+
updates: [
|
|
483
|
+
{
|
|
484
|
+
id: 2,
|
|
485
|
+
author: "Herman Melville",
|
|
486
|
+
pages: 967,
|
|
487
|
+
},
|
|
488
|
+
],
|
|
489
|
+
},
|
|
490
|
+
});
|
|
491
|
+
expect(resp.status).toBe(200);
|
|
492
|
+
const { syncDir } = resp._body;
|
|
493
|
+
const error = await getResult(app, loginCookie, syncDir);
|
|
494
|
+
await constraint.delete();
|
|
495
|
+
await cleanSyncDir(app, loginCookie, syncDir);
|
|
496
|
+
expect(error).toBeDefined();
|
|
497
|
+
expect(error).toEqual({
|
|
498
|
+
message: "Duplicate value for unique field: author_pages",
|
|
499
|
+
});
|
|
500
|
+
});
|
|
501
|
+
|
|
417
502
|
it("update with translation", async () => {
|
|
418
503
|
const app = await getApp({ disableCsrf: true });
|
|
419
504
|
const loginCookie = await getAdminLoginCookie();
|
|
@@ -438,7 +523,7 @@ describe("Upload changes", () => {
|
|
|
438
523
|
});
|
|
439
524
|
expect(resp.status).toBe(200);
|
|
440
525
|
const { syncDir } = resp._body;
|
|
441
|
-
const translatedIds = await getResult(app, loginCookie, syncDir);
|
|
526
|
+
const { translatedIds } = await getResult(app, loginCookie, syncDir);
|
|
442
527
|
await cleanSyncDir(app, loginCookie, syncDir);
|
|
443
528
|
expect(translatedIds).toBeDefined();
|
|
444
529
|
expect(translatedIds).toEqual({
|
|
@@ -476,7 +561,7 @@ describe("Upload changes", () => {
|
|
|
476
561
|
});
|
|
477
562
|
expect(resp.status).toBe(200);
|
|
478
563
|
const { syncDir } = resp._body;
|
|
479
|
-
const translatedIds = await getResult(app, loginCookie, syncDir);
|
|
564
|
+
const { translatedIds } = await getResult(app, loginCookie, syncDir);
|
|
480
565
|
await cleanSyncDir(app, loginCookie, syncDir);
|
|
481
566
|
expect(translatedIds).toBeDefined();
|
|
482
567
|
const afterDelete = await books.getRows();
|
|
@@ -520,7 +605,7 @@ describe("Upload changes", () => {
|
|
|
520
605
|
});
|
|
521
606
|
expect(resp.status).toBe(200);
|
|
522
607
|
const { syncDir } = resp._body;
|
|
523
|
-
const translatedIds = await getResult(app, loginCookie, syncDir);
|
|
608
|
+
const { translatedIds } = await getResult(app, loginCookie, syncDir);
|
|
524
609
|
await cleanSyncDir(app, loginCookie, syncDir);
|
|
525
610
|
expect(translatedIds).toBeDefined();
|
|
526
611
|
const afterDelete = await books.getRows();
|