@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/app.js CHANGED
@@ -69,6 +69,39 @@ const disabledCsurf = (req, res, next) => {
69
69
  next();
70
70
  };
71
71
 
72
+ const noCsrfLookup = (state) => {
73
+ if (!state.plugin_routes) return null;
74
+ else {
75
+ const result = new Set();
76
+ for (const [plugin, routes] of Object.entries(state.plugin_routes)) {
77
+ for (const url of routes
78
+ .filter((r) => r.noCsrf === true)
79
+ .map((r) => r.url)) {
80
+ result.add(url);
81
+ }
82
+ }
83
+ return result;
84
+ }
85
+ };
86
+
87
+ const prepPluginRouter = (pluginRoutes) => {
88
+ const router = express.Router();
89
+ for (const [plugin, routes] of Object.entries(pluginRoutes)) {
90
+ for (const route of routes) {
91
+ switch (route.method) {
92
+ case "post":
93
+ router.post(route.url, error_catcher(route.callback));
94
+ break;
95
+ case "get":
96
+ default:
97
+ router.get(route.url, error_catcher(route.callback));
98
+ break;
99
+ }
100
+ }
101
+ }
102
+ return router;
103
+ };
104
+
72
105
  // todo console.log app instance info when app stxarts - avoid to show secrets (password, etc)
73
106
 
74
107
  /**
@@ -95,17 +128,23 @@ const getApp = async (opts = {}) => {
95
128
  app.use(helmet());
96
129
  // TODO ch find a better solution
97
130
  app.use(cors());
131
+ const bodyLimit = getState().getConfig("body_limit");
98
132
  app.use(
99
133
  express.json({
100
- limit: "5mb",
134
+ limit: bodyLimit ? `${bodyLimit}kb` : "5mb",
101
135
  verify: (req, res, buf) => {
102
136
  req.rawBody = buf;
103
137
  },
104
138
  })
105
139
  );
106
- // extenetede url encoding in use
140
+ const urlencodedLimit = getState().getConfig("url_encoded_limit");
141
+ // extended url encoding in use
107
142
  app.use(
108
- express.urlencoded({ limit: "5mb", extended: true, parameterLimit: 50000 })
143
+ express.urlencoded({
144
+ limit: urlencodedLimit ? `${urlencodedLimit}kb` : "5mb",
145
+ extended: true,
146
+ parameterLimit: 50000,
147
+ })
109
148
  );
110
149
 
111
150
  // cookies
@@ -300,21 +339,34 @@ const getApp = async (opts = {}) => {
300
339
  app.use("/scapi", scapi);
301
340
 
302
341
  const csurf = csrf();
303
- if (!opts.disableCsrf)
342
+ let noCsrf = null;
343
+ if (!opts.disableCsrf) {
344
+ noCsrf = noCsrfLookup(getState());
304
345
  app.use(function (req, res, next) {
305
346
  if (
347
+ noCsrf?.has(req.url) ||
306
348
  (req.smr &&
307
349
  (req.url.startsWith("/api/") ||
308
350
  req.url === "/auth/login-with/jwt" ||
309
351
  req.url === "/auth/signup")) ||
310
- jwt_extractor(req)
352
+ jwt_extractor(req) ||
353
+ req.url === "/auth/callback/saml"
311
354
  )
312
355
  return disabledCsurf(req, res, next);
313
356
  csurf(req, res, next);
314
357
  });
315
- else app.use(disabledCsurf);
358
+ } else app.use(disabledCsurf);
316
359
 
317
360
  mountRoutes(app);
361
+ // mount plugin router with a callback for changes
362
+ let pluginRouter = prepPluginRouter(getState().plugin_routes || {});
363
+ getState().routesChangedCb = () => {
364
+ pluginRouter = prepPluginRouter(getState().plugin_routes || {});
365
+ noCsrf = noCsrfLookup(getState());
366
+ };
367
+ app.use((req, res, next) => {
368
+ pluginRouter(req, res, next);
369
+ });
318
370
  // set tenant homepage as / root
319
371
  app.get("/", error_catcher(homepage));
320
372
  // /robots.txt
package/auth/routes.js CHANGED
@@ -228,7 +228,7 @@ const loginWithJwt = async (email, password, saltcornApp, res, req) => {
228
228
  const loginFn = async () => {
229
229
  const publicUserLink = getState().getConfig("public_user_link");
230
230
  const jwt_secret = db.connectObj.jwt_secret;
231
- if (email && password) {
231
+ if (email !== undefined && password !== undefined) {
232
232
  // with credentials
233
233
  const user = await User.findOne({ email });
234
234
  if (user && user.checkPassword(password)) {
@@ -1202,25 +1202,21 @@ const loginCallback = (req, res) => () => {
1202
1202
  }
1203
1203
  };
1204
1204
 
1205
- /**
1206
- * @name get/callback/:method
1207
- * @function
1208
- * @memberof module:auth/routes~routesRouter
1209
- */
1210
- router.get(
1211
- "/callback/:method",
1212
- error_catcher(async (req, res, next) => {
1213
- const { method } = req.params;
1214
- const auth = getState().auth_methods[method];
1215
- if (auth) {
1216
- passport.authenticate(method, { failureRedirect: "/auth/login" })(
1217
- req,
1218
- res,
1219
- loginCallback(req, res)
1220
- );
1221
- }
1222
- })
1223
- );
1205
+ const callbackFn = async (req, res, next) => {
1206
+ const { method } = req.params;
1207
+ const auth = getState().auth_methods[method];
1208
+ if (auth) {
1209
+ passport.authenticate(method, { failureRedirect: "/auth/login" })(
1210
+ req,
1211
+ res,
1212
+ loginCallback(req, res)
1213
+ );
1214
+ }
1215
+ };
1216
+
1217
+ router.get("/callback/:method", error_catcher(callbackFn));
1218
+
1219
+ router.post("/callback/:method", error_catcher(callbackFn));
1224
1220
 
