@saltcorn/server 0.9.4-beta.19 → 0.9.4-beta.20
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/package.json +8 -8
- package/public/saltcorn-common.js +7 -0
- package/public/saltcorn.js +17 -0
- package/routes/fields.js +18 -3
- package/routes/pageedit.js +3 -2
- package/routes/tables.js +27 -0
- package/serve.js +127 -7
- package/tests/table.test.js +1 -5
package/package.json
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saltcorn/server",
|
|
3
|
-
"version": "0.9.4-beta.
|
|
3
|
+
"version": "0.9.4-beta.20",
|
|
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.4-beta.
|
|
11
|
-
"@saltcorn/builder": "0.9.4-beta.
|
|
12
|
-
"@saltcorn/data": "0.9.4-beta.
|
|
13
|
-
"@saltcorn/admin-models": "0.9.4-beta.
|
|
14
|
-
"@saltcorn/filemanager": "0.9.4-beta.
|
|
15
|
-
"@saltcorn/markup": "0.9.4-beta.
|
|
16
|
-
"@saltcorn/sbadmin2": "0.9.4-beta.
|
|
10
|
+
"@saltcorn/base-plugin": "0.9.4-beta.20",
|
|
11
|
+
"@saltcorn/builder": "0.9.4-beta.20",
|
|
12
|
+
"@saltcorn/data": "0.9.4-beta.20",
|
|
13
|
+
"@saltcorn/admin-models": "0.9.4-beta.20",
|
|
14
|
+
"@saltcorn/filemanager": "0.9.4-beta.20",
|
|
15
|
+
"@saltcorn/markup": "0.9.4-beta.20",
|
|
16
|
+
"@saltcorn/sbadmin2": "0.9.4-beta.20",
|
|
17
17
|
"@socket.io/cluster-adapter": "^0.2.1",
|
|
18
18
|
"@socket.io/sticky": "^1.0.1",
|
|
19
19
|
"adm-zip": "0.5.10",
|
|
@@ -1189,6 +1189,13 @@ async function common_done(res, viewname, isWeb = true) {
|
|
|
1189
1189
|
const input = form.find(
|
|
1190
1190
|
`input[name=${k}], textarea[name=${k}], select[name=${k}]`
|
|
1191
1191
|
);
|
|
1192
|
+
if (k === "id" && input.length === 0) {
|
|
1193
|
+
//TODO table.pk_name instead of id
|
|
1194
|
+
form.append(
|
|
1195
|
+
`<input type="hidden" name="id" value="${res.set_fields[k]}">`
|
|
1196
|
+
);
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1192
1199
|
if (input.attr("type") === "checkbox")
|
|
1193
1200
|
input.prop("checked", res.set_fields[k]);
|
|
1194
1201
|
else input.val(res.set_fields[k]);
|
package/public/saltcorn.js
CHANGED
|
@@ -252,6 +252,21 @@ function ajax_done(res, viewname) {
|
|
|
252
252
|
common_done(res, viewname);
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
+
function spin_action_link(e) {
|
|
256
|
+
const $e = $(e);
|
|
257
|
+
const width = $e.width();
|
|
258
|
+
$e.attr("data-innerhtml-prespin", $e.html());
|
|
259
|
+
$e.html('<i class="fas fa-spinner fa-spin"></i>').width(width);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function reset_spinners() {
|
|
263
|
+
$("[data-innerhtml-prespin]").each(function () {
|
|
264
|
+
$e = $(this);
|
|
265
|
+
$e.html($e.attr("data-innerhtml-prespin"));
|
|
266
|
+
$e.removeAttr("data-innerhtml-prespin");
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
255
270
|
function view_post(viewname, route, data, onDone, sendState) {
|
|
256
271
|
const query = sendState
|
|
257
272
|
? `?${new URL(get_current_state_url()).searchParams.toString()}`
|
|
@@ -271,9 +286,11 @@ function view_post(viewname, route, data, onDone, sendState) {
|
|
|
271
286
|
.done(function (res) {
|
|
272
287
|
if (onDone) onDone(res);
|
|
273
288
|
ajax_done(res, viewname);
|
|
289
|
+
reset_spinners();
|
|
274
290
|
})
|
|
275
291
|
.fail(function (res) {
|
|
276
292
|
notifyAlert({ type: "danger", text: res.responseText });
|
|
293
|
+
reset_spinners();
|
|
277
294
|
});
|
|
278
295
|
}
|
|
279
296
|
let logged_errors = [];
|
package/routes/fields.js
CHANGED
|
@@ -1180,9 +1180,24 @@ router.post(
|
|
|
1180
1180
|
type,
|
|
1181
1181
|
join_field,
|
|
1182
1182
|
join_fieldview,
|
|
1183
|
+
agg_outcome_type,
|
|
1184
|
+
agg_fieldview,
|
|
1185
|
+
agg_field,
|
|
1183
1186
|
_columndef,
|
|
1184
1187
|
} = req.body;
|
|
1185
1188
|
const table = Table.findOne({ name: tableName });
|
|
1189
|
+
if (agg_outcome_type && agg_fieldview) {
|
|
1190
|
+
const type = getState().types[agg_outcome_type];
|
|
1191
|
+
const fv = type?.fieldviews?.[agg_fieldview];
|
|
1192
|
+
if (!fv?.configFields) {
|
|
1193
|
+
res.send(req.query?.accept == "json" ? "[]" : "");
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
const field = table.getField(agg_field);
|
|
1197
|
+
const cfgfields = await applyAsync(fv.configFields, field || { table });
|
|
1198
|
+
res.json(cfgfields);
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1186
1201
|
if (typeof type !== "string") {
|
|
1187
1202
|
try {
|
|
1188
1203
|
type = JSON.parse(_columndef).type;
|
|
@@ -1193,19 +1208,19 @@ router.post(
|
|
|
1193
1208
|
const fieldName = type == "Field" ? field_name : join_field;
|
|
1194
1209
|
const fv_name = type == "Field" ? fieldview : join_fieldview;
|
|
1195
1210
|
if (!fieldName) {
|
|
1196
|
-
res.send("");
|
|
1211
|
+
res.send(req.query?.accept == "json" ? "[]" : "");
|
|
1197
1212
|
return;
|
|
1198
1213
|
}
|
|
1199
1214
|
|
|
1200
1215
|
const field = table.getField(fieldName);
|
|
1201
1216
|
if (!field) {
|
|
1202
|
-
res.send("");
|
|
1217
|
+
res.send(req.query?.accept == "json" ? "[]" : "");
|
|
1203
1218
|
return;
|
|
1204
1219
|
}
|
|
1205
1220
|
const fieldViewConfigForms = await calcfldViewConfig([field], false, 0);
|
|
1206
1221
|
const formFields = fieldViewConfigForms[field.name][fv_name];
|
|
1207
1222
|
if (!formFields) {
|
|
1208
|
-
res.send("");
|
|
1223
|
+
res.send(req.query?.accept == "json" ? "[]" : "");
|
|
1209
1224
|
return;
|
|
1210
1225
|
}
|
|
1211
1226
|
formFields.forEach((ff) => {
|
package/routes/pageedit.js
CHANGED
|
@@ -186,7 +186,7 @@ const pageBuilderData = async (req, context) => {
|
|
|
186
186
|
for (const view of views) {
|
|
187
187
|
fixed_state_fields[view.name] = [];
|
|
188
188
|
const table = Table.findOne(view.table_id || view.exttable_name);
|
|
189
|
-
|
|
189
|
+
if (table) view.table_name = table.name;
|
|
190
190
|
const fs = await view.get_state_fields();
|
|
191
191
|
for (const frec of fs) {
|
|
192
192
|
const f = new Field(frec);
|
|
@@ -219,9 +219,10 @@ const pageBuilderData = async (req, context) => {
|
|
|
219
219
|
}
|
|
220
220
|
}
|
|
221
221
|
}
|
|
222
|
+
|
|
222
223
|
//console.log(fixed_state_fields.ListTasks);
|
|
223
224
|
return {
|
|
224
|
-
views,
|
|
225
|
+
views: views.map((v) => v.select_option),
|
|
225
226
|
images,
|
|
226
227
|
pages,
|
|
227
228
|
page_groups,
|
package/routes/tables.js
CHANGED
|
@@ -832,6 +832,7 @@ router.get(
|
|
|
832
832
|
}
|
|
833
833
|
viewCard = {
|
|
834
834
|
type: "card",
|
|
835
|
+
id: "table-views",
|
|
835
836
|
title: req.__("Views of this table"),
|
|
836
837
|
contents:
|
|
837
838
|
viewCardContents +
|
|
@@ -1173,6 +1174,32 @@ router.post(
|
|
|
1173
1174
|
res.redirect(`/table`);
|
|
1174
1175
|
return;
|
|
1175
1176
|
}
|
|
1177
|
+
const views = await View.find(
|
|
1178
|
+
t.id ? { table_id: t.id } : { exttable_name: t.name }
|
|
1179
|
+
);
|
|
1180
|
+
if (views.length) {
|
|
1181
|
+
req.flash(
|
|
1182
|
+
"error",
|
|
1183
|
+
`${text(t.name)} has views. Delete these first: <a href="/table/${
|
|
1184
|
+
t.name
|
|
1185
|
+
}#table-views">Views for ${text(t.name)}</a>`
|
|
1186
|
+
);
|
|
1187
|
+
res.redirect(`/table`);
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
if (t.id) {
|
|
1191
|
+
const triggers = await Trigger.find({ table_id: t.id });
|
|
1192
|
+
if (triggers.length) {
|
|
1193
|
+
req.flash(
|
|
1194
|
+
"error",
|
|
1195
|
+
`${text(
|
|
1196
|
+
t.name
|
|
1197
|
+
)} has triggers. Delete these first: <a href="/actions">Trigger list</a>`
|
|
1198
|
+
);
|
|
1199
|
+
res.redirect(`/table`);
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1176
1203
|
try {
|
|
1177
1204
|
await t.delete();
|
|
1178
1205
|
req.flash("success", req.__(`Table %s deleted`, t.name));
|
package/serve.js
CHANGED
|
@@ -394,21 +394,29 @@ const setupSocket = (subdomainOffset, ...servers) => {
|
|
|
394
394
|
io.attach(server);
|
|
395
395
|
}
|
|
396
396
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
397
|
+
const passportInit = passport.initialize();
|
|
398
|
+
const sessionStore = getSessionStore();
|
|
399
|
+
const setupNamespace = (namespace) => {
|
|
400
|
+
//io.of(namespace).use(wrap(setTenant));
|
|
401
|
+
io.of(namespace).use(wrap(sessionStore));
|
|
402
|
+
io.of(namespace).use(wrap(passportInit));
|
|
403
|
+
io.of(namespace).use(wrap(passport.authenticate(["jwt", "session"])));
|
|
404
|
+
};
|
|
405
|
+
setupNamespace("/");
|
|
406
|
+
setupNamespace("/datastream");
|
|
401
407
|
if (process.send && !cluster.isMaster) io.adapter(createAdapter());
|
|
402
408
|
getState().setRoomEmitter((tenant, viewname, room_id, msg) => {
|
|
403
|
-
io.to(`${tenant}_${viewname}_${room_id}`).emit("message", msg);
|
|
409
|
+
io.of("/").to(`${tenant}_${viewname}_${room_id}`).emit("message", msg);
|
|
404
410
|
});
|
|
405
411
|
|
|
406
412
|
getState().setLogEmitter((tenant, level, msg) => {
|
|
407
413
|
const time = new Date().valueOf();
|
|
408
|
-
io.
|
|
414
|
+
io.of("/")
|
|
415
|
+
.to(`_logs_${tenant}_`)
|
|
416
|
+
.emit("log_msg", { text: msg, time, level });
|
|
409
417
|
});
|
|
410
418
|
|
|
411
|
-
io.on("connection", (socket) => {
|
|
419
|
+
io.of("/").on("connection", (socket) => {
|
|
412
420
|
socket.on("join_room", ([viewname, room_id]) => {
|
|
413
421
|
const ten = get_tenant_from_req(socket.request) || "public";
|
|
414
422
|
const f = () => {
|
|
@@ -466,4 +474,116 @@ const setupSocket = (subdomainOffset, ...servers) => {
|
|
|
466
474
|
else f();
|
|
467
475
|
});
|
|
468
476
|
});
|
|
477
|
+
|
|
478
|
+
io.of("/datastream").on("connection", (socket) => {
|
|
479
|
+
let dataStream = null;
|
|
480
|
+
let dataTarget = null;
|
|
481
|
+
socket.on(
|
|
482
|
+
"open_data_stream",
|
|
483
|
+
async ([viewName, id, fieldName, fieldView, targetOpts], callback) => {
|
|
484
|
+
const tenant =
|
|
485
|
+
get_tenant_from_req(socket.request, subdomainOffset) || "public";
|
|
486
|
+
const f = async () => {
|
|
487
|
+
try {
|
|
488
|
+
const user = socket.request.user;
|
|
489
|
+
const view = View.findOne({ name: viewName });
|
|
490
|
+
if (view.viewtemplateObj.authorizeDataStream) {
|
|
491
|
+
const authorized = await view.viewtemplateObj.authorizeDataStream(
|
|
492
|
+
view,
|
|
493
|
+
id,
|
|
494
|
+
fieldName,
|
|
495
|
+
user,
|
|
496
|
+
targetOpts
|
|
497
|
+
);
|
|
498
|
+
if (!authorized) throw new Error("Not authorized");
|
|
499
|
+
}
|
|
500
|
+
const { stream, target } = await view.openDataStream(
|
|
501
|
+
id,
|
|
502
|
+
fieldName,
|
|
503
|
+
fieldView,
|
|
504
|
+
user,
|
|
505
|
+
targetOpts
|
|
506
|
+
);
|
|
507
|
+
dataStream = stream;
|
|
508
|
+
dataTarget = target;
|
|
509
|
+
getState().log(
|
|
510
|
+
5,
|
|
511
|
+
`opened data stram to: ${JSON.stringify(dataTarget)}`
|
|
512
|
+
);
|
|
513
|
+
callback({ status: "ok", target });
|
|
514
|
+
} catch (err) {
|
|
515
|
+
getState().log(
|
|
516
|
+
1,
|
|
517
|
+
`Socket open_data_stream: ${err.message || "unknown error"}`
|
|
518
|
+
);
|
|
519
|
+
callback({ status: "error", msg: err.message || "unknown error" });
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
if (tenant && tenant !== "public") db.runWithTenant(tenant, f);
|
|
523
|
+
else f();
|
|
524
|
+
}
|
|
525
|
+
);
|
|
526
|
+
socket.on("write_to_stream", async (data, callback) => {
|
|
527
|
+
if (!dataStream) {
|
|
528
|
+
getState().log(1, "Socket write_to_stream: No stream available");
|
|
529
|
+
callback({ status: "error", msg: "No stream available" });
|
|
530
|
+
} else
|
|
531
|
+
dataStream.write(data, (err) => {
|
|
532
|
+
if (err) {
|
|
533
|
+
getState().log(1, "Socket write_to_stream: No stream available");
|
|
534
|
+
callback({ status: "error", msg: err.message || "unknown error" });
|
|
535
|
+
} else callback({ status: "ok" });
|
|
536
|
+
});
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
socket.on("close_data_stream", async (callback) => {
|
|
540
|
+
if (!dataStream) {
|
|
541
|
+
getState().log(1, "Socket close_data_stream: No stream available");
|
|
542
|
+
callback({ status: "error", msg: "No stream available" });
|
|
543
|
+
} else {
|
|
544
|
+
dataStream.close((err) => {
|
|
545
|
+
if (err) {
|
|
546
|
+
getState().log(
|
|
547
|
+
1,
|
|
548
|
+
`Socket close_data_stream: ${err.message || "unknown error"}`
|
|
549
|
+
);
|
|
550
|
+
callback({ status: "error", msg: err.message || "unknown error" });
|
|
551
|
+
} else {
|
|
552
|
+
getState().log(
|
|
553
|
+
5,
|
|
554
|
+
`closed data stram of: ${JSON.stringify(dataTarget)}`
|
|
555
|
+
);
|
|
556
|
+
callback({ status: "ok" });
|
|
557
|
+
dataStream = null;
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
socket.on("disconnect", async () => {
|
|
564
|
+
const tenant =
|
|
565
|
+
get_tenant_from_req(socket.request, subdomainOffset) || "public";
|
|
566
|
+
const f = async () => {
|
|
567
|
+
if (dataStream)
|
|
568
|
+
dataStream.close((err) => {
|
|
569
|
+
if (err) {
|
|
570
|
+
getState().log(
|
|
571
|
+
1,
|
|
572
|
+
`Socket disconnect close_data_stream: ${
|
|
573
|
+
err.message || "unknown error"
|
|
574
|
+
}`
|
|
575
|
+
);
|
|
576
|
+
} else {
|
|
577
|
+
getState().log(
|
|
578
|
+
5,
|
|
579
|
+
`closed data stram of: ${JSON.stringify(dataTarget)}`
|
|
580
|
+
);
|
|
581
|
+
dataStream = null;
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
};
|
|
585
|
+
if (tenant && tenant !== "public") db.runWithTenant(tenant, f);
|
|
586
|
+
else f();
|
|
587
|
+
});
|
|
588
|
+
});
|
|
469
589
|
};
|
package/tests/table.test.js
CHANGED
|
@@ -254,11 +254,7 @@ Gordon Kane, 218`;
|
|
|
254
254
|
await request(app)
|
|
255
255
|
.get("/table/")
|
|
256
256
|
.set("Cookie", loginCookie)
|
|
257
|
-
.expect(
|
|
258
|
-
toInclude(
|
|
259
|
-
"cannot drop table books because other objects depend on it"
|
|
260
|
-
)
|
|
261
|
-
);
|
|
257
|
+
.expect(toInclude("has views. Delete these first"));
|
|
262
258
|
});
|
|
263
259
|
});
|
|
264
260
|
describe("deletion to table with row ownership", () => {
|