@saltcorn/server 0.8.8-beta.1 → 0.8.8-beta.3

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.
@@ -4,10 +4,10 @@ const {
4
4
  getUserLoginCookie,
5
5
  getAdminLoginCookie,
6
6
  resetToFixtures,
7
- notAuthorized,
8
7
  respondJsonWith,
9
8
  } = require("../auth/testhelp");
10
9
  const db = require("@saltcorn/data/db");
10
+ const { sleep } = require("@saltcorn/data/tests/mocks");
11
11
 
12
12
  const Table = require("@saltcorn/data/models/table");
13
13
 
@@ -16,93 +16,469 @@ beforeAll(async () => {
16
16
  });
17
17
  afterAll(db.close);
18
18
 
19
- describe("Synchronise with mobile offline data", () => {
20
- it("not permitted", async () => {
21
- if (!db.isSQLite) {
22
- const patients = Table.findOne({ name: "patients" });
23
- const books = Table.findOne({ name: "books" });
24
- const patientsBefore = await patients.countRows();
25
- const booksBefore = await books.countRows();
19
+ jest.setTimeout(10000);
20
+
21
+ const initSyncInfo = async (tbls) => {
22
+ for (const tbl of tbls) {
23
+ const books = Table.findOne({ name: tbl });
24
+ if (books.has_sync_info) await db.deleteWhere(`${tbl}_sync_info`, {});
25
+ else {
26
+ books.has_sync_info = true;
27
+ await books.update(books);
28
+ }
29
+ }
30
+ };
31
+
32
+ describe("load remote insert/updates", () => {
33
+ if (!db.isSQLite) {
34
+ beforeAll(async () => {
35
+ await initSyncInfo(["books", "publisher"]);
36
+ });
37
+ it("check params", async () => {
26
38
  const app = await getApp({ disableCsrf: true });
27
- const loginCookie = await getUserLoginCookie();
39
+ const loginCookie = await getAdminLoginCookie();
28
40
  await request(app)
29
- .post("/sync/table_data")
41
+ .post("/sync/load_changes")
30
42
  .set("Cookie", loginCookie)
31
43
  .send({
32
- data: {
33
- patients: [
34
- {
35
- name: "Brad Pitt",
36
- favbook: 2,
37
- parent: 1,
38
- },
39
- {
40
- id: 84,
41
- name: "Pitt Brad",
42
- favbook: 2,
43
- parent: 1,
44
- },
45
- ],
46
- books: [
47
- {
48
- id: 3,
49
- author: "foo",
50
- pages: 20,
51
- publisher: 1,
52
- },
53
- ],
44
+ syncInfos: {
45
+ books: {},
54
46
  },
55
47
  })
56
- .expect(notAuthorized);
57
- const patientsAfter = await patients.countRows();
58
- const booksAfter = await books.countRows();
59
- expect(patientsAfter).toBe(patientsBefore);
60
- expect(booksAfter).toBe(booksBefore);
61
- }
62
- });
48
+ .expect(
49
+ respondJsonWith(400, (resp) => resp.error === "loadUntil is missing")
50
+ );
51
+ await request(app)
52
+ .post("/sync/load_changes")
53
+ .set("Cookie", loginCookie)
54
+ .send({
55
+ loadUntil: new Date().valueOf(),
56
+ })
57
+ .expect(
58
+ respondJsonWith(400, (resp) => resp.error === "syncInfos is missing")
59
+ );
60
+ });
63
61
 
64
- it("upload patients and books", async () => {
65
- if (!db.isSQLite) {
66
- const patients = Table.findOne({ name: "patients" });
62
+ it("without syncFrom", async () => {
63
+ const app = await getApp({ disableCsrf: true });
64
+ const loginCookie = await getAdminLoginCookie();
67
65
  const books = Table.findOne({ name: "books" });
68
- const patientsBefore = await patients.countRows();
69
- const booksBefore = await books.countRows();
66
+ const dbLength = await books.countRows();
67
+ const loadUntil = new Date();
68
+ const resp = await request(app)
69
+ .post("/sync/load_changes")
70
+ .set("Cookie", loginCookie)
71
+ .send({
72
+ loadUntil: loadUntil.valueOf(),
73
+ syncInfos: {
74
+ books: {
75
+ maxLoadedId: 0,
76
+ },
77
+ },
78
+ });
79
+ expect(resp.status).toBe(200);
80
+ const data = resp._body;
81
+ expect(data.books.rows.length).toBe(dbLength);
82
+ for (const row of data.books.rows) {
83
+ const fromDb = await books.getRows({ id: row._sync_info_tbl_ref_ });
84
+ expect(fromDb.length).toBe(1);
85
+ expect(row._sync_info_tbl_last_modified_).toBe(loadUntil.valueOf());
86
+ expect(row._sync_info_tbl_deleted_).toBe(null);
87
+ }
88
+ });
89
+
90
+ it("with syncFrom, without sync_infos", async () => {
70
91
  const app = await getApp({ disableCsrf: true });
71
92
  const loginCookie = await getAdminLoginCookie();
72
- await request(app)
73
- .post("/sync/table_data")
93
+ const loadUntil = new Date();
94
+ const resp = await request(app)
95
+ .post("/sync/load_changes")
74
96
  .set("Cookie", loginCookie)
75
97
  .send({
76
- data: {
77
- patients: [
78
- {
79
- name: "Brad Pitt",
80
- favbook: 2,
81
- parent: 1,
82
- },
83
- {
84
- id: 84,
85
- name: "Pitt Brad",
86
- favbook: 2,
87
- parent: 1,
98
+ loadUntil: loadUntil.valueOf(),
99
+ syncInfos: {
100
+ books: {
101
+ maxLoadedId: 0,
102
+ syncFrom: 1000,
103
+ },
104
+ },
105
+ });
106
+ expect(resp.status).toBe(200);
107
+ const data = resp._body;
108
+ expect(data.books.rows.length).toBe(0);
109
+ });
110
+
111
+ it("with syncFrom, with sync_infos", async () => {
112
+ const app = await getApp({ disableCsrf: true });
113
+ const loginCookie = await getAdminLoginCookie();
114
+ const books = Table.findOne({ name: "books" });
115
+ await books.updateRow({ author: "Herman Melville" }, 1);
116
+ await sleep(200);
117
+ const dbTime = await db.time();
118
+ await books.updateRow({ author: "Leo Tolstoy" }, 2);
119
+ const { last_modified } = await books.latestSyncInfo(2);
120
+ {
121
+ const resp = await request(app)
122
+ .post("/sync/load_changes")
123
+ .set("Cookie", loginCookie)
124
+ .send({
125
+ loadUntil: (await db.time()).valueOf(),
126
+ syncInfos: {
127
+ books: {
128
+ maxLoadedId: 0,
129
+ syncFrom: dbTime.valueOf(),
88
130
  },
89
- ],
90
- books: [
91
- {
92
- id: 3,
93
- author: "foo",
94
- pages: 20,
95
- publisher: 1,
131
+ },
132
+ });
133
+ expect(resp.status).toBe(200);
134
+ const data = resp._body;
135
+ expect(data.books.rows.length).toBe(1);
136
+
137
+ const {
138
+ _sync_info_tbl_ref_,
139
+ _sync_info_tbl_last_modified_,
140
+ _sync_info_tbl_deleted_,
141
+ ...rest
142
+ } = data.books.rows[0];
143
+ expect(_sync_info_tbl_ref_).toBe(2);
144
+ expect(_sync_info_tbl_last_modified_).toBe(last_modified.valueOf());
145
+ expect(_sync_info_tbl_deleted_).toBe(false);
146
+ expect(rest.author).toBe("Leo Tolstoy");
147
+ }
148
+ await books.updateRow({ author: "Herman Melville" }, 1);
149
+ {
150
+ const resp = await request(app)
151
+ .post("/sync/load_changes")
152
+ .set("Cookie", loginCookie)
153
+ .send({
154
+ loadUntil: (await db.time()).valueOf(),
155
+ syncInfos: {
156
+ books: {
157
+ maxLoadedId: 0,
158
+ syncFrom: dbTime.valueOf(),
96
159
  },
97
- ],
98
- },
99
- })
100
- .expect(respondJsonWith(200, ({ success }) => success));
101
- const patientsAfter = await patients.countRows();
102
- const booksAfter = await books.countRows();
103
- expect(patientsAfter).toBe(patientsBefore + 2);
104
- expect(booksAfter).toBe(booksBefore + 1);
105
- expect((await patients.getRows({ id: 84 })).length).toBe(0);
160
+ },
161
+ });
162
+ expect(resp.status).toBe(200);
163
+ const data = resp._body;
164
+ expect(data.books.rows.length).toBe(2);
165
+ for (const row of data.books.rows) {
166
+ const {
167
+ _sync_info_tbl_ref_,
168
+ _sync_info_tbl_last_modified_,
169
+ _sync_info_tbl_deleted_,
170
+ ...rest
171
+ } = row;
172
+ expect(_sync_info_tbl_ref_).toBe(rest.id);
173
+ const { last_modified } = await books.latestSyncInfo(rest.id);
174
+ expect(_sync_info_tbl_last_modified_).toBe(last_modified.valueOf());
175
+ expect(_sync_info_tbl_deleted_).toBe(false);
176
+ }
177
+ expect(data.books.rows[0].author).toBe("Herman Melville");
178
+ expect(data.books.rows[1].author).toBe("Leo Tolstoy");
179
+ }
180
+ });
181
+ } else
182
+ it("only pq support", () => {
183
+ expect(true).toBe(true);
184
+ });
185
+ });
186
+
187
+ // describe("load remote deletes", () => {});
188
+
189
+ describe("Upload changes", () => {
190
+ const doUpload = async (app, loginCookie, syncTimestamp, changes) => {
191
+ const resp = await request(app)
192
+ .post("/sync/offline_changes")
193
+ .set("Cookie", loginCookie)
194
+ .send({
195
+ syncTimestamp,
196
+ changes,
197
+ });
198
+ return resp;
199
+ };
200
+
201
+ const getResult = async (app, loginCookie, syncDir) => {
202
+ let pollCount = 0;
203
+ while (pollCount < 10) {
204
+ const resp = await request(app)
205
+ .get(`/sync/upload_finished?dir_name=${encodeURIComponent(syncDir)}`)
206
+ .set("Cookie", loginCookie);
207
+ expect(resp.status).toBe(200);
208
+ const { finished, translatedIds, error } = resp._body;
209
+ if (finished) return translatedIds ? translatedIds : error;
210
+ await sleep(1000);
106
211
  }
107
- });
212
+ return null;
213
+ };
214
+
215
+ const cleanSyncDir = async (app, loginCookie, syncDir) => {
216
+ const resp = await request(app)
217
+ .post("/sync/clean_sync_dir")
218
+ .send({ dir_name: syncDir })
219
+ .set("Cookie", loginCookie);
220
+ expect(resp.status).toBe(200);
221
+ };
222
+
223
+ const maxId = async (tblName) => {
224
+ const table = Table.findOne({ name: tblName });
225
+ const pkName = table.pk_name;
226
+ const rows = await table.getRows({}, { orderBy: pkName, orderDesc: true });
227
+ return rows.length === 0 ? 0 : rows[0][pkName];
228
+ };
229
+
230
+ if (!db.isSQLite) {
231
+ beforeAll(async () => {
232
+ await initSyncInfo(["books", "publisher"]);
233
+ });
234
+
235
+ it("inserts with translations", async () => {
236
+ const app = await getApp({ disableCsrf: true });
237
+ const loginCookie = await getAdminLoginCookie();
238
+ const resp = await doUpload(app, loginCookie, new Date().valueOf(), {
239
+ books: {
240
+ inserts: [
241
+ {
242
+ id: 1,
243
+ author: "app agi",
244
+ pages: 1,
245
+ publisher: 1,
246
+ },
247
+ ],
248
+ },
249
+ publisher: {
250
+ inserts: [
251
+ {
252
+ id: 1,
253
+ name: "agi",
254
+ },
255
+ ],
256
+ },
257
+ });
258
+ expect(resp.status).toBe(200);
259
+ const { syncDir } = resp._body;
260
+ const translatedIds = await getResult(app, loginCookie, syncDir);
261
+ await cleanSyncDir(app, loginCookie, syncDir);
262
+ expect(translatedIds).toBeDefined();
263
+ expect(translatedIds).toEqual({
264
+ books: {
265
+ 1: 3,
266
+ },
267
+ publisher: {
268
+ 1: 3,
269
+ },
270
+ });
271
+ });
272
+
273
+ it("update with translation", async () => {
274
+ const app = await getApp({ disableCsrf: true });
275
+ const loginCookie = await getAdminLoginCookie();
276
+ const maxPublId = await maxId("publisher");
277
+ const resp = await doUpload(app, loginCookie, new Date().valueOf(), {
278
+ books: {
279
+ updates: [
280
+ {
281
+ id: 1,
282
+ publisher: 1,
283
+ },
284
+ ],
285
+ },
286
+ publisher: {
287
+ inserts: [
288
+ {
289
+ id: 1,
290
+ name: "my_agi",
291
+ },
292
+ ],
293
+ },
294
+ });
295
+ expect(resp.status).toBe(200);
296
+ const { syncDir } = resp._body;
297
+ const translatedIds = await getResult(app, loginCookie, syncDir);
298
+ await cleanSyncDir(app, loginCookie, syncDir);
299
+ expect(translatedIds).toBeDefined();
300
+ expect(translatedIds).toEqual({
301
+ publisher: {
302
+ 1: maxPublId + 1,
303
+ },
304
+ });
305
+ // TODO check update conflics on book
306
+ });
307
+
308
+ it("deletes normal", async () => {
309
+ const books = Table.findOne({ name: "books" });
310
+ const syncTimeStamp = new Date();
311
+ const oldRows = await books.getRows();
312
+ const newId = await books.insertRow(
313
+ { author: "agi", pages: 22 },
314
+ null,
315
+ null,
316
+ false,
317
+ syncTimeStamp
318
+ );
319
+ const newRows = await books.getRows();
320
+ expect(newRows.length).toBe(oldRows.length + 1);
321
+ const app = await getApp({ disableCsrf: true });
322
+ const loginCookie = await getAdminLoginCookie();
323
+ const resp = await doUpload(app, loginCookie, new Date().valueOf(), {
324
+ books: {
325
+ deletes: [
326
+ {
327
+ id: newId,
328
+ last_modified: syncTimeStamp.valueOf(),
329
+ },
330
+ ],
331
+ },
332
+ });
333
+ expect(resp.status).toBe(200);
334
+ const { syncDir } = resp._body;
335
+ const translatedIds = await getResult(app, loginCookie, syncDir);
336
+ await cleanSyncDir(app, loginCookie, syncDir);
337
+ expect(translatedIds).toBeDefined();
338
+ const afterDelete = await books.getRows();
339
+ expect(afterDelete.length).toBe(oldRows.length);
340
+ });
341
+
342
+ // skip delete because of larger last_modified on the server side
343
+ it("deletes with conflicts", async () => {
344
+ const books = Table.findOne({ name: "books" });
345
+ const syncTimeStamp = new Date();
346
+ const oldRows = await books.getRows();
347
+ const newId = await books.insertRow(
348
+ { author: "my_agi", pages: 22 },
349
+ null,
350
+ null,
351
+ false,
352
+ syncTimeStamp
353
+ );
354
+ await books.updateRow(
355
+ { pages: 11 },
356
+ newId,
357
+ undefined,
358
+ undefined,
359
+ undefined,
360
+ undefined,
361
+ new Date(syncTimeStamp.valueOf() + 1)
362
+ );
363
+ const newRows = await books.getRows();
364
+ expect(newRows.length).toBe(oldRows.length + 1);
365
+ const app = await getApp({ disableCsrf: true });
366
+ const loginCookie = await getAdminLoginCookie();
367
+ const resp = await doUpload(app, loginCookie, new Date().valueOf(), {
368
+ books: {
369
+ deletes: [
370
+ {
371
+ id: newId,
372
+ last_modified: syncTimeStamp.valueOf(),
373
+ },
374
+ ],
375
+ },
376
+ });
377
+ expect(resp.status).toBe(200);
378
+ const { syncDir } = resp._body;
379
+ const translatedIds = await getResult(app, loginCookie, syncDir);
380
+ await cleanSyncDir(app, loginCookie, syncDir);
381
+ expect(translatedIds).toBeDefined();
382
+ const afterDelete = await books.getRows();
383
+ expect(afterDelete.length).toBe(newRows.length);
384
+ });
385
+
386
+ it("insert not authorized", async () => {
387
+ const books = Table.findOne({ name: "books" });
388
+ books.min_role_write = 1;
389
+ await books.update(books);
390
+ const app = await getApp({ disableCsrf: true });
391
+ const loginCookie = await getUserLoginCookie();
392
+ const resp = await doUpload(app, loginCookie, new Date().valueOf(), {
393
+ books: {
394
+ inserts: [
395
+ {
396
+ id: 1,
397
+ author: "app agi",
398
+ pages: 1,
399
+ publisher: 1,
400
+ },
401
+ ],
402
+ },
403
+ publisher: {
404
+ inserts: [
405
+ {
406
+ id: 1,
407
+ name: "agi",
408
+ },
409
+ ],
410
+ },
411
+ });
412
+ expect(resp.status).toBe(200);
413
+ const { syncDir } = resp._body;
414
+ const error = await getResult(app, loginCookie, syncDir);
415
+ await cleanSyncDir(app, loginCookie, syncDir);
416
+ expect(error).toBeDefined();
417
+ expect(error).toEqual({ message: "Unable to insert into books" });
418
+ books.min_role_write = 100;
419
+ await books.update(books);
420
+ });
421
+
422
+ it("update not authorized", async () => {
423
+ const books = Table.findOne({ name: "books" });
424
+ books.min_role_write = 1;
425
+ await books.update(books);
426
+
427
+ const app = await getApp({ disableCsrf: true });
428
+ const loginCookie = await getUserLoginCookie();
429
+ const resp = await doUpload(app, loginCookie, new Date().valueOf(), {
430
+ books: {
431
+ updates: [
432
+ {
433
+ id: 1,
434
+ pages: 1,
435
+ },
436
+ ],
437
+ },
438
+ });
439
+ expect(resp.status).toBe(200);
440
+ const { syncDir } = resp._body;
441
+ const error = await getResult(app, loginCookie, syncDir);
442
+ await cleanSyncDir(app, loginCookie, syncDir);
443
+ expect(error).toBeDefined();
444
+ expect(error).toEqual({
445
+ message: "Unable to update books: Not authorized",
446
+ });
447
+ books.min_role_write = 100;
448
+ await books.update(books);
449
+ });
450
+
451
+ it("delete not authorized", async () => {
452
+ const syncTimeStamp = new Date();
453
+ const books = Table.findOne({ name: "books" });
454
+ books.min_role_write = 1;
455
+ await books.update(books);
456
+
457
+ const app = await getApp({ disableCsrf: true });
458
+ const loginCookie = await getUserLoginCookie();
459
+ const resp = await doUpload(app, loginCookie, syncTimeStamp.valueOf(), {
460
+ books: {
461
+ deletes: [
462
+ {
463
+ id: 1,
464
+ last_modified: syncTimeStamp.valueOf() + 1,
465
+ },
466
+ ],
467
+ },
468
+ });
469
+ expect(resp.status).toBe(200);
470
+ const { syncDir } = resp._body;
471
+ const error = await getResult(app, loginCookie, syncDir);
472
+ await cleanSyncDir(app, loginCookie, syncDir);
473
+ expect(error).toBeDefined();
474
+ expect(error).toEqual({
475
+ message: "Unable to delete in 'books': Some rows were not deleted",
476
+ });
477
+ books.min_role_write = 100;
478
+ await books.update(books);
479
+ });
480
+ } else
481
+ it("only pq support", () => {
482
+ expect(true).toBe(true);
483
+ });
108
484
  });
@@ -24,6 +24,8 @@ beforeAll(async () => {
24
24
  await resetToFixtures();
25
25
  });
26
26
 
27
+ jest.setTimeout(30000);
28
+
27
29
  describe("view list endpoint", () => {
28
30
  it("should show view to unauth", async () => {
29
31
  const app = await getApp({ disableCsrf: true });
@@ -19,6 +19,8 @@ beforeAll(async () => {
19
19
  });
20
20
  afterAll(db.close);
21
21
 
22
+ jest.setTimeout(30000);
23
+
22
24
  describe("viewedit list endpoint", () => {
23
25
  itShouldRedirectUnauthToLogin("/viewedit");
24
26
 
package/wrapper.js CHANGED
@@ -312,7 +312,8 @@ module.exports = (version_tag) =>
312
312
 
313
313
  if (req.xhr) {
314
314
  const renderToHtml = layout.renderBody
315
- ? (h, role) => layout.renderBody({ title, body: h, role, alerts })
315
+ ? (h, role, req) =>
316
+ layout.renderBody({ title, body: h, role, alerts, req })
316
317
  : defaultRenderToHtml;
317
318
  res.header(
318
319
  "Cache-Control",
@@ -322,8 +323,8 @@ module.exports = (version_tag) =>
322
323
  res.set("Page-Title", encodeURIComponent(title));
323
324
  res.send(
324
325
  html.length === 1
325
- ? renderToHtml(html[0], role)
326
- : html.map((h) => renderToHtml(h, role)).join("")
326
+ ? renderToHtml(html[0], role, req)
327
+ : html.map((h) => renderToHtml(h, role, req)).join("")
327
328
  );
328
329
  return;
329
330
  }
@@ -356,11 +357,12 @@ module.exports = (version_tag) =>
356
357
  * @param role
357
358
  * @returns {string|string|*}
358
359
  */
359
- const defaultRenderToHtml = (s, role) =>
360
+ const defaultRenderToHtml = (s, role, req) =>
360
361
  typeof s === "string"
361
362
  ? s
362
363
  : renderLayout({
363
364
  blockDispatch: {},
364
365
  role,
366
+ req,
365
367
  layout: s,
366
368
  });