@saltcorn/server 0.9.0-beta.0 → 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/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
- strong,
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
- 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
- }
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(search_bar("q", req.query.q || "",
530
- { placeHolder: req.__("Search for..."), stateField: "q" })),
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
- const plugin_db = await Plugin.findOne({ name });
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(th(req.__("Package version")), td(mod.version)),
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
- latest || "",
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/${plugin_db.name}`,
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
- const plugin = await Plugin.store_by_name(decodeURIComponent(name));
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
- delete plugin.id;
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
- res.json({ finished: true, translatedIds });
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
- "Version history allows to track table data changes"
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
- req.__("Database table"),
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 !== "Database table"
1070
+ rest.provider_name !== "-"
1070
1071
  ) {
1071
1072
  const table = await Table.create(name, rest);
1072
1073
  res.redirect(`/table/provider-cfg/${table.id}`);
@@ -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.__("Column width"),
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;
@@ -568,11 +568,11 @@ describe("tags", () => {
568
568
  .post("/tag")
569
569
  .set("Cookie", loginCookie)
570
570
  .send("name=MyNewTestTag")
571
- .expect(toRedirect("/tag/1?show_list=tables"));
571
+ .expect(toRedirect("/tag/2?show_list=tables"));
572
572
  });
573
573
 
574
574
  itShouldIncludeTextForAdmin("/tag", "MyNewTestTag");
575
- itShouldIncludeTextForAdmin("/tag/1", "MyNewTestTag");
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");
@@ -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) return translatedIds ? translatedIds : error;
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();