@saltcorn/server 0.9.0 → 0.9.1-beta.0
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 +2 -0
- package/auth/testhelp.js +13 -0
- package/help/API actions.tmd +12 -0
- package/locales/en.json +6 -1
- package/locales/fr.json +7 -1
- package/package.json +8 -8
- package/public/saltcorn-common.js +31 -7
- package/public/saltcorn.js +14 -0
- package/routes/api.js +2 -2
- package/routes/common_lists.js +11 -5
- package/routes/homepage.js +10 -4
- package/routes/pageedit.js +1 -1
- package/tests/api.test.js +67 -0
package/auth/admin.js
CHANGED
package/auth/testhelp.js
CHANGED
|
@@ -215,6 +215,18 @@ const succeedJsonWith = (pred) => (res) => {
|
|
|
215
215
|
}
|
|
216
216
|
};
|
|
217
217
|
|
|
218
|
+
const succeedJsonWithWholeBody = (pred) => (res) => {
|
|
219
|
+
if (res.statusCode !== 200) {
|
|
220
|
+
console.log(res.text);
|
|
221
|
+
throw new Error(`Expected status 200, received ${res.statusCode}`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (!pred(res.body)) {
|
|
225
|
+
console.log(res.body);
|
|
226
|
+
throw new Error(`Not satisfied`);
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
|
|
218
230
|
/**
|
|
219
231
|
*
|
|
220
232
|
* @param {number} code
|
|
@@ -260,6 +272,7 @@ module.exports = {
|
|
|
260
272
|
notAuthorized,
|
|
261
273
|
respondJsonWith,
|
|
262
274
|
toSucceedWithImage,
|
|
275
|
+
succeedJsonWithWholeBody,
|
|
263
276
|
resToLoginCookie,
|
|
264
277
|
itShouldIncludeTextForAdmin,
|
|
265
278
|
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
This trigger can be run with by making an HTTP request to
|
|
2
|
+
|
|
3
|
+
{{scState.getConfig("base_url","").replace(/\/$/, "")}}/api/action/{{ query.name }}
|
|
4
|
+
|
|
5
|
+
If you make a POST request, the POST body is expected to be JSON and its value
|
|
6
|
+
is accessible using the `row` variable (e.g. in a `run_js_code`)
|
|
7
|
+
|
|
8
|
+
If you make a GET request, the query string values
|
|
9
|
+
are accessible as a JSON object using the `row` variable (e.g. in a `run_js_code`).
|
|
10
|
+
|
|
11
|
+
If you return a value from the action, it will be available in the `data`
|
|
12
|
+
subfield of the JSON body in the HTTP response
|
package/locales/en.json
CHANGED
|
@@ -1268,5 +1268,10 @@
|
|
|
1268
1268
|
"Pack file": "Pack file",
|
|
1269
1269
|
"Upload a pack file": "Upload a pack file",
|
|
1270
1270
|
"No menu": "No menu",
|
|
1271
|
-
"Omit the menu from this page": "Omit the menu from this page"
|
|
1271
|
+
"Omit the menu from this page": "Omit the menu from this page",
|
|
1272
|
+
"%s finished without a result": "%s finished without a result",
|
|
1273
|
+
"Body size limit (Kb)": "Body size limit (Kb)",
|
|
1274
|
+
"Maximum request body size in kilobytes": "Maximum request body size in kilobytes",
|
|
1275
|
+
"URL encoded size limit (Kb)": "URL encoded size limit (Kb)",
|
|
1276
|
+
"Maximum URL encoded request size in kilobytes": "Maximum URL encoded request size in kilobytes"
|
|
1272
1277
|
}
|
package/locales/fr.json
CHANGED
|
@@ -286,5 +286,11 @@
|
|
|
286
286
|
"Users and security": "Users and security",
|
|
287
287
|
"Site structure": "Site structure",
|
|
288
288
|
"Events": "Events",
|
|
289
|
-
"Are you sure?": "Are you sure?"
|
|
289
|
+
"Are you sure?": "Are you sure?",
|
|
290
|
+
"API token": "API token",
|
|
291
|
+
"No API token issued": "No API token issued",
|
|
292
|
+
"Generate": "Generate",
|
|
293
|
+
"Two-factor authentication": "Two-factor authentication",
|
|
294
|
+
"Two-factor authentication is disabled": "Two-factor authentication is disabled",
|
|
295
|
+
"Enable 2FA": "Enable 2FA"
|
|
290
296
|
}
|
package/package.json
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saltcorn/server",
|
|
3
|
-
"version": "0.9.0",
|
|
3
|
+
"version": "0.9.1-beta.0",
|
|
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",
|
|
10
|
-
"@saltcorn/builder": "0.9.0",
|
|
11
|
-
"@saltcorn/data": "0.9.0",
|
|
12
|
-
"@saltcorn/admin-models": "0.9.0",
|
|
13
|
-
"@saltcorn/filemanager": "0.9.0",
|
|
14
|
-
"@saltcorn/markup": "0.9.0",
|
|
15
|
-
"@saltcorn/sbadmin2": "0.9.0",
|
|
9
|
+
"@saltcorn/base-plugin": "0.9.1-beta.0",
|
|
10
|
+
"@saltcorn/builder": "0.9.1-beta.0",
|
|
11
|
+
"@saltcorn/data": "0.9.1-beta.0",
|
|
12
|
+
"@saltcorn/admin-models": "0.9.1-beta.0",
|
|
13
|
+
"@saltcorn/filemanager": "0.9.1-beta.0",
|
|
14
|
+
"@saltcorn/markup": "0.9.1-beta.0",
|
|
15
|
+
"@saltcorn/sbadmin2": "0.9.1-beta.0",
|
|
16
16
|
"@socket.io/cluster-adapter": "^0.2.1",
|
|
17
17
|
"@socket.io/sticky": "^1.0.1",
|
|
18
18
|
"adm-zip": "0.5.10",
|
|
@@ -154,7 +154,8 @@ function apply_showif() {
|
|
|
154
154
|
e.empty();
|
|
155
155
|
e.prop("data-fetch-options-current-set", qs);
|
|
156
156
|
const toAppend = [];
|
|
157
|
-
if (!dynwhere.required)
|
|
157
|
+
if (!dynwhere.required)
|
|
158
|
+
toAppend.push({ label: dynwhere.neutral_label || "" });
|
|
158
159
|
let currentDataOption = undefined;
|
|
159
160
|
const dataOptions = [];
|
|
160
161
|
//console.log(success);
|
|
@@ -173,12 +174,28 @@ function apply_showif() {
|
|
|
173
174
|
const selected = `${current}` === `${r[dynwhere.refname]}`;
|
|
174
175
|
dataOptions.push({ text: label, value });
|
|
175
176
|
if (selected) currentDataOption = value;
|
|
176
|
-
|
|
177
|
-
selected ? "selected" : ""
|
|
178
|
-
} value="${value}">${label}</option>`;
|
|
179
|
-
toAppend.push(html);
|
|
177
|
+
toAppend.push({ selected, value, label });
|
|
180
178
|
});
|
|
181
|
-
|
|
179
|
+
toAppend.sort((a, b) =>
|
|
180
|
+
a.label === dynwhere.neutral_label
|
|
181
|
+
? -1
|
|
182
|
+
: b.label === dynwhere.neutral_label
|
|
183
|
+
? 1
|
|
184
|
+
: (a.label?.toLowerCase?.() || a.label) >
|
|
185
|
+
(b.label?.toLowerCase?.() || b.label)
|
|
186
|
+
? 1
|
|
187
|
+
: -1
|
|
188
|
+
);
|
|
189
|
+
e.html(
|
|
190
|
+
toAppend
|
|
191
|
+
.map(
|
|
192
|
+
({ label, value, selected }) =>
|
|
193
|
+
`<option${selected ? ` selected` : ""}${
|
|
194
|
+
value ? ` value="${value}"` : ""
|
|
195
|
+
}>${label || ""}</option>`
|
|
196
|
+
)
|
|
197
|
+
.join("")
|
|
198
|
+
);
|
|
182
199
|
|
|
183
200
|
//TODO: also sort inserted HTML options
|
|
184
201
|
dataOptions.sort((a, b) =>
|
|
@@ -1022,7 +1039,14 @@ function common_done(res, viewname, isWeb = true) {
|
|
|
1022
1039
|
if (res.notify) handle(res.notify, notifyAlert);
|
|
1023
1040
|
if (res.error)
|
|
1024
1041
|
handle(res.error, (text) => notifyAlert({ type: "danger", text: text }));
|
|
1025
|
-
|
|
1042
|
+
|
|
1043
|
+
if (res.eval_js && res.row && res.field_names) {
|
|
1044
|
+
const f = new Function(`row, {${res.field_names}}`, res.eval_js);
|
|
1045
|
+
const evalres = f(res.row, res.row);
|
|
1046
|
+
if (evalres) common_done(evalres, viewname, isWeb);
|
|
1047
|
+
} else if (res.eval_js) {
|
|
1048
|
+
handle(res.eval_js, eval);
|
|
1049
|
+
}
|
|
1026
1050
|
|
|
1027
1051
|
if (res.reload_page) {
|
|
1028
1052
|
(isWeb ? location : parent).reload(); //TODO notify to cookie if reload or goto
|
package/public/saltcorn.js
CHANGED
|
@@ -572,6 +572,20 @@ function ajax_post_btn(e, reload_on_done, reload_delay) {
|
|
|
572
572
|
|
|
573
573
|
return false;
|
|
574
574
|
}
|
|
575
|
+
|
|
576
|
+
function api_action_call(name, body) {
|
|
577
|
+
$.ajax(`/api/action/${name}`, {
|
|
578
|
+
type: "POST",
|
|
579
|
+
headers: {
|
|
580
|
+
"CSRF-Token": _sc_globalCsrf,
|
|
581
|
+
},
|
|
582
|
+
data: body,
|
|
583
|
+
success: function (res) {
|
|
584
|
+
common_done(res.data);
|
|
585
|
+
},
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
|
|
575
589
|
function make_unique_field(
|
|
576
590
|
id,
|
|
577
591
|
table_id,
|
package/routes/api.js
CHANGED
|
@@ -332,7 +332,7 @@ router.get(
|
|
|
332
332
|
* @function
|
|
333
333
|
* @memberof module:routes/api~apiRouter
|
|
334
334
|
*/
|
|
335
|
-
router.
|
|
335
|
+
router.all(
|
|
336
336
|
"/action/:actionname/",
|
|
337
337
|
error_catcher(async (req, res, next) => {
|
|
338
338
|
const { actionname } = req.params;
|
|
@@ -361,7 +361,7 @@ router.post(
|
|
|
361
361
|
const resp = await action.run({
|
|
362
362
|
configuration: trigger.configuration,
|
|
363
363
|
body: req.body,
|
|
364
|
-
row: req.body,
|
|
364
|
+
row: req.method === "GET" ? req.query : req.body,
|
|
365
365
|
req,
|
|
366
366
|
user: user || req.user,
|
|
367
367
|
});
|
package/routes/common_lists.js
CHANGED
|
@@ -9,7 +9,7 @@ const {
|
|
|
9
9
|
post_dropdown_item,
|
|
10
10
|
} = require("@saltcorn/markup");
|
|
11
11
|
const { get_base_url } = require("./utils.js");
|
|
12
|
-
const { h4, p, div, a, input, text } = require("@saltcorn/markup/tags");
|
|
12
|
+
const { h4, p, div, a, i, input, text } = require("@saltcorn/markup/tags");
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* @param {string} col
|
|
@@ -385,10 +385,16 @@ const getTriggerList = (triggers, req, { tagId, domId, showList } = {}) => {
|
|
|
385
385
|
},
|
|
386
386
|
{
|
|
387
387
|
label: req.__("When"),
|
|
388
|
-
key: (
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
388
|
+
key: (act) =>
|
|
389
|
+
act.when_trigger +
|
|
390
|
+
(act.when_trigger === "API call"
|
|
391
|
+
? a(
|
|
392
|
+
{
|
|
393
|
+
href: `javascript:ajax_modal('/admin/help/API%20actions?name=${act.name}')`,
|
|
394
|
+
},
|
|
395
|
+
i({ class: "fas fa-question-circle ms-1" })
|
|
396
|
+
)
|
|
397
|
+
: ""),
|
|
392
398
|
},
|
|
393
399
|
{
|
|
394
400
|
label: req.__("Test run"),
|
package/routes/homepage.js
CHANGED
|
@@ -262,10 +262,16 @@ const actionsTab = async (req, triggers) => {
|
|
|
262
262
|
},
|
|
263
263
|
{
|
|
264
264
|
label: req.__("When"),
|
|
265
|
-
key: (
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
265
|
+
key: (act) =>
|
|
266
|
+
act.when_trigger +
|
|
267
|
+
(act.when_trigger === "API call"
|
|
268
|
+
? a(
|
|
269
|
+
{
|
|
270
|
+
href: `javascript:ajax_modal('/admin/help/API%20actions?name=${act.name}')`,
|
|
271
|
+
},
|
|
272
|
+
i({ class: "fas fa-question-circle ms-1" })
|
|
273
|
+
)
|
|
274
|
+
: ""),
|
|
269
275
|
},
|
|
270
276
|
],
|
|
271
277
|
triggers
|
package/routes/pageedit.js
CHANGED
|
@@ -289,7 +289,7 @@ const wrap = (contents, noCard, req, page) => ({
|
|
|
289
289
|
crumbs: [
|
|
290
290
|
{ text: req.__("Pages"), href: "/pageedit" },
|
|
291
291
|
page
|
|
292
|
-
? { href: `/
|
|
292
|
+
? { href: `/page/${page.name}`, text: page.name }
|
|
293
293
|
: { text: req.__("New") },
|
|
294
294
|
],
|
|
295
295
|
},
|
package/tests/api.test.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
const request = require("supertest");
|
|
2
2
|
const getApp = require("../app");
|
|
3
3
|
const Table = require("@saltcorn/data/models/table");
|
|
4
|
+
const Trigger = require("@saltcorn/data/models/trigger");
|
|
5
|
+
|
|
6
|
+
const Field = require("@saltcorn/data/models/field");
|
|
4
7
|
const {
|
|
5
8
|
getStaffLoginCookie,
|
|
6
9
|
getAdminLoginCookie,
|
|
@@ -9,6 +12,7 @@ const {
|
|
|
9
12
|
succeedJsonWith,
|
|
10
13
|
notAuthorized,
|
|
11
14
|
toRedirect,
|
|
15
|
+
succeedJsonWithWholeBody,
|
|
12
16
|
} = require("../auth/testhelp");
|
|
13
17
|
const db = require("@saltcorn/data/db");
|
|
14
18
|
const User = require("@saltcorn/data/models/user");
|
|
@@ -322,3 +326,66 @@ describe("API authentication", () => {
|
|
|
322
326
|
.expect(succeedJsonWith((rows) => rows.length == 2));
|
|
323
327
|
});
|
|
324
328
|
});
|
|
329
|
+
|
|
330
|
+
describe("API action", () => {
|
|
331
|
+
it("should set up trigger", async () => {
|
|
332
|
+
const table = await Table.create("triggercounter");
|
|
333
|
+
await Field.create({
|
|
334
|
+
table,
|
|
335
|
+
name: "thing",
|
|
336
|
+
label: "TheThing",
|
|
337
|
+
type: "String",
|
|
338
|
+
});
|
|
339
|
+
await Trigger.create({
|
|
340
|
+
action: "run_js_code",
|
|
341
|
+
when_trigger: "API call",
|
|
342
|
+
name: "mywebhook",
|
|
343
|
+
min_role: 100,
|
|
344
|
+
configuration: {
|
|
345
|
+
code: `
|
|
346
|
+
const table = Table.findOne({ name: "triggercounter" });
|
|
347
|
+
await table.insertRow({ thing: row?.thing || "no body" });
|
|
348
|
+
return {studio: 54}
|
|
349
|
+
`,
|
|
350
|
+
},
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
it("should POST to trigger", async () => {
|
|
354
|
+
const app = await getApp({ disableCsrf: true });
|
|
355
|
+
await request(app)
|
|
356
|
+
.post("/api/action/mywebhook")
|
|
357
|
+
.send({
|
|
358
|
+
thing: "inthebody",
|
|
359
|
+
})
|
|
360
|
+
.set("Content-Type", "application/json")
|
|
361
|
+
.set("Accept", "application/json")
|
|
362
|
+
.expect(succeedJsonWithWholeBody((resp) => resp?.data?.studio === 54));
|
|
363
|
+
const table = Table.findOne({ name: "triggercounter" });
|
|
364
|
+
const counts = await table.getRows({});
|
|
365
|
+
expect(counts.map((c) => c.thing)).toContain("inthebody");
|
|
366
|
+
expect(counts.map((c) => c.thing)).not.toContain("no body");
|
|
367
|
+
});
|
|
368
|
+
it("should GET with query to trigger", async () => {
|
|
369
|
+
const app = await getApp({ disableCsrf: true });
|
|
370
|
+
await request(app)
|
|
371
|
+
.get("/api/action/mywebhook?thing=inthequery")
|
|
372
|
+
.set("Content-Type", "application/json")
|
|
373
|
+
.set("Accept", "application/json")
|
|
374
|
+
.expect(succeedJsonWithWholeBody((resp) => resp?.data?.studio === 54));
|
|
375
|
+
const table = Table.findOne({ name: "triggercounter" });
|
|
376
|
+
const counts = await table.getRows({});
|
|
377
|
+
expect(counts.map((c) => c.thing)).toContain("inthequery");
|
|
378
|
+
expect(counts.map((c) => c.thing)).not.toContain("no body");
|
|
379
|
+
});
|
|
380
|
+
it("should GET to trigger", async () => {
|
|
381
|
+
const app = await getApp({ disableCsrf: true });
|
|
382
|
+
await request(app)
|
|
383
|
+
.get("/api/action/mywebhook")
|
|
384
|
+
.set("Content-Type", "application/json")
|
|
385
|
+
.set("Accept", "application/json")
|
|
386
|
+
.expect(succeedJsonWithWholeBody((resp) => resp?.data?.studio === 54));
|
|
387
|
+
const table = Table.findOne({ name: "triggercounter" });
|
|
388
|
+
const counts = await table.getRows({});
|
|
389
|
+
expect(counts.map((c) => c.thing)).toContain("no body");
|
|
390
|
+
});
|
|
391
|
+
});
|