@saltcorn/server 1.0.0-beta.9 → 1.0.0-rc.10
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/auth/routes.js +4 -1
- 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 +4 -1
- package/load_plugins.js +90 -8
- package/locales/en.json +14 -1
- package/locales/pl.json +407 -85
- package/package.json +11 -11
- package/public/gridedit.js +3 -1
- package/public/saltcorn-builder.css +0 -7
- package/public/saltcorn-common.js +196 -101
- package/public/saltcorn.css +17 -0
- package/public/saltcorn.js +91 -17
- package/routes/actions.js +19 -25
- package/routes/admin.js +256 -54
- package/routes/api.js +36 -1
- package/routes/delete.js +4 -1
- package/routes/eventlog.js +2 -1
- package/routes/fields.js +21 -5
- package/routes/files.js +9 -2
- package/routes/infoarch.js +40 -2
- package/routes/list.js +1 -1
- package/routes/menu.js +3 -0
- package/routes/pageedit.js +9 -2
- package/routes/plugins.js +124 -28
- package/routes/scapi.js +19 -0
- package/routes/search.js +6 -1
- package/routes/sync.js +3 -5
- package/routes/tables.js +14 -4
- package/routes/viewedit.js +7 -4
- package/serve.js +17 -3
- package/tests/api.test.js +29 -0
- package/tests/fields.test.js +32 -0
- package/tests/plugin_install.test.js +235 -0
- package/tests/plugins.test.js +140 -0
package/routes/admin.js
CHANGED
|
@@ -309,13 +309,13 @@ router.get(
|
|
|
309
309
|
backupForm.values.auto_backup_expire_days = getState().getConfig(
|
|
310
310
|
"auto_backup_expire_days"
|
|
311
311
|
);
|
|
312
|
-
|
|
312
|
+
aBackupFilePrefixForm.values.backup_with_event_log = getState().getConfig(
|
|
313
313
|
"backup_with_event_log"
|
|
314
314
|
);
|
|
315
|
-
|
|
315
|
+
aBackupFilePrefixForm.values.backup_with_system_zip = getState().getConfig(
|
|
316
316
|
"backup_with_system_zip"
|
|
317
317
|
);
|
|
318
|
-
|
|
318
|
+
aBackupFilePrefixForm.values.backup_system_zip_level = getState().getConfig(
|
|
319
319
|
"backup_system_zip_level"
|
|
320
320
|
);
|
|
321
321
|
//
|
|
@@ -378,7 +378,9 @@ router.get(
|
|
|
378
378
|
: { type: "blank", contents: "" },
|
|
379
379
|
{
|
|
380
380
|
type: "card",
|
|
381
|
-
title:
|
|
381
|
+
title:
|
|
382
|
+
req.__("Snapshots") +
|
|
383
|
+
`<a href="javascript:ajax_modal('/admin/help/Snapshots?')"><i class="fas fa-question-circle ms-1"></i></a>`,
|
|
382
384
|
titleAjaxIndicator: true,
|
|
383
385
|
contents: div(
|
|
384
386
|
p(
|
|
@@ -673,7 +675,10 @@ router.get(
|
|
|
673
675
|
const backup_file_prefix = getState().getConfig("backup_file_prefix");
|
|
674
676
|
if (
|
|
675
677
|
!isRoot ||
|
|
676
|
-
!(
|
|
678
|
+
!(
|
|
679
|
+
path.resolve(filename).startsWith(backup_file_prefix) &&
|
|
680
|
+
filename.endsWith(".zip")
|
|
681
|
+
)
|
|
677
682
|
) {
|
|
678
683
|
res.redirect("/admin/backup");
|
|
679
684
|
return;
|
|
@@ -708,6 +713,33 @@ const backupFilePrefixForm = (req) =>
|
|
|
708
713
|
sublabel: req.__("Include table history in backup"),
|
|
709
714
|
default: true,
|
|
710
715
|
},
|
|
716
|
+
{
|
|
717
|
+
type: "Bool",
|
|
718
|
+
label: req.__("Include Event Logs"),
|
|
719
|
+
sublabel: req.__("Backup with event logs"),
|
|
720
|
+
name: "backup_with_event_log",
|
|
721
|
+
},
|
|
722
|
+
{
|
|
723
|
+
type: "Bool",
|
|
724
|
+
label: req.__("Use system zip"),
|
|
725
|
+
sublabel: req.__(
|
|
726
|
+
"Recommended. Executable <code>zip</code> must be installed"
|
|
727
|
+
),
|
|
728
|
+
name: "backup_with_system_zip",
|
|
729
|
+
},
|
|
730
|
+
{
|
|
731
|
+
type: "Integer",
|
|
732
|
+
label: req.__("Zip compression level"),
|
|
733
|
+
sublabel: req.__("1=Fast, larger file, 9=Slow, smaller files"),
|
|
734
|
+
name: "backup_system_zip_level",
|
|
735
|
+
attributes: {
|
|
736
|
+
min: 1,
|
|
737
|
+
max: 9,
|
|
738
|
+
},
|
|
739
|
+
showIf: {
|
|
740
|
+
backup_with_system_zip: true,
|
|
741
|
+
},
|
|
742
|
+
},
|
|
711
743
|
],
|
|
712
744
|
});
|
|
713
745
|
|
|
@@ -835,40 +867,6 @@ const autoBackupForm = (req) => {
|
|
|
835
867
|
},
|
|
836
868
|
]
|
|
837
869
|
: []),
|
|
838
|
-
{
|
|
839
|
-
type: "Bool",
|
|
840
|
-
label: req.__("Include Event Logs"),
|
|
841
|
-
sublabel: req.__("Backup with event logs"),
|
|
842
|
-
name: "backup_with_event_log",
|
|
843
|
-
showIf: {
|
|
844
|
-
auto_backup_frequency: ["Daily", "Weekly"],
|
|
845
|
-
},
|
|
846
|
-
},
|
|
847
|
-
{
|
|
848
|
-
type: "Bool",
|
|
849
|
-
label: req.__("Use system zip"),
|
|
850
|
-
sublabel: req.__(
|
|
851
|
-
"Recommended. Executable <code>zip</code> must be installed"
|
|
852
|
-
),
|
|
853
|
-
name: "backup_with_system_zip",
|
|
854
|
-
showIf: {
|
|
855
|
-
auto_backup_frequency: ["Daily", "Weekly"],
|
|
856
|
-
},
|
|
857
|
-
},
|
|
858
|
-
{
|
|
859
|
-
type: "Integer",
|
|
860
|
-
label: req.__("Zip compression level"),
|
|
861
|
-
sublabel: req.__("1=Fast, larger file, 9=Slow, smaller files"),
|
|
862
|
-
name: "backup_system_zip_level",
|
|
863
|
-
attributes: {
|
|
864
|
-
min: 1,
|
|
865
|
-
max: 9,
|
|
866
|
-
},
|
|
867
|
-
showIf: {
|
|
868
|
-
auto_backup_frequency: ["Daily", "Weekly"],
|
|
869
|
-
backup_with_system_zip: true,
|
|
870
|
-
},
|
|
871
|
-
},
|
|
872
870
|
],
|
|
873
871
|
});
|
|
874
872
|
};
|
|
@@ -1270,6 +1268,50 @@ const pullCordovaBuilder = (req, res) => {
|
|
|
1270
1268
|
});
|
|
1271
1269
|
};
|
|
1272
1270
|
|
|
1271
|
+
const tryInstallSdNotify = (req, res) => {
|
|
1272
|
+
const child = spawn("npm", ["install", "-g", "sd-notify"], {
|
|
1273
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1274
|
+
});
|
|
1275
|
+
return new Promise((resolve, reject) => {
|
|
1276
|
+
child.stdout.on("data", (data) => {
|
|
1277
|
+
res.write(data);
|
|
1278
|
+
});
|
|
1279
|
+
child.stderr?.on("data", (data) => {
|
|
1280
|
+
res.write(data);
|
|
1281
|
+
});
|
|
1282
|
+
child.on("exit", function (code, signal) {
|
|
1283
|
+
resolve(code);
|
|
1284
|
+
});
|
|
1285
|
+
child.on("error", (msg) => {
|
|
1286
|
+
const message = msg.message ? msg.message : msg.code;
|
|
1287
|
+
res.write(req.__("Error: ") + message + "\n");
|
|
1288
|
+
resolve(msg.code);
|
|
1289
|
+
});
|
|
1290
|
+
});
|
|
1291
|
+
};
|
|
1292
|
+
|
|
1293
|
+
const pruneDocker = (req, res) => {
|
|
1294
|
+
const child = spawn("docker", ["image", "prune", "-f"], {
|
|
1295
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1296
|
+
});
|
|
1297
|
+
return new Promise((resolve, reject) => {
|
|
1298
|
+
child.stdout.on("data", (data) => {
|
|
1299
|
+
res.write(data);
|
|
1300
|
+
});
|
|
1301
|
+
child.stderr?.on("data", (data) => {
|
|
1302
|
+
res.write(data);
|
|
1303
|
+
});
|
|
1304
|
+
child.on("exit", function (code, signal) {
|
|
1305
|
+
resolve(code);
|
|
1306
|
+
});
|
|
1307
|
+
child.on("error", (msg) => {
|
|
1308
|
+
const message = msg.message ? msg.message : msg.code;
|
|
1309
|
+
res.write(req.__("Error: ") + message + "\n");
|
|
1310
|
+
resolve(msg.code);
|
|
1311
|
+
});
|
|
1312
|
+
});
|
|
1313
|
+
};
|
|
1314
|
+
|
|
1273
1315
|
/*
|
|
1274
1316
|
* fetch available saltcorn versions and show a dialog to select one
|
|
1275
1317
|
*/
|
|
@@ -1285,6 +1327,7 @@ router.get(
|
|
|
1285
1327
|
throw new Error(req.__("Unable to fetch versions"));
|
|
1286
1328
|
const versions = Object.keys(pkgInfo.versions);
|
|
1287
1329
|
if (versions.length === 0) throw new Error(req.__("No versions found"));
|
|
1330
|
+
const tags = pkgInfo["dist-tags"] || {};
|
|
1288
1331
|
res.set("Page-Title", req.__("%s versions", "Saltcorn"));
|
|
1289
1332
|
let selected = packagejson.version;
|
|
1290
1333
|
res.send(
|
|
@@ -1294,6 +1337,7 @@ router.get(
|
|
|
1294
1337
|
method: "post",
|
|
1295
1338
|
},
|
|
1296
1339
|
input({ type: "hidden", name: "_csrf", value: req.csrfToken() }),
|
|
1340
|
+
// version select
|
|
1297
1341
|
div(
|
|
1298
1342
|
{ class: "form-group" },
|
|
1299
1343
|
label(
|
|
@@ -1319,6 +1363,54 @@ router.get(
|
|
|
1319
1363
|
)
|
|
1320
1364
|
)
|
|
1321
1365
|
),
|
|
1366
|
+
// tag select
|
|
1367
|
+
div(
|
|
1368
|
+
{ class: "form-group" },
|
|
1369
|
+
label(
|
|
1370
|
+
{
|
|
1371
|
+
for: "tag_select",
|
|
1372
|
+
class: "form-label fw-bold",
|
|
1373
|
+
},
|
|
1374
|
+
req.__("Tags")
|
|
1375
|
+
),
|
|
1376
|
+
select(
|
|
1377
|
+
{
|
|
1378
|
+
id: "tag_select",
|
|
1379
|
+
class: "form-control form-select",
|
|
1380
|
+
},
|
|
1381
|
+
option({
|
|
1382
|
+
id: "empty_opt",
|
|
1383
|
+
value: "",
|
|
1384
|
+
label: req.__("Select tag"),
|
|
1385
|
+
selected: true,
|
|
1386
|
+
}),
|
|
1387
|
+
Object.keys(tags).map((tag) =>
|
|
1388
|
+
option({
|
|
1389
|
+
id: `${tag}_opt`,
|
|
1390
|
+
value: tags[tag],
|
|
1391
|
+
label: `${tag} (${tags[tag]})`,
|
|
1392
|
+
})
|
|
1393
|
+
)
|
|
1394
|
+
)
|
|
1395
|
+
),
|
|
1396
|
+
// deep clean checkbox
|
|
1397
|
+
div(
|
|
1398
|
+
{ class: "form-group" },
|
|
1399
|
+
input({
|
|
1400
|
+
id: "deep_clean",
|
|
1401
|
+
class: "form-check-input",
|
|
1402
|
+
type: "checkbox",
|
|
1403
|
+
name: "deep_clean",
|
|
1404
|
+
checked: false,
|
|
1405
|
+
}),
|
|
1406
|
+
label(
|
|
1407
|
+
{
|
|
1408
|
+
for: "deep_clean",
|
|
1409
|
+
class: "form-label ms-2",
|
|
1410
|
+
},
|
|
1411
|
+
req.__("clean node_modules")
|
|
1412
|
+
)
|
|
1413
|
+
),
|
|
1322
1414
|
div(
|
|
1323
1415
|
{ class: "d-flex justify-content-end" },
|
|
1324
1416
|
button(
|
|
@@ -1338,7 +1430,18 @@ router.get(
|
|
|
1338
1430
|
req.__("Install")
|
|
1339
1431
|
)
|
|
1340
1432
|
)
|
|
1341
|
-
)
|
|
1433
|
+
) +
|
|
1434
|
+
script(
|
|
1435
|
+
domReady(`
|
|
1436
|
+
document.getElementById('tag_select').addEventListener('change', () => {
|
|
1437
|
+
const version = document.getElementById('tag_select').value;
|
|
1438
|
+
if (version) document.getElementById('version_select').value = version;
|
|
1439
|
+
});
|
|
1440
|
+
document.getElementById('version_select').addEventListener('change', () => {
|
|
1441
|
+
document.getElementById('tag_select').value = '';
|
|
1442
|
+
});
|
|
1443
|
+
`)
|
|
1444
|
+
)
|
|
1342
1445
|
);
|
|
1343
1446
|
} catch (error) {
|
|
1344
1447
|
getState().log(
|
|
@@ -1350,7 +1453,17 @@ router.get(
|
|
|
1350
1453
|
})
|
|
1351
1454
|
);
|
|
1352
1455
|
|
|
1353
|
-
const
|
|
1456
|
+
const cleanNodeModules = async () => {
|
|
1457
|
+
const topSaltcornDir = path.join(__dirname, "..", "..", "..", "..", "..");
|
|
1458
|
+
if (path.basename(topSaltcornDir) === "@saltcorn")
|
|
1459
|
+
await fs.promises.rm(topSaltcornDir, { recursive: true, force: true });
|
|
1460
|
+
else
|
|
1461
|
+
throw new Error(
|
|
1462
|
+
`'${topSaltcornDir}' is not a Saltcorn installation directory`
|
|
1463
|
+
);
|
|
1464
|
+
};
|
|
1465
|
+
|
|
1466
|
+
const doInstall = async (req, res, version, deepClean, runPull) => {
|
|
1354
1467
|
if (db.getTenantSchema() !== db.connectObj.default_schema) {
|
|
1355
1468
|
req.flash("error", req.__("Not possible for tenant"));
|
|
1356
1469
|
res.redirect("/admin");
|
|
@@ -1360,6 +1473,14 @@ const doInstall = async (req, res, version, runPull) => {
|
|
|
1360
1473
|
? req.__("Starting upgrade, please wait...\n")
|
|
1361
1474
|
: req.__("Installing %s, please wait...\n", version)
|
|
1362
1475
|
);
|
|
1476
|
+
if (deepClean) {
|
|
1477
|
+
res.write(req.__("Cleaning node_modules...\n"));
|
|
1478
|
+
try {
|
|
1479
|
+
await cleanNodeModules();
|
|
1480
|
+
} catch (e) {
|
|
1481
|
+
res.write(req.__("Error cleaning node_modules: %s\n", e.message));
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1363
1484
|
const child = spawn(
|
|
1364
1485
|
"npm",
|
|
1365
1486
|
["install", "-g", `@saltcorn/cli@${version}`, "--unsafe"],
|
|
@@ -1374,10 +1495,26 @@ const doInstall = async (req, res, version, runPull) => {
|
|
|
1374
1495
|
res.write(data);
|
|
1375
1496
|
});
|
|
1376
1497
|
child.on("exit", async function (code, signal) {
|
|
1377
|
-
if (code === 0
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1498
|
+
if (code === 0) {
|
|
1499
|
+
if (deepClean) {
|
|
1500
|
+
res.write(req.__("Installing sd-notify") + "\n");
|
|
1501
|
+
const sdNotifyCode = await tryInstallSdNotify(req, res);
|
|
1502
|
+
res.write(
|
|
1503
|
+
req.__("sd-notify install done with code %s", sdNotifyCode) + "\n"
|
|
1504
|
+
);
|
|
1505
|
+
}
|
|
1506
|
+
if (runPull) {
|
|
1507
|
+
res.write(
|
|
1508
|
+
req.__("Pulling the cordova-builder docker image...") + "\n"
|
|
1509
|
+
);
|
|
1510
|
+
const pullCode = await pullCordovaBuilder(req, res);
|
|
1511
|
+
res.write(req.__("Pull done with code %s", pullCode) + "\n");
|
|
1512
|
+
if (pullCode === 0) {
|
|
1513
|
+
res.write(req.__("Pruning docker...") + "\n");
|
|
1514
|
+
const pruneCode = await pruneDocker(req, res);
|
|
1515
|
+
res.write(req.__("Prune done with code %s", pruneCode) + "\n");
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1381
1518
|
}
|
|
1382
1519
|
res.end(
|
|
1383
1520
|
version === "latest"
|
|
@@ -1397,8 +1534,8 @@ const doInstall = async (req, res, version, runPull) => {
|
|
|
1397
1534
|
};
|
|
1398
1535
|
|
|
1399
1536
|
router.post("/install", isAdmin, async (req, res) => {
|
|
1400
|
-
const { version } = req.body;
|
|
1401
|
-
await doInstall(req, res, version, false);
|
|
1537
|
+
const { version, deep_clean } = req.body;
|
|
1538
|
+
await doInstall(req, res, version, deep_clean === "on", false);
|
|
1402
1539
|
});
|
|
1403
1540
|
|
|
1404
1541
|
/**
|
|
@@ -1411,7 +1548,7 @@ router.post(
|
|
|
1411
1548
|
"/upgrade",
|
|
1412
1549
|
isAdmin,
|
|
1413
1550
|
error_catcher(async (req, res) => {
|
|
1414
|
-
await doInstall(req, res, "latest", true);
|
|
1551
|
+
await doInstall(req, res, "latest", false, true);
|
|
1415
1552
|
})
|
|
1416
1553
|
);
|
|
1417
1554
|
/**
|
|
@@ -1749,9 +1886,10 @@ router.get(
|
|
|
1749
1886
|
});
|
|
1750
1887
|
})
|
|
1751
1888
|
);
|
|
1752
|
-
const buildDialogScript = (cordovaBuilderAvailable) =>
|
|
1753
|
-
|
|
1889
|
+
const buildDialogScript = (cordovaBuilderAvailable, isSbadmin2) =>
|
|
1890
|
+
`<script>
|
|
1754
1891
|
var cordovaBuilderAvailable = ${cordovaBuilderAvailable};
|
|
1892
|
+
var isSbadmin2 = ${isSbadmin2};
|
|
1755
1893
|
function showEntrySelect(type) {
|
|
1756
1894
|
for( const currentType of ["view", "page", "pagegroup"]) {
|
|
1757
1895
|
const tab = $('#' + currentType + 'NavLinkID');
|
|
@@ -1776,7 +1914,6 @@ const buildDialogScript = (cordovaBuilderAvailable) => {
|
|
|
1776
1914
|
notifyAlert("Building the app, please wait.", true)
|
|
1777
1915
|
}
|
|
1778
1916
|
</script>`;
|
|
1779
|
-
};
|
|
1780
1917
|
|
|
1781
1918
|
const imageAvailable = async () => {
|
|
1782
1919
|
try {
|
|
@@ -1834,19 +1971,29 @@ router.get(
|
|
|
1834
1971
|
const plugins = (await Plugin.find()).filter(
|
|
1835
1972
|
(plugin) => ["base", "sbadmin2"].indexOf(plugin.name) < 0
|
|
1836
1973
|
);
|
|
1974
|
+
const pluginsReadyForMobile = plugins
|
|
1975
|
+
.filter((plugin) => plugin.ready_for_mobile())
|
|
1976
|
+
.map((plugin) => plugin.name);
|
|
1837
1977
|
const builderSettings =
|
|
1838
1978
|
getState().getConfig("mobile_builder_settings") || {};
|
|
1839
1979
|
const dockerAvailable = await imageAvailable();
|
|
1840
1980
|
const xcodeCheckRes = await checkXcodebuild();
|
|
1841
1981
|
const xcodebuildAvailable = xcodeCheckRes.installed;
|
|
1842
1982
|
const xcodebuildVersion = xcodeCheckRes.version;
|
|
1983
|
+
const layout = getState().getLayout(req.user);
|
|
1984
|
+
const isSbadmin2 = layout === getState().layouts.sbadmin2;
|
|
1843
1985
|
send_admin_page({
|
|
1844
1986
|
res,
|
|
1845
1987
|
req,
|
|
1846
1988
|
active_sub: "Mobile app",
|
|
1847
1989
|
headers: [
|
|
1848
1990
|
{
|
|
1849
|
-
headerTag: buildDialogScript(dockerAvailable),
|
|
1991
|
+
headerTag: buildDialogScript(dockerAvailable, isSbadmin2),
|
|
1992
|
+
},
|
|
1993
|
+
{
|
|
1994
|
+
headerTag: `<script>var pluginsReadyForMobile = ${JSON.stringify(
|
|
1995
|
+
pluginsReadyForMobile
|
|
1996
|
+
)}</script>`,
|
|
1850
1997
|
},
|
|
1851
1998
|
],
|
|
1852
1999
|
contents: {
|
|
@@ -2884,17 +3031,66 @@ router.get(
|
|
|
2884
3031
|
})
|
|
2885
3032
|
);
|
|
2886
3033
|
|
|
3034
|
+
const validateBuildDirName = (buildDirName) => {
|
|
3035
|
+
// ensure characters
|
|
3036
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(buildDirName)) {
|
|
3037
|
+
getState().log(
|
|
3038
|
+
4,
|
|
3039
|
+
`Invalid characters in build directory name '${buildDirName}'`
|
|
3040
|
+
);
|
|
3041
|
+
return false;
|
|
3042
|
+
}
|
|
3043
|
+
// ensure format is 'build_1234567890'
|
|
3044
|
+
if (!/^build_\d+$/.test(buildDirName)) {
|
|
3045
|
+
getState().log(4, `Invalid build directory name format '${buildDirName}'`);
|
|
3046
|
+
return false;
|
|
3047
|
+
}
|
|
3048
|
+
return true;
|
|
3049
|
+
};
|
|
3050
|
+
|
|
3051
|
+
const validateBuildDir = (buildDir, rootPath) => {
|
|
3052
|
+
const resolvedBuildDir = path.resolve(buildDir);
|
|
3053
|
+
if (!resolvedBuildDir.startsWith(path.join(rootPath, "mobile_app"))) {
|
|
3054
|
+
getState().log(4, `Invalid build directory path '${buildDir}'`);
|
|
3055
|
+
return false;
|
|
3056
|
+
}
|
|
3057
|
+
return true;
|
|
3058
|
+
};
|
|
3059
|
+
|
|
2887
3060
|
router.get(
|
|
2888
3061
|
"/build-mobile-app/result",
|
|
2889
3062
|
isAdmin,
|
|
2890
3063
|
error_catcher(async (req, res) => {
|
|
2891
3064
|
const { build_dir_name } = req.query;
|
|
3065
|
+
if (!validateBuildDirName(build_dir_name)) {
|
|
3066
|
+
return res.sendWrap(req.__(`Admin`), {
|
|
3067
|
+
above: [
|
|
3068
|
+
{
|
|
3069
|
+
type: "card",
|
|
3070
|
+
title: req.__("Build Result"),
|
|
3071
|
+
contents: div(req.__("Invalid build directory name")),
|
|
3072
|
+
},
|
|
3073
|
+
],
|
|
3074
|
+
});
|
|
3075
|
+
}
|
|
2892
3076
|
const rootFolder = await File.rootFolder();
|
|
2893
3077
|
const buildDir = path.join(
|
|
2894
3078
|
rootFolder.location,
|
|
2895
3079
|
"mobile_app",
|
|
2896
3080
|
build_dir_name
|
|
2897
3081
|
);
|
|
3082
|
+
if (!validateBuildDir(buildDir, rootFolder.location)) {
|
|
3083
|
+
return res.sendWrap(req.__(`Admin`), {
|
|
3084
|
+
above: [
|
|
3085
|
+
{
|
|
3086
|
+
type: "card",
|
|
3087
|
+
title: req.__("Build Result"),
|
|
3088
|
+
contents: div(req.__("Invalid build directory path")),
|
|
3089
|
+
},
|
|
3090
|
+
],
|
|
3091
|
+
});
|
|
3092
|
+
}
|
|
3093
|
+
|
|
2898
3094
|
const files = await Promise.all(
|
|
2899
3095
|
fs
|
|
2900
3096
|
.readdirSync(buildDir)
|
|
@@ -2947,6 +3143,11 @@ router.post(
|
|
|
2947
3143
|
} = req.body;
|
|
2948
3144
|
if (!includedPlugins) includedPlugins = [];
|
|
2949
3145
|
if (!synchedTables) synchedTables = [];
|
|
3146
|
+
if (!entryPoint) {
|
|
3147
|
+
return res.json({
|
|
3148
|
+
error: req.__("Please select an entry point."),
|
|
3149
|
+
});
|
|
3150
|
+
}
|
|
2950
3151
|
if (!androidPlatform && !iOSPlatform) {
|
|
2951
3152
|
return res.json({
|
|
2952
3153
|
error: req.__("Please select at least one platform (android or iOS)."),
|
|
@@ -3093,8 +3294,8 @@ router.post(
|
|
|
3093
3294
|
error_catcher(async (req, res) => {
|
|
3094
3295
|
const state = getState();
|
|
3095
3296
|
const child = spawn(
|
|
3096
|
-
"docker
|
|
3097
|
-
["
|
|
3297
|
+
`${process.env.DOCKER_BIN ? `${process.env.DOCKER_BIN}/` : ""}docker`,
|
|
3298
|
+
["pull", "saltcorn/cordova-builder:latest"],
|
|
3098
3299
|
{
|
|
3099
3300
|
stdio: ["ignore", "pipe", "pipe"],
|
|
3100
3301
|
cwd: ".",
|
|
@@ -3545,6 +3746,7 @@ router.get(
|
|
|
3545
3746
|
send_admin_page({
|
|
3546
3747
|
res,
|
|
3547
3748
|
req,
|
|
3749
|
+
page_title: req.__(`%s code page`, name),
|
|
3548
3750
|
active_sub: "Development",
|
|
3549
3751
|
sub2_page: req.__(`%s code page`, name),
|
|
3550
3752
|
contents: {
|
package/routes/api.js
CHANGED
|
@@ -27,6 +27,7 @@ const Table = require("@saltcorn/data/models/table");
|
|
|
27
27
|
const View = require("@saltcorn/data/models/view");
|
|
28
28
|
//const Field = require("@saltcorn/data/models/field");
|
|
29
29
|
const Trigger = require("@saltcorn/data/models/trigger");
|
|
30
|
+
const File = require("@saltcorn/data/models/file");
|
|
30
31
|
//const load_plugins = require("../load_plugins");
|
|
31
32
|
const passport = require("passport");
|
|
32
33
|
|
|
@@ -189,6 +190,39 @@ router.post(
|
|
|
189
190
|
)(req, res, next);
|
|
190
191
|
})
|
|
191
192
|
);
|
|
193
|
+
|
|
194
|
+
router.get(
|
|
195
|
+
"/serve-files/*",
|
|
196
|
+
//passport.authenticate("api-bearer", { session: false }),
|
|
197
|
+
error_catcher(async (req, res, next) => {
|
|
198
|
+
await passport.authenticate(
|
|
199
|
+
"api-bearer",
|
|
200
|
+
{ session: false },
|
|
201
|
+
async function (err, user, info) {
|
|
202
|
+
const role = req?.user?.role_id || user?.role_id || 100;
|
|
203
|
+
const user_id = req?.user?.id || user?.id;
|
|
204
|
+
const serve_path = req.params[0];
|
|
205
|
+
const file = await File.findOne(serve_path);
|
|
206
|
+
if (
|
|
207
|
+
file &&
|
|
208
|
+
(role <= file.min_role_read || (user_id && user_id === file.user_id))
|
|
209
|
+
) {
|
|
210
|
+
res.type(file.mimetype);
|
|
211
|
+
const cacheability =
|
|
212
|
+
file.min_role_read === 100 ? "public" : "private";
|
|
213
|
+
const maxAge = getState().getConfig("files_cache_maxage", 86400);
|
|
214
|
+
res.set("Cache-Control", `${cacheability}, max-age=${maxAge}`);
|
|
215
|
+
if (file.s3_store)
|
|
216
|
+
res.status(404).json({ error: req.__("Not found") });
|
|
217
|
+
else res.sendFile(file.location);
|
|
218
|
+
} else {
|
|
219
|
+
res.status(404).json({ error: req.__("Not found") });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
)(req, res, next);
|
|
223
|
+
})
|
|
224
|
+
);
|
|
225
|
+
|
|
192
226
|
/**
|
|
193
227
|
*
|
|
194
228
|
*/
|
|
@@ -294,11 +328,12 @@ router.get(
|
|
|
294
328
|
} else {
|
|
295
329
|
const tbl_fields = table.getFields();
|
|
296
330
|
readState(req_query, tbl_fields, req);
|
|
297
|
-
const qstate =
|
|
331
|
+
const qstate = stateFieldsToWhere({
|
|
298
332
|
fields: tbl_fields,
|
|
299
333
|
approximate: !!approximate,
|
|
300
334
|
state: req_query,
|
|
301
335
|
table,
|
|
336
|
+
prefix: "a.",
|
|
302
337
|
});
|
|
303
338
|
const joinFields = {};
|
|
304
339
|
const derefs = Array.isArray(dereference)
|
package/routes/delete.js
CHANGED
|
@@ -38,7 +38,10 @@ router.post(
|
|
|
38
38
|
try {
|
|
39
39
|
if (role <= table.min_role_write)
|
|
40
40
|
await table.deleteRows({ id }, req.user || { role_id: 100 });
|
|
41
|
-
else if (
|
|
41
|
+
else if (
|
|
42
|
+
(table.ownership_field_id || table.ownership_formula) &&
|
|
43
|
+
req.user
|
|
44
|
+
) {
|
|
42
45
|
const row = await table.getRow(
|
|
43
46
|
{ id },
|
|
44
47
|
{ forUser: req.user, forPublic: !req.user }
|
package/routes/eventlog.js
CHANGED
|
@@ -41,6 +41,7 @@ const {
|
|
|
41
41
|
i,
|
|
42
42
|
th,
|
|
43
43
|
pre,
|
|
44
|
+
text,
|
|
44
45
|
} = require("@saltcorn/markup/tags");
|
|
45
46
|
const Table = require("@saltcorn/data/models/table");
|
|
46
47
|
const { send_events_page } = require("../markup/admin.js");
|
|
@@ -442,7 +443,7 @@ router.get(
|
|
|
442
443
|
) +
|
|
443
444
|
div(
|
|
444
445
|
{ class: "eventpayload" },
|
|
445
|
-
ev.payload ? pre(JSON.stringify(ev.payload, null, 2)) : ""
|
|
446
|
+
ev.payload ? pre(text(JSON.stringify(ev.payload, null, 2))) : ""
|
|
446
447
|
),
|
|
447
448
|
},
|
|
448
449
|
});
|
package/routes/fields.js
CHANGED
|
@@ -136,6 +136,9 @@ const fieldForm = async (req, fkey_opts, existing_names, id, hasData) => {
|
|
|
136
136
|
sublabel: req.__("Calculated from other fields with a formula"),
|
|
137
137
|
type: "Bool",
|
|
138
138
|
disabled: !!id,
|
|
139
|
+
help: {
|
|
140
|
+
topic: "Calculated fields",
|
|
141
|
+
},
|
|
139
142
|
}),
|
|
140
143
|
new Field({
|
|
141
144
|
label: req.__("Required"),
|
|
@@ -169,6 +172,9 @@ const fieldForm = async (req, fkey_opts, existing_names, id, hasData) => {
|
|
|
169
172
|
type: "Bool",
|
|
170
173
|
disabled: !!id,
|
|
171
174
|
showIf: { calculated: true },
|
|
175
|
+
help: {
|
|
176
|
+
topic: "Calculated fields",
|
|
177
|
+
},
|
|
172
178
|
}),
|
|
173
179
|
new Field({
|
|
174
180
|
label: req.__("Protected"),
|
|
@@ -176,6 +182,9 @@ const fieldForm = async (req, fkey_opts, existing_names, id, hasData) => {
|
|
|
176
182
|
sublabel: req.__("Set role to access"),
|
|
177
183
|
type: "Bool",
|
|
178
184
|
showIf: { calculated: false },
|
|
185
|
+
help: {
|
|
186
|
+
topic: "Protected fields",
|
|
187
|
+
},
|
|
179
188
|
}),
|
|
180
189
|
{
|
|
181
190
|
label: req.__("Minimum role to write"),
|
|
@@ -919,12 +928,19 @@ router.post(
|
|
|
919
928
|
"/test-formula",
|
|
920
929
|
isAdmin,
|
|
921
930
|
error_catcher(async (req, res) => {
|
|
922
|
-
|
|
931
|
+
let { formula, tablename, stored } = req.body;
|
|
932
|
+
if (stored === "false") stored = false;
|
|
933
|
+
|
|
923
934
|
const table = Table.findOne({ name: tablename });
|
|
924
935
|
const fields = table.getFields();
|
|
925
936
|
const freeVars = freeVariables(formula);
|
|
926
937
|
const joinFields = {};
|
|
927
|
-
|
|
938
|
+
add_free_variables_to_joinfields(freeVars, joinFields, fields);
|
|
939
|
+
if (!stored && Object.keys(joinFields).length > 0) {
|
|
940
|
+
return res
|
|
941
|
+
.status(400)
|
|
942
|
+
.send(`Joinfields only permitted in stored calculated fields`);
|
|
943
|
+
}
|
|
928
944
|
const rows = await table.getJoinedRows({
|
|
929
945
|
joinFields,
|
|
930
946
|
orderBy: "RANDOM()",
|
|
@@ -950,9 +966,9 @@ router.post(
|
|
|
950
966
|
);
|
|
951
967
|
} catch (e) {
|
|
952
968
|
console.error(e);
|
|
953
|
-
return res
|
|
954
|
-
|
|
955
|
-
|
|
969
|
+
return res
|
|
970
|
+
.status(400)
|
|
971
|
+
.send(`Error on running on row with id=${rows[0].id}: ${e.message}`);
|
|
956
972
|
}
|
|
957
973
|
})
|
|
958
974
|
);
|
package/routes/files.js
CHANGED
|
@@ -94,11 +94,15 @@ router.get(
|
|
|
94
94
|
const { dir, no_subdirs } = req.query;
|
|
95
95
|
const noSubdirs = no_subdirs === "true";
|
|
96
96
|
const safeDir = File.normalise(dir || "/");
|
|
97
|
-
const absFolder =
|
|
97
|
+
const absFolder = File.normalise_in_base(
|
|
98
98
|
db.connectObj.file_store,
|
|
99
99
|
db.getTenantSchema(),
|
|
100
|
-
|
|
100
|
+
dir || "/"
|
|
101
101
|
);
|
|
102
|
+
if (absFolder === null) {
|
|
103
|
+
res.json({ error: "Invalid path" });
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
102
106
|
const dirOnDisk = await File.from_file_on_disk(
|
|
103
107
|
path.basename(absFolder),
|
|
104
108
|
path.dirname(absFolder)
|
|
@@ -594,6 +598,9 @@ router.get(
|
|
|
594
598
|
isAdmin,
|
|
595
599
|
error_catcher(async (req, res) => {
|
|
596
600
|
const form = await storage_form(req);
|
|
601
|
+
form.blurb = [
|
|
602
|
+
`<div class="alert alert-warning">S3 storage options may not work for this release. Enabling S3 storage is not recommended</div>`,
|
|
603
|
+
];
|
|
597
604
|
send_files_page({
|
|
598
605
|
res,
|
|
599
606
|
req,
|