@saltcorn/server 0.9.0-beta.7 → 0.9.0-beta.9

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.
@@ -0,0 +1,9 @@
1
+ The action chosen will determine what happens when the trigger occurs.
2
+ After you have chosen an action, there will be further configuration of that action.
3
+
4
+ The available actions, some of which are only accessible when the trigger is
5
+ table-related, are:
6
+
7
+ {{# for (const [name, action] of Object.entries(scState.actions)) { }}
8
+ * `{{name}}`: {{action.description||""}}
9
+ {{# } }}
@@ -0,0 +1,161 @@
1
+ Here you can enter the code that needs to run when this trigger occurs.
2
+ The action can manipulate rows in the database, manipulate files, interact
3
+ with remote APIs, or issue directives for the user's display.
4
+
5
+ Your code can use await at the top level, and should do so whenever calling
6
+ database queries or other aynchronous code (see example below)
7
+
8
+ The variable `table` is the associated table (if any; note lowercase). If you want to access a different table,
9
+ use the `Table` variable (note uppercase) to access the Table class of tables (see
10
+ [documentation for Table class](https://saltcorn.github.io/saltcorn/classes/_saltcorn_data.models.Table-1.html))
11
+
12
+ Example:
13
+
14
+ ```
15
+ await table.insertRow({name: "Alex", age: 43})
16
+ const otherTable = Table.findOne({name: "Orders"})
17
+ await otherTable.deleteRows({id: order})
18
+ ```
19
+
20
+ In addition to `table` and `Table`, you can use other functions/variables:
21
+
22
+ #### `console`
23
+
24
+ Use this to print to the terminal.
25
+
26
+ Example: `console.log("Hello world")`
27
+
28
+ #### `Actions`
29
+
30
+ Use `Actions.{ACTION NAME}` to run an action.
31
+
32
+ Your available action names are: {{ Object.keys(scState.actions).join(", ") }}
33
+
34
+ Example:
35
+
36
+ ```
37
+ await Actions.set_user_language({language: "fr"})
38
+ ```
39
+
40
+ #### `sleep`
41
+
42
+ A small utility function to sleep for certain number of milliseconds. Use this with await
43
+
44
+ Example: `await sleep(1000)`
45
+
46
+ #### `require`
47
+
48
+ Use require to access NPM packages listed under your [Development settings](/admin/dev)
49
+
50
+ Example: `const _ = require("underscore")`
51
+
52
+ #### `fetch` and `fetchJSON`
53
+
54
+ Use these to make HTTP API calls. `fetch` is the standard JavaScript `fetch` (provided by
55
+ [node-fetch](https://www.npmjs.com/package/node-fetch#common-usage)). `fetchJSON` performs a fetch
56
+ and then reads its reponse to JSON
57
+
58
+ Example:
59
+
60
+ ```
61
+ const response = await fetch('https://api.github.com/users/github');
62
+ const data = await response.json();
63
+ ```
64
+
65
+ which is the same as
66
+
67
+ ```
68
+ const data = await fetchJSON('https://api.github.com/users/github');
69
+ ```
70
+
71
+ ## Return directives
72
+
73
+ Your code can with its return value give directives to the current page.
74
+ Valid return values are:
75
+
76
+ #### `notify`
77
+
78
+ Send a pop-up notification indicating success to the user
79
+
80
+ Example: `return { notify: "Order completed!" }`
81
+
82
+ #### `error`
83
+
84
+ Send a pop-up notification indicating error to the user.
85
+
86
+ Example: `return { error: "Invalid command!" }`
87
+
88
+ If this is triggered by an Edit view with the SubmitWithAjax,
89
+ halt navigation and stay on page. This can be used for complex validation logic,
90
+ When added as an Insert or Update trigger. If you delete the inserted row, You
91
+ may also need to clear the returned id in order to allow the user to continue editing.
92
+
93
+ Example:
94
+
95
+ ```
96
+ if(amount>cash_on_hand) {
97
+ await table.deleteRows({ id })
98
+ return {
99
+ error: "Invalid order!",
100
+ id: null
101
+ }
102
+ }
103
+ ```
104
+
105
+ #### `goto`
106
+
107
+ Navigate to a different URL:
108
+
109
+ Example: `return { goto: "https://saltcorn.com" }`
110
+
111
+ Add `target: "_blank"` to open in a new tab.
112
+
113
+ #### `reload_page`
114
+
115
+ Request a page reload with the existing URL.
116
+
117
+ Example: `return { reload_page: true }`
118
+
119
+ #### `popup`
120
+
121
+ Open a URL in a popup:
122
+
123
+ Example:
124
+
125
+ ```
126
+ return { popup: `/view/Orders?id=${parent}` }
127
+ ```
128
+
129
+ #### `download`
130
+
131
+ Download a file to the client browser.
132
+
133
+ Example:
134
+
135
+ ```
136
+ return { download: {
137
+ mimetype: "text/csv",
138
+ blob: filecontents
139
+ }
140
+ }
141
+ ```
142
+
143
+ #### `set_fields`
144
+
145
+ If triggered from an edit view, set fields dynamically in the form. The
146
+ value should be an object with keys that are field variable names.
147
+
148
+ Example:
149
+
150
+ ```
151
+ return { set_fields: {
152
+ zidentifier: `${name.toUpperCase()}-${id}`
153
+ }
154
+ }
155
+ ```
156
+
157
+ #### `eval_js`
158
+
159
+ Execute JavaScript in the browser.
160
+
161
+ Example: `return { eval_js: 'alert("Hello world")' }`
package/package.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "@saltcorn/server",
3
- "version": "0.9.0-beta.7",
3
+ "version": "0.9.0-beta.9",
4
4
  "description": "Server app for Saltcorn, open-source no-code platform",
5
5
  "homepage": "https://saltcorn.com",
6
6
  "main": "index.js",
7
7
  "license": "MIT",
8
8
  "dependencies": {
9
- "@saltcorn/base-plugin": "0.9.0-beta.7",
10
- "@saltcorn/builder": "0.9.0-beta.7",
11
- "@saltcorn/data": "0.9.0-beta.7",
12
- "@saltcorn/admin-models": "0.9.0-beta.7",
13
- "@saltcorn/filemanager": "0.9.0-beta.7",
14
- "@saltcorn/markup": "0.9.0-beta.7",
15
- "@saltcorn/sbadmin2": "0.9.0-beta.7",
9
+ "@saltcorn/base-plugin": "0.9.0-beta.9",
10
+ "@saltcorn/builder": "0.9.0-beta.9",
11
+ "@saltcorn/data": "0.9.0-beta.9",
12
+ "@saltcorn/admin-models": "0.9.0-beta.9",
13
+ "@saltcorn/filemanager": "0.9.0-beta.9",
14
+ "@saltcorn/markup": "0.9.0-beta.9",
15
+ "@saltcorn/sbadmin2": "0.9.0-beta.9",
16
16
  "@socket.io/cluster-adapter": "^0.2.1",
17
17
  "@socket.io/sticky": "^1.0.1",
18
18
  "adm-zip": "0.5.10",
@@ -690,10 +690,13 @@ function initialize_page() {
690
690
  setTimeout(() => {
691
691
  codes.forEach((el) => {
692
692
  //console.log($(el).attr("mode"), el);
693
+ if ($(el).hasClass("codemirror-enabled")) return;
694
+
693
695
  const cm = CodeMirror.fromTextArea(el, {
694
696
  lineNumbers: true,
695
697
  mode: $(el).attr("mode"),
696
698
  });
699
+ $(el).addClass("codemirror-enabled");
697
700
  cm.on(
698
701
  "change",
699
702
  $.debounce(() => {
@@ -1061,6 +1064,7 @@ function common_done(res, viewname, isWeb = true) {
1061
1064
  if (
1062
1065
  prev.origin === next.origin &&
1063
1066
  prev.pathname === next.pathname &&
1067
+ prev.searchParams.toString() === next.searchParams.toString() &&
1064
1068
  next.hash !== prev.hash
1065
1069
  )
1066
1070
  location.reload();
package/routes/actions.js CHANGED
@@ -174,8 +174,9 @@ const triggerForm = async (req, trigger) => {
174
174
  attributes: {
175
175
  explainers: {
176
176
  Often: req.__("Every 5 minutes"),
177
- Never:
178
- req.__("Not scheduled but can be run as an action from a button click"),
177
+ Never: req.__(
178
+ "Not scheduled but can be run as an action from a button click"
179
+ ),
179
180
  },
180
181
  },
181
182
  },
@@ -201,6 +202,7 @@ const triggerForm = async (req, trigger) => {
201
202
  label: req.__("Action"),
202
203
  type: "String",
203
204
  required: true,
205
+ help: { topic: "Actions" },
204
206
  attributes: {
205
207
  calcOptions: ["when_trigger", action_options],
206
208
  },
@@ -402,7 +404,7 @@ router.get(
402
404
  form.values = trigger.configuration;
403
405
  const events = Trigger.when_options;
404
406
  const actions = Trigger.find({
405
- when_trigger: {or: ["API call", "Never"]},
407
+ when_trigger: { or: ["API call", "Never"] },
406
408
  });
407
409
  const tables = (await Table.find({})).map((t) => ({
408
410
  name: t.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"))
@@ -13,6 +13,7 @@ const db = require("@saltcorn/data/db");
13
13
  const { sleep } = require("@saltcorn/data/tests/mocks");
14
14
 
15
15
  const Table = require("@saltcorn/data/models/table");
16
+ const TableConstraint = require("@saltcorn/data/models/table_constraints");
16
17
  const Field = require("@saltcorn/data/models/field");
17
18
  const User = require("@saltcorn/data/models/user");
18
19
 
@@ -349,8 +350,9 @@ describe("Upload changes", () => {
349
350
  .get(`/sync/upload_finished?dir_name=${encodeURIComponent(syncDir)}`)
350
351
  .set("Cookie", loginCookie);
351
352
  expect(resp.status).toBe(200);
352
- const { finished, translatedIds, error } = resp._body;
353
- if (finished) return translatedIds ? translatedIds : error;
353
+ const { finished, translatedIds, uniqueConflicts, error } = resp._body;
354
+ if (finished)
355
+ return translatedIds ? { translatedIds, uniqueConflicts } : error;
354
356
  await sleep(1000);
355
357
  }
356
358
  return null;
@@ -401,7 +403,7 @@ describe("Upload changes", () => {
401
403
  });
402
404
  expect(resp.status).toBe(200);
403
405
  const { syncDir } = resp._body;
404
- const translatedIds = await getResult(app, loginCookie, syncDir);
406
+ const { translatedIds } = await getResult(app, loginCookie, syncDir);
405
407
  await cleanSyncDir(app, loginCookie, syncDir);
406
408
  expect(translatedIds).toBeDefined();
407
409
  expect(translatedIds).toEqual({
@@ -414,6 +416,89 @@ describe("Upload changes", () => {
414
416
  });
415
417
  });
416
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
+
417
502
  it("update with translation", async () => {
418
503
  const app = await getApp({ disableCsrf: true });
419
504
  const loginCookie = await getAdminLoginCookie();
@@ -438,7 +523,7 @@ describe("Upload changes", () => {
438
523
  });
439
524
  expect(resp.status).toBe(200);
440
525
  const { syncDir } = resp._body;
441
- const translatedIds = await getResult(app, loginCookie, syncDir);
526
+ const { translatedIds } = await getResult(app, loginCookie, syncDir);
442
527
  await cleanSyncDir(app, loginCookie, syncDir);
443
528
  expect(translatedIds).toBeDefined();
444
529
  expect(translatedIds).toEqual({
@@ -476,7 +561,7 @@ describe("Upload changes", () => {
476
561
  });
477
562
  expect(resp.status).toBe(200);
478
563
  const { syncDir } = resp._body;
479
- const translatedIds = await getResult(app, loginCookie, syncDir);
564
+ const { translatedIds } = await getResult(app, loginCookie, syncDir);
480
565
  await cleanSyncDir(app, loginCookie, syncDir);
481
566
  expect(translatedIds).toBeDefined();
482
567
  const afterDelete = await books.getRows();
@@ -520,7 +605,7 @@ describe("Upload changes", () => {
520
605
  });
521
606
  expect(resp.status).toBe(200);
522
607
  const { syncDir } = resp._body;
523
- const translatedIds = await getResult(app, loginCookie, syncDir);
608
+ const { translatedIds } = await getResult(app, loginCookie, syncDir);
524
609
  await cleanSyncDir(app, loginCookie, syncDir);
525
610
  expect(translatedIds).toBeDefined();
526
611
  const afterDelete = await books.getRows();