1225
1221
  /**
1226
1222
  * @param {object} req
package/errors.js CHANGED
@@ -46,10 +46,10 @@ module.exports =
46
46
  ? text(err.message)
47
47
  : req.__("An error occurred")
48
48
  );
49
- } else
50
- res
51
- .status(code)
52
- .sendWrap(
49
+ } else {
50
+ const _res = res.status(code);
51
+ if (_res.sendWrap)
52
+ _res.sendWrap(
53
53
  req.__(headline),
54
54
  devmode ? pre(text(err.stack)) : h3(req.__(headline)),
55
55
  role === 1 && !devmode ? pre(text(err.message)) : "",
@@ -61,4 +61,15 @@ module.exports =
61
61
  )
62
62
  : ""
63
63
  );
64
+ else
65
+ _res.send(
66
+ `<h2>${
67
+ err.message
68
+ ? err.message
69
+ : req.__
70
+ ? req.__("An error occurred")
71
+ : "An error occurred"
72
+ }</h2>`
73
+ );
74
+ }
64
75
  };
@@ -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,62 @@
1
+ {{# const srcTable = Table.findOne({ name: query.srcTable }) }}
2
+ {{# const view = View.findOne({ name: query.view_name }) }}
3
+ {{# const destTable = Table.findOne({ id: view.table_id }) }}
4
+
5
+ The extra state formula when embedding or linking to a view is used
6
+ to influence the row that will be included in that views display,
7
+ in addition to any selection based on a chosen relationship.
8
+
9
+ The formula takes the form of a JavaScript object where the object
10
+ keys are fields (variable names) on the table of the embedded view,
11
+ and the values are the JavaScript expressions for those fields which set the criteria
12
+ for the rows to be included. For example, if you would like to display
13
+ all rows where the Active field is true you would use this as the extra
14
+ state formula: `{ active: true }`. This is an object where the only
15
+ key is `active` and its value is `true`. You can have more than one
16
+ key, for example:
17
+
18
+ ```
19
+ { active: true, age: {gt: 21}, manager: user.id }
20
+ ```
21
+ (See below for using the `user` variable and greater-than filters)
22
+
23
+ The keys you can use are the variable names on the {{destTable.name}} table:
24
+
25
+ | Field | Variable name | Type |
26
+ | ----- | ------------- | ---- |
27
+ {{# for (const field of destTable.fields) { }} | {{ field.label }} | `{{ field.name }}` | {{ field.pretty_type }} |
28
+ {{# } }}
29
+
30
+ In the value expressions (to the right of the colon) you can use
31
+ literals (e.g. 17 for numbers or true for booleans; string literals
32
+ must be enclosed in quotes (single, double or
33
+ [backtick](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals))). 
34
+ You can also as the value use a more complex JavaScript expression.
35
+
36
+ You can refer to the logged in user using the variable name user.
37
+ This is an object and you must use the dot to access user fields,
38
+ e.g. user.id for the logged in users id. A very common scenario
39
+ is to restrict rows to show to those that belong to the current user.
40
+ If you have a key to users field called `theUser`, you would use this
41
+ as the extra state formula: `{theUser: user.id}`
42
+
43
+ The user object has other fields than the primary key identifier.
44
+ For your login session the user object is:
45
+
46
+ ```
47
+ {{JSON.stringify(user, null, 2)}}
48
+ ```
49
+ {{# const userkey = oneOf(Object.keys(user)) }}
50
+
51
+ For example to use the `{{userkey}}` field on the user object, write `user.{{userkey}}`
52
+ {{# if(srcTable) { }}
53
+ You can also use variables from the currently displayed row in the value expressions,
54
+ in this case the table {{srcTable.name}}:
55
+
56
+ | Field | Variable name | Type |
57
+ | ----- | ------------- | ---- |
58
+ {{# for (const field of srcTable.fields) { }} | {{ field.label }} | `{{ field.name }}` | {{ field.pretty_type }} |
59
+ {{# } }}
60
+
61
+ {{# } }}
62
+
@@ -0,0 +1,22 @@
1
+ {{# const table = Table.findOne({ name: query.table_name }) }}
2
+ {{# const mode = query.mode }}
3
+ {{# const field = table.getField(query.field_name) }}
4
+ {{# const fvsObj = field.type === "Key" ? scState.keyFieldviews : field.type === "File" ? scState.fileviews : field.type?.fieldviews || {} }}
5
+ {{# const allFVs = Object.entries(fvsObj) }}
6
+ {{# const myFVs = mode==='filter' ? allFVs.filter(([nm,fv])=>fv.isFilter || fv.isEdit) : mode==='edit' ? allFVs.filter(([nm,fv])=>!fv.isFilter || fv.isEdit) : mode==='show'||mode==='list' ? allFVs.filter(([nm,fv])=>!fv.isFilter && !fv.isEdit) : allFVs }}
7
+
8
+ The field view determines how a field value is shown to the user and how the
9
+ user can interact with (or edit, in Edit views) that value. The available
10
+ field views are determined by the field type and the view pattern.
11
+
12
+ Some field views have configuration options which can be used to further
13
+ customize how the value is displayed or how the user interacts with it.
14
+ This configuration options will appear in the field settings below the
15
+ field view choice.
16
+
17
+ In the table {{ table.name }} the field {{ field.label }} of type
18
+ {{ field.pretty_type }} has the following available field views:
19
+
20
+ {{# for (const [name, fv] of myFVs) { }}
21
+ * {{name}}{{mode==='edit' && !fv.isEdit? ` [Not editable]`:''}}: {{fv.description||undefined}}
22
+ {{# } }}
@@ -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")' }`
@@ -1,3 +1,5 @@
1
+ {{# const table = Table.findOne({ name: query.table }) }}
2
+
1
3
  The constraint formula should be a JavaScript expression which evaluates to a
2
4
  boolean (true or false). If the formula evaluates to false then the row is not
3
5
  valid and will not be accepted in the table. If you are editing an existing row
@@ -6,14 +8,14 @@ to the user. 
6
8
 
7
9
  Some examples:
8
10
 
9
- {{# for (const fml of table.getFormulaExamples("Bool")) { }} * `{{= fml}}`
11
+ {{# for (const fml of table.getFormulaExamples("Bool")) { }} * `{{ fml}}`
10
12
  {{# } }}
11
13
 
12
- This formula can use any of the fields in table {{= table.name }} as variables:
14
+ This formula can use any of the fields in table {{ table.name }} as variables:
13
15
 
14
16
  | Field | Variable name | Type |
15
17
  | ----- | ------------- | ---- |
16
- {{# for (const field of table.fields) { }} | {{= field.label }} | `{{= field.name }}` | {{= field.pretty_type }} |
18
+ {{# for (const field of table.fields) { }} | {{ field.label }} | `{{ field.name }}` | {{ field.pretty_type }} |
17
19
  {{# } }}
18
20
 
19
21
 
@@ -32,7 +34,7 @@ The first-order join fields you can use in the constraint formula are:
32
34
 
33
35
  {{# for (const field of table.fields.filter(f=>f.is_fkey && f.reftable_name)) { }}
34
36
  {{# const reftable = Table.findOne( field.reftable_name); }}
35
- * Through {{=field.label}} key field: {{= table.fields.map(jf=>`\`${field.name}.${jf.name}\``).join(", ") }}
37
+ * Through {{field.label}} key field: {{ reftable.fields.map(jf=>`\`${field.name}.${jf.name}\``).join(", ") }}
36
38
 
37
39
 
38
40
  {{# } }}
@@ -0,0 +1,35 @@
1
+ The view pattern is a fundamental concept in Saltcorn that determines how
2
+ the view will relate to the database table and the overall behavior of the
3
+ view. A view will always follow one specific view pattern, and will require
4
+ further configuration according to which view pattern is chosen. This
5
+ configuration could be as simple as making a few choices, or it could be as
6
+ complicated as specifying a layout using drag and drop. In addition most view
7
+ patterns require choosing a single table, which it can use as a source or
8
+ destination of data, although in many cases data from other tables can be
9
+ brought in using table relationships.
10
+
11
+ There are fundamental view patterns in Saltcorn that you will use in
12
+ every application:
13
+
14
+ * A List view shows a tabular grid of multiple rows in a table. You can define
15
+ its columns which can be data from this or related tables, action buttons or
16
+ links to other views.
17
+
18
+ * An Edit view is a form that can be used to create a new row or edit an existing
19
+ row in a table.
20
+
21
+ * A Show view shows a single existing row in the table. The row must be specified
22
+ either by linking or embedding from another view or page.
23
+
24
+ * A Filter view does not display any data from the table but can be used to set
25
+ up user interface elements that determine which rows are shown in other views
26
+ on the same page. 
27
+
28
+ * A Feed view is configured by another view which shows a single row and will
29
+ repeat this view for all available rows.
30
+
31
+ The additional view patterns available in your system are:
32
+ {{# const vts = Object.values(scState.viewtemplates).filter(vt=>!["List","Show","Edit","Filter","Feed",].includes(vt.name))}}
33
+ {{# for (const vt of vts) { }}
34
+ * {{ vt.name }}{{vt.description ? `: ${vt.description}` : ""}}
35
+ {{# } }}
@@ -0,0 +1,30 @@
1
+ {{# const srcTable = Table.findOne({ name: query.table_name }) }}
2
+ {{# const field = srcTable.getField(query.field_name) }}
3
+ {{# const refTable = Table.findOne(field.reftable_name) }}
4
+
5
+ The where formula allows you to restrict the options provided. This can be
6
+ done according to a static criterion, all according to a dynamic criterion
7
+ that depends on the value of another form field. 
8
+
9
+ The formula should take the form of a JavaScript boolean expression such as
10
+ `a === b` or `x>y && w==1` etc.
11
+
12
+ In this expression you can use variables from the target table from which
13
+ you are selecting values, here {{ refTable.name }}. These can accessed by
14
+ their variable nmame. The fields on the table {{ refTable.name }} are:
15
+
16
+ | Field | Variable name | Type |
17
+ | ----- | ------------- | ---- |
18
+ {{# for (const field of refTable.fields) { }} | {{ field.label }} | `{{ field.name }}` | {{ field.pretty_type }} |
19
+ {{# } }}
20
+
21
+ You can also use variables from the current form. In that case the options will
22
+ respond dynamically to the state of the form fields. These form variables must be proceeded by
23
+ a `$`:
24
+
25
+ | Field | Variable name | Type |
26
+ | ----- | ------------- | ---- |
27
+ {{# for (const field of srcTable.fields) { }} | {{ field.label }} | `${{ field.name }}` | {{ field.pretty_type }} |
28
+ {{# } }}
29
+
30
+ You can mix the two types of variables, as in `a == $b`
package/help/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  const Table = require("@saltcorn/data/models/table");
2
+ const View = require("@saltcorn/data/models/view");
2
3
  const File = require("@saltcorn/data/models/file");
3
4
  const _ = require("underscore");
4
5
  const fs = require("fs").promises;
@@ -6,10 +7,12 @@ const MarkdownIt = require("markdown-it"),
6
7
  md = new MarkdownIt();
7
8
 
8
9
  const { pre } = require("@saltcorn/markup/tags");
9
-
10
+ const path = require("path");
11
+ const { getState } = require("@saltcorn/data/db/state");
12
+ const { oneOf } = require("@saltcorn/types/generators");
10
13
  const get_md_file = async (topic) => {
11
14
  try {
12
- const fp = require.resolve(`./${File.normalise(topic)}.tmd`);
15
+ const fp = path.join(__dirname, `${File.normalise(topic)}.tmd`);
13
16
  const fileBuf = await fs.readFile(fp);
14
17
  return fileBuf.toString();
15
18
  } catch (e) {
@@ -17,20 +20,26 @@ const get_md_file = async (topic) => {
17
20
  }
18
21
  };
19
22
 
20
- _.templateSettings = {
21
- evaluate: /\{\{#(.+?)\}\}/g,
22
- interpolate: /\{\{=(.+?)\}\}/g,
23
+ md.renderer.rules.table_open = function (tokens, idx) {
24
+ return '<table class="help-md">';
23
25
  };
24
26
 
25
27
  const get_help_markup = async (topic, query, req) => {
26
28
  try {
27
- const context = { user: req.user, Table };
28
- if (query.table) {
29
- context.table = Table.findOne({ name: query.table });
30
- }
29
+ const context = {
30
+ user: req.user,
31
+ Table,
32
+ View,
33
+ scState: getState(),
34
+ query,
35
+ oneOf,
36
+ };
31
37
  const mdTemplate = await get_md_file(topic);
32
38
  if (!mdTemplate) return { markup: "Topic not found" };
33
- const template = _.template(mdTemplate);
39
+ const template = _.template(mdTemplate, {
40
+ evaluate: /\{\{#(.+?)\}\}/g,
41
+ interpolate: /\{\{([^#].+?)\}\}/g,
42
+ });
34
43
  const mdTopic = template(context);
35
44
  const markup = md.render(mdTopic);
36
45
  return { markup };
package/load_plugins.js CHANGED
@@ -176,9 +176,10 @@ const loadAllPlugins = async () => {
176
176
  * @param plugin
177
177
  * @param force
178
178
  * @param noSignalOrDB
179
+ * @param manager - optional plugin manager
179
180
  * @returns {Promise<void>}
180
181
  */
181
- const loadAndSaveNewPlugin = async (plugin, force, noSignalOrDB) => {
182
+ const loadAndSaveNewPlugin = async (plugin, force, noSignalOrDB, manager) => {
182
183
  const tenants_unsafe_plugins = getRootState().getConfig(
183
184
  "tenants_unsafe_plugins",
184
185
  false
@@ -200,7 +201,8 @@ const loadAndSaveNewPlugin = async (plugin, force, noSignalOrDB) => {
200
201
  }
201
202
  const { version, plugin_module, location } = await requirePlugin(
202
203
  plugin,
203
- force
204
+ force,
205
+ manager,
204
206
  );
205
207
 
206
208
  // install dependecies