@saltcorn/server 0.9.1-beta.6 → 0.9.1-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/auth/admin.js +1 -1
- package/auth/routes.js +4 -6
- package/help/Event types.tmd +7 -0
- package/help/JavaScript action code.tmd +1 -14
- package/locales/en.json +2 -1
- package/package.json +8 -8
- package/public/saltcorn-common.js +35 -24
- package/public/saltcorn.js +1 -1
- package/routes/actions.js +1 -1
- package/routes/page.js +8 -6
- package/routes/view.js +8 -7
- package/tests/view.test.js +58 -0
package/auth/admin.js
CHANGED
package/auth/routes.js
CHANGED
|
@@ -877,17 +877,17 @@ router.post(
|
|
|
877
877
|
|
|
878
878
|
const unsuitableEmailPassword = async (urecord) => {
|
|
879
879
|
const { email, password, passwordRepeat } = urecord;
|
|
880
|
-
if (
|
|
880
|
+
if (email == "" || !password) {
|
|
881
881
|
req.flash("danger", req.__("E-mail and password required"));
|
|
882
882
|
res.redirect("/auth/signup");
|
|
883
883
|
return true;
|
|
884
884
|
}
|
|
885
|
-
if (email.length > 127) {
|
|
885
|
+
if (email && email.length > 127) {
|
|
886
886
|
req.flash("danger", req.__("E-mail too long"));
|
|
887
887
|
res.redirect("/auth/signup");
|
|
888
888
|
return true;
|
|
889
889
|
}
|
|
890
|
-
if (!User.valid_email(email)) {
|
|
890
|
+
if (email && !User.valid_email(email)) {
|
|
891
891
|
req.flash("danger", req.__("Not a valid e-mail address"));
|
|
892
892
|
res.redirect("/auth/signup");
|
|
893
893
|
return true;
|
|
@@ -905,9 +905,7 @@ router.post(
|
|
|
905
905
|
res.redirect("/auth/signup");
|
|
906
906
|
return true;
|
|
907
907
|
}
|
|
908
|
-
|
|
909
|
-
const us = await User.find({ email });
|
|
910
|
-
if (us.length > 0) {
|
|
908
|
+
if (await User.matches_existing_user(urecord)) {
|
|
911
909
|
req.flash("danger", req.__("Account already exists"));
|
|
912
910
|
res.redirect("/auth/signup");
|
|
913
911
|
return true;
|
package/help/Event types.tmd
CHANGED
|
@@ -16,6 +16,13 @@ outbound emails would have a trigger with When = Insert and Action = send_email
|
|
|
16
16
|
**Update**: run this action when changes are made to an existing row. The old row can
|
|
17
17
|
be accessed with the `old_row` variable.
|
|
18
18
|
|
|
19
|
+
**Validate**: run before inserts or updates. If the action returns `error` (for example,
|
|
20
|
+
`run_js_code` code: `return {error: "Invalid row"}`), the insert/update is aborted. If the
|
|
21
|
+
trigger returns `set_fields` (for example, `run_js_code` code: `return {set_fields: {full_name: "Unknown"}}`)
|
|
22
|
+
these values are inserted in row.
|
|
23
|
+
|
|
24
|
+
Guaranteed to run before any Insert or Update triggers
|
|
25
|
+
|
|
19
26
|
**Delete**: run this action when a row is deleted
|
|
20
27
|
|
|
21
28
|
## Periodic events
|
|
@@ -87,20 +87,7 @@ Example: `return { error: "Invalid command!" }`
|
|
|
87
87
|
|
|
88
88
|
If this is triggered by an Edit view with the SubmitWithAjax,
|
|
89
89
|
halt navigation and stay on page. This can be used for complex validation logic,
|
|
90
|
-
|
|
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
|
-
```
|
|
90
|
+
when added as a Validate trigger.
|
|
104
91
|
|
|
105
92
|
#### `goto`
|
|
106
93
|
|
package/locales/en.json
CHANGED
|
@@ -1277,5 +1277,6 @@
|
|
|
1277
1277
|
"HTML file": "HTML file",
|
|
1278
1278
|
"HTML file to use as page content": "HTML file to use as page content",
|
|
1279
1279
|
"Offline mode: cannot load file": "Offline mode: cannot load file",
|
|
1280
|
-
"None - use drag and drop builder": "None - use drag and drop builder"
|
|
1280
|
+
"None - use drag and drop builder": "None - use drag and drop builder",
|
|
1281
|
+
"Do not pick or compare time": "Do not pick or compare time"
|
|
1281
1282
|
}
|
package/package.json
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saltcorn/server",
|
|
3
|
-
"version": "0.9.1-beta.
|
|
3
|
+
"version": "0.9.1-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
9
|
"@aws-sdk/client-s3": "^3.451.0",
|
|
10
|
-
"@saltcorn/base-plugin": "0.9.1-beta.
|
|
11
|
-
"@saltcorn/builder": "0.9.1-beta.
|
|
12
|
-
"@saltcorn/data": "0.9.1-beta.
|
|
13
|
-
"@saltcorn/admin-models": "0.9.1-beta.
|
|
14
|
-
"@saltcorn/filemanager": "0.9.1-beta.
|
|
15
|
-
"@saltcorn/markup": "0.9.1-beta.
|
|
16
|
-
"@saltcorn/sbadmin2": "0.9.1-beta.
|
|
10
|
+
"@saltcorn/base-plugin": "0.9.1-beta.8",
|
|
11
|
+
"@saltcorn/builder": "0.9.1-beta.8",
|
|
12
|
+
"@saltcorn/data": "0.9.1-beta.8",
|
|
13
|
+
"@saltcorn/admin-models": "0.9.1-beta.8",
|
|
14
|
+
"@saltcorn/filemanager": "0.9.1-beta.8",
|
|
15
|
+
"@saltcorn/markup": "0.9.1-beta.8",
|
|
16
|
+
"@saltcorn/sbadmin2": "0.9.1-beta.8",
|
|
17
17
|
"@socket.io/cluster-adapter": "^0.2.1",
|
|
18
18
|
"@socket.io/sticky": "^1.0.1",
|
|
19
19
|
"adm-zip": "0.5.10",
|
|
@@ -50,6 +50,11 @@ const nubBy = (prop, xs) => {
|
|
|
50
50
|
return true;
|
|
51
51
|
});
|
|
52
52
|
};
|
|
53
|
+
|
|
54
|
+
function valid_js_var_name(s) {
|
|
55
|
+
if (!s) return false;
|
|
56
|
+
return !!s.match(/^[a-zA-Z_$][a-zA-Z_$0-9]*$/);
|
|
57
|
+
}
|
|
53
58
|
function apply_showif() {
|
|
54
59
|
const isNode = typeof parent?.saltcorn?.data?.state === "undefined";
|
|
55
60
|
$("[data-show-if]").each(function (ix, element) {
|
|
@@ -83,7 +88,7 @@ function apply_showif() {
|
|
|
83
88
|
const e = $(element);
|
|
84
89
|
const rec = get_form_record(e);
|
|
85
90
|
const href = new Function(
|
|
86
|
-
`{${Object.keys(rec).join(",")}}`,
|
|
91
|
+
`{${Object.keys(rec).filter(valid_js_var_name).join(",")}}`,
|
|
87
92
|
"return " + e.attr("data-dyn-href")
|
|
88
93
|
)(rec);
|
|
89
94
|
e.attr("href", href);
|
|
@@ -756,31 +761,37 @@ function initialize_page() {
|
|
|
756
761
|
$(el).addClass("codemirror-enabled");
|
|
757
762
|
cm.on(
|
|
758
763
|
"change",
|
|
759
|
-
$.debounce(
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
764
|
+
$.debounce(
|
|
765
|
+
(cm1) => {
|
|
766
|
+
cm1.save();
|
|
767
|
+
if ($(el).hasClass("validate-statements")) {
|
|
768
|
+
try {
|
|
769
|
+
let AsyncFunction = Object.getPrototypeOf(
|
|
770
|
+
async function () {}
|
|
771
|
+
).constructor;
|
|
772
|
+
AsyncFunction(cm.getValue());
|
|
773
|
+
$(el).closest("form").trigger("change");
|
|
774
|
+
} catch (e) {
|
|
775
|
+
const form = $(el).closest("form");
|
|
776
|
+
const errorArea = form.parent().find(".full-form-error");
|
|
777
|
+
if (errorArea.length) errorArea.text(e.message);
|
|
778
|
+
else
|
|
779
|
+
form
|
|
780
|
+
.parent()
|
|
781
|
+
.append(
|
|
782
|
+
`<p class="text-danger full-form-error">${e.message}</p>`
|
|
783
|
+
);
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
} else {
|
|
787
|
+
cm1.save();
|
|
766
788
|
$(el).closest("form").trigger("change");
|
|
767
|
-
} catch (e) {
|
|
768
|
-
const form = $(el).closest("form");
|
|
769
|
-
const errorArea = form.parent().find(".full-form-error");
|
|
770
|
-
if (errorArea.length) errorArea.text(e.message);
|
|
771
|
-
else
|
|
772
|
-
form
|
|
773
|
-
.parent()
|
|
774
|
-
.append(
|
|
775
|
-
`<p class="text-danger full-form-error">${e.message}</p>`
|
|
776
|
-
);
|
|
777
|
-
return;
|
|
778
789
|
}
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
790
|
+
},
|
|
791
|
+
500,
|
|
792
|
+
null,
|
|
793
|
+
true
|
|
794
|
+
)
|
|
784
795
|
);
|
|
785
796
|
});
|
|
786
797
|
}, 100);
|
package/public/saltcorn.js
CHANGED
|
@@ -360,7 +360,7 @@ function submitWithAjax(e) {
|
|
|
360
360
|
saveAndContinue(e, (res) => {
|
|
361
361
|
if (res && res.responseJSON && res.responseJSON.url_when_done)
|
|
362
362
|
window.location.href = res.responseJSON.url_when_done;
|
|
363
|
-
if (res && res.responseJSON && res.responseJSON.error)
|
|
363
|
+
if (res && res.responseJSON && res.responseJSON.error && res.status < 300)
|
|
364
364
|
notifyAlert({ type: "danger", text: res.responseJSON.error });
|
|
365
365
|
});
|
|
366
366
|
}
|
package/routes/actions.js
CHANGED
|
@@ -145,7 +145,7 @@ const triggerForm = async (req, trigger) => {
|
|
|
145
145
|
.filter(([k, v]) => v.hasChannel)
|
|
146
146
|
.map(([k, v]) => k);
|
|
147
147
|
const allActions = actions.map((t) => t.name);
|
|
148
|
-
const table_triggers = ["Insert", "Update", "Delete"];
|
|
148
|
+
const table_triggers = ["Insert", "Update", "Delete", "Validate"];
|
|
149
149
|
const action_options = {};
|
|
150
150
|
const actionsNotRequiringRow = actions
|
|
151
151
|
.filter((a) => !a.requireRow)
|
package/routes/page.js
CHANGED
|
@@ -16,6 +16,7 @@ const {
|
|
|
16
16
|
isAdmin,
|
|
17
17
|
sendHtmlFile,
|
|
18
18
|
} = require("../routes/utils.js");
|
|
19
|
+
const { isTest } = require("@saltcorn/data/utils");
|
|
19
20
|
const { add_edit_bar } = require("../markup/admin.js");
|
|
20
21
|
const { traverseSync } = require("@saltcorn/data/models/layout");
|
|
21
22
|
const { run_action_column } = require("@saltcorn/data/plugin-helper");
|
|
@@ -52,12 +53,13 @@ router.get(
|
|
|
52
53
|
const title = scan_for_page_title(contents, db_page.title);
|
|
53
54
|
const tock = new Date();
|
|
54
55
|
const ms = tock.getTime() - tic.getTime();
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
if (!isTest())
|
|
57
|
+
Trigger.emitEvent("PageLoad", null, req.user, {
|
|
58
|
+
text: req.__("Page '%s' was loaded", pagename),
|
|
59
|
+
type: "page",
|
|
60
|
+
name: pagename,
|
|
61
|
+
render_time: ms,
|
|
62
|
+
});
|
|
61
63
|
if (contents.html_file) await sendHtmlFile(req, res, contents.html_file);
|
|
62
64
|
else
|
|
63
65
|
res.sendWrap(
|
package/routes/view.js
CHANGED
|
@@ -18,7 +18,7 @@ const {
|
|
|
18
18
|
setTenant,
|
|
19
19
|
} = require("../routes/utils.js");
|
|
20
20
|
const { add_edit_bar } = require("../markup/admin.js");
|
|
21
|
-
const { InvalidConfiguration } = require("@saltcorn/data/utils");
|
|
21
|
+
const { InvalidConfiguration, isTest } = require("@saltcorn/data/utils");
|
|
22
22
|
const { getState } = require("@saltcorn/data/db/state");
|
|
23
23
|
|
|
24
24
|
/**
|
|
@@ -88,12 +88,13 @@ router.get(
|
|
|
88
88
|
res.set("SaltcornModalLinkOut", `true`);
|
|
89
89
|
const tock = new Date();
|
|
90
90
|
const ms = tock.getTime() - tic.getTime();
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
91
|
+
if (!isTest())
|
|
92
|
+
Trigger.emitEvent("PageLoad", null, req.user, {
|
|
93
|
+
text: req.__("View '%s' was loaded", viewname),
|
|
94
|
+
type: "view",
|
|
95
|
+
name: viewname,
|
|
96
|
+
render_time: ms,
|
|
97
|
+
});
|
|
97
98
|
if (typeof contents === "object" && contents.goto)
|
|
98
99
|
res.redirect(contents.goto);
|
|
99
100
|
else
|
package/tests/view.test.js
CHANGED
|
@@ -17,6 +17,9 @@ const View = require("@saltcorn/data/models/view");
|
|
|
17
17
|
const Table = require("@saltcorn/data/models/table");
|
|
18
18
|
|
|
19
19
|
const { plugin_with_routes } = require("@saltcorn/data/tests/mocks");
|
|
20
|
+
const {
|
|
21
|
+
prepareArtistsAlbumRelation,
|
|
22
|
+
} = require("@saltcorn/data/tests/common_helpers");
|
|
20
23
|
|
|
21
24
|
afterAll(db.close);
|
|
22
25
|
beforeAll(async () => {
|
|
@@ -586,3 +589,58 @@ describe("inbound relations", () => {
|
|
|
586
589
|
.expect(toNotInclude("Content of post CPost C"));
|
|
587
590
|
});
|
|
588
591
|
});
|
|
592
|
+
|
|
593
|
+
describe("many to many relations", () => {
|
|
594
|
+
beforeAll(async () => {
|
|
595
|
+
await prepareArtistsAlbumRelation();
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
it("artist_plays_on_album", async () => {
|
|
599
|
+
const app = await getApp({ disableCsrf: true });
|
|
600
|
+
const loginCookie = await getAdminLoginCookie();
|
|
601
|
+
await request(app)
|
|
602
|
+
.get("/view/show_artist?id=1")
|
|
603
|
+
.set("Cookie", loginCookie)
|
|
604
|
+
.expect(toInclude("album A"))
|
|
605
|
+
.expect(toInclude("album B"));
|
|
606
|
+
|
|
607
|
+
await request(app)
|
|
608
|
+
.get("/view/show_artist?id=2")
|
|
609
|
+
.set("Cookie", loginCookie)
|
|
610
|
+
.expect(toInclude("album A"))
|
|
611
|
+
.expect(toNotInclude("album B"));
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
it("albums feed with query", async () => {
|
|
615
|
+
const app = await getApp({ disableCsrf: true });
|
|
616
|
+
const loginCookie = await getAdminLoginCookie();
|
|
617
|
+
|
|
618
|
+
const queryObj_1 = {
|
|
619
|
+
relation: ".artists.artist_plays_on_album$artist.album",
|
|
620
|
+
srcId: 1,
|
|
621
|
+
};
|
|
622
|
+
await request(app)
|
|
623
|
+
.get(
|
|
624
|
+
`/view/albums_feed?_inbound_relation_path_=${encodeURIComponent(
|
|
625
|
+
JSON.stringify(queryObj_1)
|
|
626
|
+
)}`
|
|
627
|
+
)
|
|
628
|
+
.set("Cookie", loginCookie)
|
|
629
|
+
.expect(toInclude("album A"))
|
|
630
|
+
.expect(toInclude("album B"));
|
|
631
|
+
|
|
632
|
+
const queryObj_2 = {
|
|
633
|
+
relation: ".artists.artist_plays_on_album$artist.album",
|
|
634
|
+
srcId: 2,
|
|
635
|
+
};
|
|
636
|
+
await request(app)
|
|
637
|
+
.get(
|
|
638
|
+
`/view/albums_feed?_inbound_relation_path_=${encodeURIComponent(
|
|
639
|
+
JSON.stringify(queryObj_2)
|
|
640
|
+
)}`
|
|
641
|
+
)
|
|
642
|
+
.set("Cookie", loginCookie)
|
|
643
|
+
.expect(toInclude("album A"))
|
|
644
|
+
.expect(toNotInclude("album B"));
|
|
645
|
+
});
|
|
646
|
+
});
|