@saltcorn/server 0.9.0-beta.1 → 0.9.0-beta.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 +58 -6
- package/auth/routes.js +16 -20
- package/errors.js +15 -4
- package/help/Actions.tmd +9 -0
- package/help/Extra state formula.tmd +62 -0
- package/help/Field views.tmd +22 -0
- package/help/JavaScript action code.tmd +161 -0
- package/help/Table formula constraint.tmd +2 -0
- package/help/View patterns.tmd +35 -0
- package/help/Where formula.tmd +30 -0
- package/help/index.js +11 -5
- package/locales/da.json +709 -709
- package/locales/de.json +1049 -1049
- package/locales/en.json +16 -2
- package/locales/pl.json +1155 -1155
- package/locales/ru.json +1101 -1101
- package/locales/si.json +1196 -1196
- package/locales/uk.json +1168 -1168
- package/locales/zh.json +886 -886
- package/package.json +10 -9
- package/public/saltcorn-builder.css +4 -0
- package/public/saltcorn-common.js +59 -5
- package/public/saltcorn.js +29 -3
- package/routes/actions.js +5 -3
- package/routes/admin.js +19 -4
- package/routes/fields.js +15 -3
- package/routes/menu.js +1 -1
- package/routes/packs.js +134 -9
- package/routes/plugins.js +186 -36
- package/routes/sync.js +4 -1
- package/routes/tables.js +4 -3
- package/routes/viewedit.js +21 -1
- package/tests/admin.test.js +2 -2
- package/tests/sync.test.js +140 -6
package/routes/plugins.js
CHANGED
|
@@ -7,13 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
const Router = require("express-promise-router");
|
|
9
9
|
const { isAdmin, error_catcher } = require("./utils.js");
|
|
10
|
-
const {
|
|
11
|
-
mkTable,
|
|
12
|
-
renderForm,
|
|
13
|
-
link,
|
|
14
|
-
post_btn,
|
|
15
|
-
post_delete_btn,
|
|
16
|
-
} = require("@saltcorn/markup");
|
|
10
|
+
const { renderForm, link, post_btn } = require("@saltcorn/markup");
|
|
17
11
|
const {
|
|
18
12
|
getState,
|
|
19
13
|
restart_tenant,
|
|
@@ -26,7 +20,6 @@ const { fetch_available_packs } = require("@saltcorn/admin-models/models/pack");
|
|
|
26
20
|
const {
|
|
27
21
|
upgrade_all_tenants_plugins,
|
|
28
22
|
} = require("@saltcorn/admin-models/models/tenant");
|
|
29
|
-
const { getConfig, setConfig } = require("@saltcorn/data/models/config");
|
|
30
23
|
const db = require("@saltcorn/data/db");
|
|
31
24
|
const {
|
|
32
25
|
plugin_types_info_card,
|
|
@@ -37,7 +30,6 @@ const {
|
|
|
37
30
|
const load_plugins = require("../load_plugins");
|
|
38
31
|
const {
|
|
39
32
|
h5,
|
|
40
|
-
nbsp,
|
|
41
33
|
a,
|
|
42
34
|
div,
|
|
43
35
|
span,
|
|
@@ -50,7 +42,11 @@ const {
|
|
|
50
42
|
th,
|
|
51
43
|
td,
|
|
52
44
|
p,
|
|
53
|
-
|
|
45
|
+
form,
|
|
46
|
+
select,
|
|
47
|
+
option,
|
|
48
|
+
input,
|
|
49
|
+
label,
|
|
54
50
|
text,
|
|
55
51
|
} = require("@saltcorn/markup/tags");
|
|
56
52
|
const { search_bar } = require("@saltcorn/markup/helpers");
|
|
@@ -58,8 +54,9 @@ const fs = require("fs");
|
|
|
58
54
|
const path = require("path");
|
|
59
55
|
const { get_latest_npm_version } = require("@saltcorn/data/models/config");
|
|
60
56
|
const { flash_restart } = require("../markup/admin.js");
|
|
61
|
-
const { sleep } = require("@saltcorn/data/utils");
|
|
57
|
+
const { sleep, removeNonWordChars } = require("@saltcorn/data/utils");
|
|
62
58
|
const { loadAllPlugins } = require("../load_plugins");
|
|
59
|
+
const npmFetch = require("npm-registry-fetch");
|
|
63
60
|
|
|
64
61
|
/**
|
|
65
62
|
* @type {object}
|
|
@@ -177,6 +174,7 @@ const get_store_items = async () => {
|
|
|
177
174
|
has_theme: plugin.has_theme,
|
|
178
175
|
has_auth: plugin.has_auth,
|
|
179
176
|
unsafe: plugin.unsafe,
|
|
177
|
+
source: plugin.source,
|
|
180
178
|
}))
|
|
181
179
|
.filter((p) => !p.unsafe || isRoot || tenants_unsafe_plugins);
|
|
182
180
|
const local_logins = installed_plugins
|
|
@@ -290,15 +288,19 @@ const store_item_html = (req) => (item) => ({
|
|
|
290
288
|
div(
|
|
291
289
|
!item.installed &&
|
|
292
290
|
item.plugin &&
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
291
|
+
div(
|
|
292
|
+
{ class: "me-2 d-inline" },
|
|
293
|
+
post_btn(
|
|
294
|
+
`/plugins/install/${encodeURIComponent(item.name)}`,
|
|
295
|
+
req.__("Install"),
|
|
296
|
+
req.csrfToken(),
|
|
297
|
+
{
|
|
298
|
+
klass: "store-install",
|
|
299
|
+
small: true,
|
|
300
|
+
onClick: "press_store_button(this)",
|
|
301
|
+
formClass: "d-inline",
|
|
302
|
+
}
|
|
303
|
+
)
|
|
302
304
|
),
|
|
303
305
|
!item.installed &&
|
|
304
306
|
item.pack &&
|
|
@@ -526,8 +528,12 @@ const plugin_store_html = (items, req) => {
|
|
|
526
528
|
contents: div(
|
|
527
529
|
{ class: "d-flex justify-content-between" },
|
|
528
530
|
storeNavPills(req),
|
|
529
|
-
div(
|
|
530
|
-
|
|
531
|
+
div(
|
|
532
|
+
search_bar("q", req.query.q || "", {
|
|
533
|
+
placeHolder: req.__("Search for..."),
|
|
534
|
+
stateField: "q",
|
|
535
|
+
})
|
|
536
|
+
),
|
|
531
537
|
div(store_actions_dropdown(req))
|
|
532
538
|
),
|
|
533
539
|
},
|
|
@@ -558,6 +564,105 @@ router.get(
|
|
|
558
564
|
})
|
|
559
565
|
);
|
|
560
566
|
|
|
567
|
+
router.get(
|
|
568
|
+
"/versions_dialog/:name",
|
|
569
|
+
isAdmin,
|
|
570
|
+
error_catcher(async (req, res) => {
|
|
571
|
+
const { name } = req.params;
|
|
572
|
+
const withoutOrg = name.replace(/^@saltcorn\//, "");
|
|
573
|
+
const plugin = await Plugin.store_by_name(decodeURIComponent(withoutOrg));
|
|
574
|
+
if (!plugin) {
|
|
575
|
+
getState().log(
|
|
576
|
+
2,
|
|
577
|
+
`GET /versions_dialog${withoutOrg}: '${withoutOrg}' not found`
|
|
578
|
+
);
|
|
579
|
+
return res
|
|
580
|
+
.status(404)
|
|
581
|
+
.json({ error: req.__("Module '%s' not found", withoutOrg) });
|
|
582
|
+
} else {
|
|
583
|
+
try {
|
|
584
|
+
const pkgInfo = await npmFetch.json(
|
|
585
|
+
`https://registry.npmjs.org/${plugin.location}`
|
|
586
|
+
);
|
|
587
|
+
if (!pkgInfo?.versions)
|
|
588
|
+
throw new Error(req.__("Unable to fetch versions"));
|
|
589
|
+
res.set("Page-Title", req.__("%s versions", text(withoutOrg)));
|
|
590
|
+
const versions = Object.keys(pkgInfo.versions);
|
|
591
|
+
if (versions.length === 0) throw new Error(req.__("No versions found"));
|
|
592
|
+
let selected = null;
|
|
593
|
+
if (getState().plugins[plugin.name]) {
|
|
594
|
+
const mod = await load_plugins.requirePlugin(plugin);
|
|
595
|
+
if (mod) selected = mod.version;
|
|
596
|
+
}
|
|
597
|
+
if (!selected) selected = versions[versions.length - 1];
|
|
598
|
+
return res.send(
|
|
599
|
+
form(
|
|
600
|
+
{
|
|
601
|
+
action: `/plugins/install/${encodeURIComponent(name)}`,
|
|
602
|
+
method: "post",
|
|
603
|
+
},
|
|
604
|
+
input({ type: "hidden", name: "_csrf", value: req.csrfToken() }),
|
|
605
|
+
div(
|
|
606
|
+
{ class: "form-group" },
|
|
607
|
+
label(
|
|
608
|
+
{
|
|
609
|
+
for: "version_select",
|
|
610
|
+
class: "form-label fw-bold",
|
|
611
|
+
},
|
|
612
|
+
req.__("Version")
|
|
613
|
+
),
|
|
614
|
+
select(
|
|
615
|
+
{
|
|
616
|
+
id: "version_select",
|
|
617
|
+
class: "form-control form-select",
|
|
618
|
+
name: "version",
|
|
619
|
+
},
|
|
620
|
+
versions.map((version) =>
|
|
621
|
+
option({
|
|
622
|
+
id: `${version}_opt`,
|
|
623
|
+
value: version,
|
|
624
|
+
label: version,
|
|
625
|
+
selected: version === selected,
|
|
626
|
+
})
|
|
627
|
+
)
|
|
628
|
+
)
|
|
629
|
+
),
|
|
630
|
+
div(
|
|
631
|
+
{ class: "d-flex justify-content-end" },
|
|
632
|
+
button(
|
|
633
|
+
{
|
|
634
|
+
type: "button",
|
|
635
|
+
class: "btn btn-secondary me-2",
|
|
636
|
+
"data-bs-dismiss": "modal",
|
|
637
|
+
},
|
|
638
|
+
req.__("Close")
|
|
639
|
+
),
|
|
640
|
+
button(
|
|
641
|
+
{
|
|
642
|
+
type: "submit",
|
|
643
|
+
class: "btn btn-primary",
|
|
644
|
+
onClick: "press_store_button(this)",
|
|
645
|
+
},
|
|
646
|
+
req.__("Install")
|
|
647
|
+
)
|
|
648
|
+
)
|
|
649
|
+
)
|
|
650
|
+
);
|
|
651
|
+
} catch (error) {
|
|
652
|
+
getState().log(
|
|
653
|
+
2,
|
|
654
|
+
`GET /versions_dialog${withoutOrg}: ${
|
|
655
|
+
error.message || "unknown error"
|
|
656
|
+
}`
|
|
657
|
+
);
|
|
658
|
+
return res
|
|
659
|
+
.status(500)
|
|
660
|
+
.json({ error: error.message || "unknown error" });
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
})
|
|
664
|
+
);
|
|
665
|
+
|
|
561
666
|
/**
|
|
562
667
|
* @name get/configure/:name
|
|
563
668
|
* @function
|
|
@@ -810,7 +915,12 @@ router.get(
|
|
|
810
915
|
isAdmin,
|
|
811
916
|
error_catcher(async (req, res) => {
|
|
812
917
|
const { name } = req.params;
|
|
813
|
-
|
|
918
|
+
let plugin_db = await Plugin.findOne({ name });
|
|
919
|
+
if (!plugin_db) {
|
|
920
|
+
req.flash("warning", req.__("Module not found"));
|
|
921
|
+
res.redirect("/plugins");
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
814
924
|
const mod = await load_plugins.requirePlugin(plugin_db);
|
|
815
925
|
const store_items = await get_store_items();
|
|
816
926
|
const store_item = store_items.find((item) => item.name === name);
|
|
@@ -821,27 +931,49 @@ router.get(
|
|
|
821
931
|
update_permitted &&
|
|
822
932
|
(await get_latest_npm_version(plugin_db.location, 1000));
|
|
823
933
|
const can_update = update_permitted && latest && mod.version !== latest;
|
|
934
|
+
const can_select_version = update_permitted && plugin_db.source === "npm";
|
|
824
935
|
let pkgjson;
|
|
825
936
|
if (mod.location && fs.existsSync(path.join(mod.location, "package.json")))
|
|
826
937
|
pkgjson = require(path.join(mod.location, "package.json"));
|
|
827
|
-
|
|
828
|
-
if (!plugin_db) {
|
|
829
|
-
req.flash("warning", req.__("Module not found"));
|
|
830
|
-
res.redirect("/plugins");
|
|
831
|
-
return;
|
|
832
|
-
}
|
|
938
|
+
const domId = `${removeNonWordChars(mod.name)}_store_version_btn`;
|
|
833
939
|
const infoTable = table(
|
|
834
940
|
tbody(
|
|
835
941
|
tr(th(req.__("Package name")), td(mod.name)),
|
|
836
|
-
tr(
|
|
942
|
+
tr(
|
|
943
|
+
th(req.__("Package version")),
|
|
944
|
+
td(
|
|
945
|
+
span(
|
|
946
|
+
{ style: "display: inline-block; min-width: 2.9rem;" },
|
|
947
|
+
mod.version
|
|
948
|
+
),
|
|
949
|
+
can_select_version
|
|
950
|
+
? a(
|
|
951
|
+
{
|
|
952
|
+
id: domId,
|
|
953
|
+
class: "store-install btn btn-sm btn-primary ms-2",
|
|
954
|
+
onClick: "press_store_button(this, true)",
|
|
955
|
+
href: `javascript:ajax_modal('/plugins/versions_dialog/${encodeURIComponent(
|
|
956
|
+
encodeURIComponent(plugin_db.name)
|
|
957
|
+
)}', { onOpen: () => { restore_old_button('${domId}'); }, onError: (res) => { selectVersionError(res, '${domId}') } });`,
|
|
958
|
+
},
|
|
959
|
+
req.__("install a different version")
|
|
960
|
+
)
|
|
961
|
+
: ""
|
|
962
|
+
)
|
|
963
|
+
),
|
|
837
964
|
tr(
|
|
838
965
|
th(req.__("Latest version")),
|
|
839
966
|
td(
|
|
840
|
-
|
|
967
|
+
span(
|
|
968
|
+
{ style: "display: inline-block; min-width: 2.9rem;" },
|
|
969
|
+
latest || ""
|
|
970
|
+
),
|
|
841
971
|
can_update
|
|
842
972
|
? a(
|
|
843
973
|
{
|
|
844
|
-
href: `/plugins/upgrade-plugin/${
|
|
974
|
+
href: `/plugins/upgrade-plugin/${encodeURIComponent(
|
|
975
|
+
plugin_db.name
|
|
976
|
+
)}`,
|
|
845
977
|
class: "btn btn-primary btn-sm ms-2",
|
|
846
978
|
},
|
|
847
979
|
req.__("Upgrade")
|
|
@@ -979,7 +1111,7 @@ router.get(
|
|
|
979
1111
|
await plugin.upgrade_version((p, f) => load_plugins.loadPlugin(p, f));
|
|
980
1112
|
req.flash("success", req.__(`Module up-to-date`));
|
|
981
1113
|
|
|
982
|
-
res.redirect(`/plugins/info/${plugin.name}`);
|
|
1114
|
+
res.redirect(`/plugins/info/${encodeURIComponent(plugin.name)}`);
|
|
983
1115
|
})
|
|
984
1116
|
);
|
|
985
1117
|
|
|
@@ -1068,11 +1200,25 @@ router.post(
|
|
|
1068
1200
|
isAdmin,
|
|
1069
1201
|
error_catcher(async (req, res) => {
|
|
1070
1202
|
const { name } = req.params;
|
|
1203
|
+
const { version } = req.body;
|
|
1071
1204
|
const tenants_unsafe_plugins = getRootState().getConfig(
|
|
1072
1205
|
"tenants_unsafe_plugins",
|
|
1073
1206
|
false
|
|
1074
1207
|
);
|
|
1075
|
-
|
|
1208
|
+
// when a version is specified, either update the db row or use the plugin from the store
|
|
1209
|
+
// when no version is specified, allways use the plugin from the store
|
|
1210
|
+
let plugin = null;
|
|
1211
|
+
if (version) {
|
|
1212
|
+
plugin = await Plugin.findOne({ name: name });
|
|
1213
|
+
if (plugin) plugin.version = version;
|
|
1214
|
+
}
|
|
1215
|
+
if (!plugin) {
|
|
1216
|
+
plugin = await Plugin.store_by_name(decodeURIComponent(name));
|
|
1217
|
+
if (plugin) {
|
|
1218
|
+
delete plugin.id;
|
|
1219
|
+
if (version) plugin.version = version;
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1076
1222
|
if (!plugin) {
|
|
1077
1223
|
req.flash(
|
|
1078
1224
|
"error",
|
|
@@ -1081,6 +1227,11 @@ router.post(
|
|
|
1081
1227
|
res.redirect(`/plugins`);
|
|
1082
1228
|
return;
|
|
1083
1229
|
}
|
|
1230
|
+
let forceReInstall =
|
|
1231
|
+
version !== undefined ||
|
|
1232
|
+
(plugin.source === "npm" && plugin.version === "latest");
|
|
1233
|
+
if (version) plugin.version = version;
|
|
1234
|
+
|
|
1084
1235
|
const isRoot = db.getTenantSchema() === db.connectObj.default_schema;
|
|
1085
1236
|
if (!isRoot && plugin.unsafe && !tenants_unsafe_plugins) {
|
|
1086
1237
|
req.flash(
|
|
@@ -1090,8 +1241,7 @@ router.post(
|
|
|
1090
1241
|
res.redirect(`/plugins`);
|
|
1091
1242
|
return;
|
|
1092
1243
|
}
|
|
1093
|
-
|
|
1094
|
-
await load_plugins.loadAndSaveNewPlugin(plugin);
|
|
1244
|
+
await load_plugins.loadAndSaveNewPlugin(plugin, forceReInstall);
|
|
1095
1245
|
const plugin_module = getState().plugins[name];
|
|
1096
1246
|
if (plugin_module && plugin_module.configuration_workflow) {
|
|
1097
1247
|
const plugin_db = await Plugin.findOne({ name });
|
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/tables.js
CHANGED
|
@@ -163,7 +163,7 @@ const tableForm = async (table, req) => {
|
|
|
163
163
|
{
|
|
164
164
|
label: req.__("Version history"),
|
|
165
165
|
sublabel: req.__(
|
|
166
|
-
"
|
|
166
|
+
"Track table data changes over time"
|
|
167
167
|
),
|
|
168
168
|
name: "versioned",
|
|
169
169
|
type: "Bool",
|
|
@@ -235,7 +235,8 @@ router.get(
|
|
|
235
235
|
name: "provider_name",
|
|
236
236
|
input_type: "select",
|
|
237
237
|
options: [
|
|
238
|
-
|
|
238
|
+
// Due to packages/saltcorn-markup/helpers.ts#L45 (select_options replaces label if o.value === "")
|
|
239
|
+
{label:req.__("Database table"), value:'-'},
|
|
239
240
|
...table_provider_names,
|
|
240
241
|
],
|
|
241
242
|
required: true,
|
|
@@ -1066,7 +1067,7 @@ router.post(
|
|
|
1066
1067
|
res.redirect(`/table/new`);
|
|
1067
1068
|
} else if (
|
|
1068
1069
|
rest.provider_name &&
|
|
1069
|
-
rest.provider_name !== "
|
|
1070
|
+
rest.provider_name !== "-"
|
|
1070
1071
|
) {
|
|
1071
1072
|
const table = await Table.create(name, rest);
|
|
1072
1073
|
res.redirect(`/table/provider-cfg/${table.id}`);
|
package/routes/viewedit.js
CHANGED
|
@@ -147,6 +147,10 @@ const viewForm = async (req, tableOptions, roles, pages, values) => {
|
|
|
147
147
|
sublabel: req.__(
|
|
148
148
|
"The view pattern sets the foundation of how the view relates to the table and the behaviour of the view"
|
|
149
149
|
),
|
|
150
|
+
help: {
|
|
151
|
+
topic: "View patterns",
|
|
152
|
+
context: {},
|
|
153
|
+
},
|
|
150
154
|
options: Object.keys(getState().viewtemplates),
|
|
151
155
|
attributes: {
|
|
152
156
|
explainers: mapObjectValues(
|
|
@@ -209,7 +213,7 @@ const viewForm = async (req, tableOptions, roles, pages, values) => {
|
|
|
209
213
|
}),
|
|
210
214
|
{
|
|
211
215
|
name: "popup_width",
|
|
212
|
-
label: req.__("
|
|
216
|
+
label: req.__("Popup width"),
|
|
213
217
|
type: "Integer",
|
|
214
218
|
tab: "Popup settings",
|
|
215
219
|
parent_field: "attributes",
|
|
@@ -232,6 +236,9 @@ const viewForm = async (req, tableOptions, roles, pages, values) => {
|
|
|
232
236
|
label: req.__("Save indicator"),
|
|
233
237
|
type: "Bool",
|
|
234
238
|
parent_field: "attributes",
|
|
239
|
+
sublabel: req.__(
|
|
240
|
+
"Show an icon in the title bar to indicate when form data is being saved"
|
|
241
|
+
),
|
|
235
242
|
tab: "Popup settings",
|
|
236
243
|
},
|
|
237
244
|
{
|
|
@@ -625,6 +632,19 @@ router.post(
|
|
|
625
632
|
|
|
626
633
|
const view = await View.findOne({ name });
|
|
627
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
|
+
};
|
|
628
648
|
const wfres = await configFlow.run(req.body, req);
|
|
629
649
|
|
|
630
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
|
@@ -5,11 +5,15 @@ const {
|
|
|
5
5
|
getAdminLoginCookie,
|
|
6
6
|
resetToFixtures,
|
|
7
7
|
respondJsonWith,
|
|
8
|
+
toRedirect,
|
|
9
|
+
toInclude,
|
|
10
|
+
toSucceed,
|
|
8
11
|
} = require("../auth/testhelp");
|
|
9
12
|
const db = require("@saltcorn/data/db");
|
|
10
13
|
const { sleep } = require("@saltcorn/data/tests/mocks");
|
|
11
14
|
|
|
12
15
|
const Table = require("@saltcorn/data/models/table");
|
|
16
|
+
const TableConstraint = require("@saltcorn/data/models/table_constraints");
|
|
13
17
|
const Field = require("@saltcorn/data/models/field");
|
|
14
18
|
const User = require("@saltcorn/data/models/user");
|
|
15
19
|
|
|
@@ -181,6 +185,52 @@ describe("load remote insert/updates", () => {
|
|
|
181
185
|
}
|
|
182
186
|
});
|
|
183
187
|
|
|
188
|
+
it("sync table with capitals", async () => {
|
|
189
|
+
const app = await getApp({ disableCsrf: true });
|
|
190
|
+
const loginCookie = await getAdminLoginCookie();
|
|
191
|
+
// create table
|
|
192
|
+
await request(app)
|
|
193
|
+
.post("/table")
|
|
194
|
+
.set("Cookie", loginCookie)
|
|
195
|
+
.send(`name=${encodeURIComponent("Table with capitals")}`)
|
|
196
|
+
.expect(toRedirect("/table/16"));
|
|
197
|
+
// add a field
|
|
198
|
+
await request(app)
|
|
199
|
+
.post("/field/")
|
|
200
|
+
.send("stepName=Basic properties")
|
|
201
|
+
.send("name=string_field")
|
|
202
|
+
.send("label=StringField")
|
|
203
|
+
.send("type=String")
|
|
204
|
+
.send(
|
|
205
|
+
`contextEnc=${encodeURIComponent(JSON.stringify({ table_id: 16 }))}`
|
|
206
|
+
)
|
|
207
|
+
.set("Cookie", loginCookie)
|
|
208
|
+
.expect(toInclude("options"));
|
|
209
|
+
// init sync_info table
|
|
210
|
+
await request(app)
|
|
211
|
+
.post("/table")
|
|
212
|
+
.send("id=16")
|
|
213
|
+
.send("has_sync_info=on")
|
|
214
|
+
.set("Cookie", loginCookie)
|
|
215
|
+
.expect(toRedirect("/table/16"));
|
|
216
|
+
const dbTime = await db.time();
|
|
217
|
+
|
|
218
|
+
// call load changes
|
|
219
|
+
await request(app)
|
|
220
|
+
.post("/sync/load_changes")
|
|
221
|
+
.set("Cookie", loginCookie)
|
|
222
|
+
.send({
|
|
223
|
+
loadUntil: (await db.time()).valueOf(),
|
|
224
|
+
syncInfos: {
|
|
225
|
+
"Table with capitals": {
|
|
226
|
+
maxLoadedId: 0,
|
|
227
|
+
syncFrom: dbTime.valueOf(),
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
})
|
|
231
|
+
.expect(toSucceed());
|
|
232
|
+
});
|
|
233
|
+
|
|
184
234
|
it("load sync not authorized", async () => {
|
|
185
235
|
const app = await getApp({ disableCsrf: true });
|
|
186
236
|
const loginCookie = await getUserLoginCookie();
|
|
@@ -300,8 +350,9 @@ describe("Upload changes", () => {
|
|
|
300
350
|
.get(`/sync/upload_finished?dir_name=${encodeURIComponent(syncDir)}`)
|
|
301
351
|
.set("Cookie", loginCookie);
|
|
302
352
|
expect(resp.status).toBe(200);
|
|
303
|
-
const { finished, translatedIds, error } = resp._body;
|
|
304
|
-
if (finished)
|
|
353
|
+
const { finished, translatedIds, uniqueConflicts, error } = resp._body;
|
|
354
|
+
if (finished)
|
|
355
|
+
return translatedIds ? { translatedIds, uniqueConflicts } : error;
|
|
305
356
|
await sleep(1000);
|
|
306
357
|
}
|
|
307
358
|
return null;
|
|
@@ -352,7 +403,7 @@ describe("Upload changes", () => {
|
|
|
352
403
|
});
|
|
353
404
|
expect(resp.status).toBe(200);
|
|
354
405
|
const { syncDir } = resp._body;
|
|
355
|
-
const translatedIds = await getResult(app, loginCookie, syncDir);
|
|
406
|
+
const { translatedIds } = await getResult(app, loginCookie, syncDir);
|
|
356
407
|
await cleanSyncDir(app, loginCookie, syncDir);
|
|
357
408
|
expect(translatedIds).toBeDefined();
|
|
358
409
|
expect(translatedIds).toEqual({
|
|
@@ -365,6 +416,89 @@ describe("Upload changes", () => {
|
|
|
365
416
|
});
|
|
366
417
|
});
|
|
367
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
|
+
|
|
368
502
|
it("update with translation", async () => {
|
|
369
503
|
const app = await getApp({ disableCsrf: true });
|
|
370
504
|
const loginCookie = await getAdminLoginCookie();
|
|
@@ -389,7 +523,7 @@ describe("Upload changes", () => {
|
|
|
389
523
|
});
|
|
390
524
|
expect(resp.status).toBe(200);
|
|
391
525
|
const { syncDir } = resp._body;
|
|
392
|
-
const translatedIds = await getResult(app, loginCookie, syncDir);
|
|
526
|
+
const { translatedIds } = await getResult(app, loginCookie, syncDir);
|
|
393
527
|
await cleanSyncDir(app, loginCookie, syncDir);
|
|
394
528
|
expect(translatedIds).toBeDefined();
|
|
395
529
|
expect(translatedIds).toEqual({
|
|
@@ -427,7 +561,7 @@ describe("Upload changes", () => {
|
|
|
427
561
|
});
|
|
428
562
|
expect(resp.status).toBe(200);
|
|
429
563
|
const { syncDir } = resp._body;
|
|
430
|
-
const translatedIds = await getResult(app, loginCookie, syncDir);
|
|
564
|
+
const { translatedIds } = await getResult(app, loginCookie, syncDir);
|
|
431
565
|
await cleanSyncDir(app, loginCookie, syncDir);
|
|
432
566
|
expect(translatedIds).toBeDefined();
|
|
433
567
|
const afterDelete = await books.getRows();
|
|
@@ -471,7 +605,7 @@ describe("Upload changes", () => {
|
|
|
471
605
|
});
|
|
472
606
|
expect(resp.status).toBe(200);
|
|
473
607
|
const { syncDir } = resp._body;
|
|
474
|
-
const translatedIds = await getResult(app, loginCookie, syncDir);
|
|
608
|
+
const { translatedIds } = await getResult(app, loginCookie, syncDir);
|
|
475
609
|
await cleanSyncDir(app, loginCookie, syncDir);
|
|
476
610
|
expect(translatedIds).toBeDefined();
|
|
477
611
|
const afterDelete = await books.getRows();
|