@saltcorn/server 0.8.8-beta.0 → 0.8.8-beta.2

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/sync.js CHANGED
@@ -3,83 +3,319 @@ const Router = require("express-promise-router");
3
3
  const db = require("@saltcorn/data/db");
4
4
  const { getState } = require("@saltcorn/data/db/state");
5
5
  const Table = require("@saltcorn/data/models/table");
6
+ const File = require("@saltcorn/data/models/file");
7
+ const { getSafeSaltcornCmd } = require("@saltcorn/data/utils");
8
+ const { spawn, spawnSync } = require("child_process");
9
+ const path = require("path");
10
+ const fs = require("fs").promises;
6
11
 
7
12
  const router = new Router();
8
13
  module.exports = router;
9
14
 
10
- const pickFields = (table, row) => {
11
- const result = {};
12
- const fields = table.getFields();
13
- for (const { name, type, calculated } of table.getFields()) {
14
- if (name === "id" || calculated) continue;
15
- if (type?.name === "Date") {
16
- result[name] = row[name] ? new Date(row[name]) : undefined;
17
- } else {
18
- result[name] = row[name];
15
+ router.get(
16
+ "/sync_timestamp",
17
+ error_catcher(async (req, res) => {
18
+ try {
19
+ res.json({ syncTimestamp: (await db.time()).valueOf() });
20
+ } catch (error) {
21
+ getState().log(2, `GET /sync_timestamp: '${error.message}'`);
22
+ res.status(400).json({ error: error.message || error });
23
+ }
24
+ })
25
+ );
26
+
27
+ const getSyncRows = async (syncInfo, table, syncUntil, client) => {
28
+ const tblName = table.name;
29
+ const pkName = table.pk_name;
30
+ const schema = db.getTenantSchemaPrefix();
31
+ if (!syncInfo.syncFrom) {
32
+ const { rows } = await client.query(
33
+ `select
34
+ info_tbl.ref "_sync_info_tbl_ref_",
35
+ info_tbl.last_modified "_sync_info_tbl_last_modified_",
36
+ info_tbl.deleted "_sync_info_tbl_deleted_",
37
+ data_tbl.*
38
+ from ${schema}"${db.sqlsanitize(
39
+ tblName
40
+ )}_sync_info" "info_tbl" right join "${db.sqlsanitize(
41
+ tblName
42
+ )}" "data_tbl"
43
+ on info_tbl.ref = data_tbl."${db.sqlsanitize(
44
+ pkName
45
+ )}" and info_tbl.deleted = false
46
+ where data_tbl."${db.sqlsanitize(pkName)}" > ${
47
+ syncInfo.maxLoadedId
48
+ } order by data_tbl."${db.sqlsanitize(pkName)}"`
49
+ );
50
+ for (const row of rows) {
51
+ if (row._sync_info_tbl_last_modified_)
52
+ row._sync_info_tbl_last_modified_ =
53
+ row._sync_info_tbl_last_modified_.valueOf();
54
+ else row._sync_info_tbl_last_modified_ = new Date(syncUntil).valueOf();
55
+ row._sync_info_tbl_ref_ = row[pkName];
19
56
  }
57
+ return rows;
58
+ } else {
59
+ const { rows } = await client.query(
60
+ `select
61
+ info_tbl.ref "_sync_info_tbl_ref_",
62
+ info_tbl.last_modified "_sync_info_tbl_last_modified_",
63
+ info_tbl.deleted "_sync_info_tbl_deleted_",
64
+ data_tbl.*
65
+ from ${schema}"${db.sqlsanitize(
66
+ tblName
67
+ )}_sync_info" "info_tbl" join ${schema}"${db.sqlsanitize(
68
+ tblName
69
+ )}" "data_tbl"
70
+ on info_tbl.ref = data_tbl."${db.sqlsanitize(pkName)}"
71
+ where date_trunc('milliseconds', info_tbl.last_modified) > to_timestamp(${
72
+ new Date(syncInfo.syncFrom).valueOf() / 1000.0
73
+ })
74
+ and date_trunc('milliseconds', info_tbl.last_modified) < to_timestamp(${
75
+ new Date(syncUntil).valueOf() / 1000.0
76
+ })
77
+ and info_tbl.deleted = false
78
+ and info_tbl.ref > ${syncInfo.maxLoadedId}
79
+ order by info_tbl.ref`
80
+ );
81
+ for (const row of rows) {
82
+ if (row._sync_info_tbl_last_modified_)
83
+ row._sync_info_tbl_last_modified_ =
84
+ row._sync_info_tbl_last_modified_.valueOf();
85
+ else row._sync_info_tbl_last_modified_ = syncUntil.valueOf();
86
+ }
87
+ return rows;
20
88
  }
21
- return result;
22
89
  };
23
90
 
24
- const allowInsert = (table, user) => {
25
- const role = user?.role_id || 100;
26
- return table.min_role_write >= role;
27
- };
91
+ /*
92
+ load inserts/updates after syncFrom
93
+ If a table has no syncFrom then it's the first sync and we have to send everything
94
+ */
95
+ router.post(
96
+ "/load_changes",
97
+ error_catcher(async (req, res) => {
98
+ const result = {};
99
+ const { syncInfos, loadUntil } = req.body;
100
+ if (!loadUntil) {
101
+ getState().log(2, `POST /load_changes: loadUntil is missing`);
102
+ return res.status(400).json({ error: "loadUntil is missing" });
103
+ }
104
+ if (!syncInfos) {
105
+ getState().log(2, `POST /load_changes: syncInfos is missing`);
106
+ return res.status(400).json({ error: "syncInfos is missing" });
107
+ }
108
+ const role = req.user ? req.user.role_id : 100;
109
+ const client = await db.getClient();
110
+ let rowLimit = 1000;
111
+ try {
112
+ await client.query(`BEGIN`);
113
+ for (const [tblName, syncInfo] of Object.entries(syncInfos)) {
114
+ const table = Table.findOne({ name: tblName });
115
+ if (!table) throw new Error(`The table '${tblName}' does not exists`);
116
+ const pkName = table.pk_name;
117
+ let rows = await getSyncRows(syncInfo, table, loadUntil, client);
118
+ if (role > table.min_role_read) {
119
+ if (
120
+ role === 100 ||
121
+ (!table.ownership_field_id && !table.ownership_formula)
122
+ )
123
+ continue;
124
+ else if (table.ownership_field_id) {
125
+ } else if (table.ownership_formula) {
126
+ rows = rows.filter((row) => table.is_owner(req.user, row));
127
+ }
128
+ }
129
+ if (rows.length > rowLimit) {
130
+ rows.splice(rowLimit);
131
+ }
132
+ rowLimit -= rows.length;
133
+ result[tblName] = {
134
+ rows,
135
+ maxLoadedId: rows.length > 0 ? rows[rows.length - 1][pkName] : 0,
136
+ };
137
+ }
138
+ await client.query("COMMIT");
139
+ res.json(result);
140
+ } catch (error) {
141
+ await client.query("ROLLBACK");
142
+ getState().log(2, `POST /load_changes: '${error.message}'`);
143
+ res.status(400).json({ error: error.message || error });
144
+ } finally {
145
+ client.release(true);
146
+ }
147
+ })
148
+ );
28
149
 
29
- const throwWithCode = (message, code) => {
30
- const err = new Error(message);
31
- err.statusCode = code;
32
- throw err;
150
+ const getDelRows = async (tblName, syncFrom, syncUntil, client) => {
151
+ const schema = db.getTenantSchemaPrefix();
152
+ const dbRes = await client.query(
153
+ `select *
154
+ from (
155
+ select ref, max(last_modified) from ${schema}"${db.sqlsanitize(
156
+ tblName
157
+ )}_sync_info"
158
+ group by ref, deleted having deleted = true) as alias
159
+ where alias.max < to_timestamp(${syncUntil.valueOf() / 1000.0})
160
+ and alias.max > to_timestamp(${syncFrom.valueOf() / 1000.0})`
161
+ );
162
+ for (const row of dbRes.rows) {
163
+ if (row.last_modified) row.last_modified = row.last_modified.valueOf();
164
+ if (row.max) row.max = row.max.valueOf();
165
+ }
166
+ return dbRes.rows;
33
167
  };
34
168
 
35
- /**
36
- * insert the offline data uploaded by the mobile-app
37
- */
169
+ /*
170
+ load deletes after syncFrom
171
+ If a table has no syncFrom then it's the first sync and there is nothing to delete
172
+ */
38
173
  router.post(
39
- "/table_data",
174
+ "/deletes",
40
175
  error_catcher(async (req, res) => {
41
- // TODO sqlite
42
- getState().log(
43
- 4,
44
- `POST /sync/table_data user: '${req.user ? req.user.id : "public"}'`
45
- );
46
- let aborted = false;
47
- req.socket.on("close", () => {
48
- aborted = true;
49
- });
50
- req.socket.on("timeout", () => {
51
- aborted = true;
52
- });
53
- const client = db.isSQLite ? db : await db.getClient();
176
+ const { syncInfos, syncTimestamp } = req.body;
177
+ const client = await db.getClient();
54
178
  try {
55
- await client.query("BEGIN");
56
- await client.query("SET CONSTRAINTS ALL DEFERRED");
57
- for (const [tblName, offlineRows] of Object.entries(req.body.data) ||
58
- []) {
59
- const table = Table.findOne({ name: tblName });
60
- if (!table) throw new Error(`The table '${tblName}' does not exist.`);
61
- if (!allowInsert(table, req.user))
62
- throwWithCode(req.__("Not authorized"), 401);
63
- if (tblName !== "users") {
64
- for (const newRow of offlineRows.map((row) =>
65
- pickFields(table, row)
66
- )) {
67
- if (aborted) throw new Error("connection closed by client");
68
- await db.insert(table.name, newRow, { client: client });
69
- }
179
+ await client.query(`BEGIN`);
180
+ const syncUntil = new Date(syncTimestamp);
181
+ const result = {
182
+ deletes: {},
183
+ };
184
+ for (const [tblName, syncInfo] of Object.entries(syncInfos)) {
185
+ if (syncInfo.syncFrom) {
186
+ result.deletes[tblName] = await getDelRows(
187
+ tblName,
188
+ new Date(syncInfo.syncFrom),
189
+ syncUntil,
190
+ client
191
+ );
70
192
  }
71
193
  }
72
- if (aborted) throw new Error("connection closed by client");
73
194
  await client.query("COMMIT");
74
- res.json({ success: true });
195
+ res.json(result);
75
196
  } catch (error) {
76
197
  await client.query("ROLLBACK");
77
- getState().log(2, `POST /sync/table_data error: '${error.message}'`);
78
- res
79
- .status(error.statusCode || 400)
80
- .json({ error: error.message || error });
198
+ getState().log(2, `POST /sync/deletes: '${error.message}'`);
199
+ res.status(400).json({ error: error.message || error });
81
200
  } finally {
82
- if (!db.isSQLite) await client.release(true);
201
+ client.release(true);
202
+ }
203
+ })
204
+ );
205
+
206
+ /*
207
+ insert the app offline data
208
+ */
209
+ router.post(
210
+ "/offline_changes",
211
+ error_catcher(async (req, res) => {
212
+ const { changes, syncTimestamp } = req.body;
213
+ const rootFolder = await File.rootFolder();
214
+ try {
215
+ const syncDirName = `${syncTimestamp}_${req.user?.email || "public"}`;
216
+ const syncDir = path.join(
217
+ rootFolder.location,
218
+ "mobile_app",
219
+ "sync",
220
+ syncDirName
221
+ );
222
+ await fs.mkdir(syncDir, { recursive: true });
223
+ await fs.writeFile(
224
+ path.join(syncDir, "changes.json"),
225
+ JSON.stringify(changes)
226
+ );
227
+ const spawnParams = ["sync-upload-data"];
228
+ if (req.user?.email) spawnParams.push("--userEmail", req.user.email);
229
+ spawnParams.push("--directory", syncDir);
230
+ if (
231
+ db.is_it_multi_tenant() &&
232
+ db.getTenantSchema() !== db.connectObj.default_schema
233
+ ) {
234
+ spawnParams.push("--tenantAppName", db.getTenantSchema());
235
+ }
236
+ spawnParams.push("--syncTimestamp", syncTimestamp);
237
+
238
+ res.json({ syncDir: syncDirName });
239
+ const child = spawn(getSafeSaltcornCmd(), spawnParams, {
240
+ stdio: ["pipe", "pipe", "pipe"],
241
+ cwd: ".",
242
+ });
243
+
244
+ child.on("exit", async (exitCode, signal) => {
245
+ getState().log(
246
+ 5,
247
+ `POST /sync/offline_changes: upload offline data finished with code: ${exitCode}`
248
+ );
249
+ });
250
+ child.on("error", (msg) => {
251
+ const message = msg.message ? msg.message : msg.code;
252
+ getState().log(
253
+ 5,
254
+ `POST /sync/offline_changes: upload offline data failed: ${message}`
255
+ );
256
+ });
257
+ } catch (error) {
258
+ getState().log(2, `POST /sync/offline_changes: '${error.message}'`);
259
+ res.status(400).json({ error: error.message || error });
260
+ }
261
+ })
262
+ );
263
+
264
+ router.get(
265
+ "/upload_finished",
266
+ error_catcher(async (req, res) => {
267
+ const { dir_name } = req.query;
268
+ try {
269
+ const rootFolder = await File.rootFolder();
270
+ const syncDir = path.join(
271
+ rootFolder.location,
272
+ "mobile_app",
273
+ "sync",
274
+ dir_name
275
+ );
276
+ let entries = null;
277
+ try {
278
+ entries = await fs.readdir(syncDir);
279
+ } catch (error) {
280
+ return res.json({ finished: false });
281
+ }
282
+ if (entries.indexOf("translated-ids.json") >= 0) {
283
+ const translatedIds = JSON.parse(
284
+ await fs.readFile(path.join(syncDir, "translated-ids.json"))
285
+ );
286
+ res.json({ finished: true, translatedIds });
287
+ } else if (entries.indexOf("error.json") >= 0) {
288
+ const error = JSON.parse(
289
+ await fs.readFile(path.join(syncDir, "error.json"))
290
+ );
291
+ res.json({ finished: true, error });
292
+ } else {
293
+ res.json({ finished: false });
294
+ }
295
+ } catch (error) {
296
+ getState().log(2, `GET /sync/upload_finished: '${error.message}'`);
297
+ res.status(400).json({ error: error.message || error });
298
+ }
299
+ })
300
+ );
301
+
302
+ router.post(
303
+ "/clean_sync_dir",
304
+ error_catcher(async (req, res) => {
305
+ const { dir_name } = req.body;
306
+ try {
307
+ const rootFolder = await File.rootFolder();
308
+ const syncDir = path.join(
309
+ rootFolder.location,
310
+ "mobile_app",
311
+ "sync",
312
+ dir_name
313
+ );
314
+ await fs.rm(syncDir, { recursive: true, force: true });
315
+ res.status(200).send("");
316
+ } catch (error) {
317
+ getState().log(2, `POST /sync/clean_sync_dir: '${error.message}'`);
318
+ res.status(400).json({ error: error.message || error });
83
319
  }
84
320
  })
85
321
  );
package/routes/tables.js CHANGED
@@ -168,6 +168,19 @@ const tableForm = async (table, req) => {
168
168
  name: "versioned",
169
169
  type: "Bool",
170
170
  },
171
+ ...(table.name === "users"
172
+ ? []
173
+ : [
174
+ {
175
+ label: req.__("Sync information"),
176
+ sublabel: req.__(
177
+ "Sync information tracks the last modification or deletion timestamp " +
178
+ "so that the table data can be synchronized with the mobile app"
179
+ ),
180
+ name: "has_sync_info",
181
+ type: "Bool",
182
+ },
183
+ ]),
171
184
  ]),
172
185
  ],
173
186
  });
@@ -922,7 +935,10 @@ router.get(
922
935
  { href: `/table/constraints/${table.id}` },
923
936
  i({ class: "fas fa-2x fa-tasks" }),
924
937
  "<br/>",
925
- req.__("Constraints")
938
+ req.__("Constraints") +
939
+ (table.constraints?.length
940
+ ? ` (${table.constraints.length})`
941
+ : "")
926
942
  )
927
943
  ),
928
944
 
@@ -1080,9 +1096,11 @@ router.post(
1080
1096
  const { id, _csrf, ...rest } = v;
1081
1097
  const table = Table.findOne({ id: parseInt(id) });
1082
1098
  const old_versioned = table.versioned;
1099
+ const old_has_sync_info = table.has_sync_info;
1083
1100
  let hasError = false;
1084
1101
  let notify = "";
1085
1102
  if (!rest.versioned) rest.versioned = false;
1103
+ if (!rest.has_sync_info) rest.has_sync_info = false;
1086
1104
  if (rest.ownership_field_id === "_formula") {
1087
1105
  rest.ownership_field_id = null;
1088
1106
  const fmlValidRes = expressionValidator(rest.ownership_formula);
@@ -0,0 +1,114 @@
1
+ const Table = require("@saltcorn/data/models/table");
2
+ const Plugin = require("@saltcorn/data/models/plugin");
3
+ const { getState, add_tenant } = require("@saltcorn/data/db/state");
4
+ const { install_pack } = require("@saltcorn/admin-models/models/pack");
5
+ const {
6
+ switchToTenant,
7
+ insertTenant,
8
+ create_tenant,
9
+ } = require("@saltcorn/admin-models/models/tenant");
10
+ const { resetToFixtures } = require("../auth/testhelp");
11
+ const db = require("@saltcorn/data/db");
12
+ const load_plugins = require("../load_plugins");
13
+
14
+ beforeAll(async () => {
15
+ if (!db.isSQLite) await db.query(`drop schema if exists test101 CASCADE `);
16
+ await resetToFixtures();
17
+ });
18
+ afterAll(db.close);
19
+
20
+ jest.setTimeout(30000);
21
+ const plugin_pack = (plugin) => ({
22
+ tables: [],
23
+ views: [],
24
+ plugins: [
25
+ {
26
+ ...plugin,
27
+ configuration: null,
28
+ },
29
+ ],
30
+ pages: [],
31
+ roles: [],
32
+ library: [],
33
+ triggers: [],
34
+ });
35
+
36
+ describe("Tenant cannot install unsafe plugins", () => {
37
+ if (!db.isSQLite) {
38
+ it("creates a new tenant", async () => {
39
+ db.enable_multi_tenant();
40
+ const loadAndSaveNewPlugin = load_plugins.loadAndSaveNewPlugin;
41
+
42
+ await getState().setConfig("base_url", "http://example.com/");
43
+
44
+ add_tenant("test101");
45
+
46
+ await switchToTenant(
47
+ await insertTenant("test101", "foo@foo.com", ""),
48
+ "http://test101.example.com/"
49
+ );
50
+
51
+ await create_tenant({
52
+ t: "test101",
53
+ loadAndSaveNewPlugin,
54
+ plugin_loader() {},
55
+ });
56
+ });
57
+ it("can install safe plugins on tenant", async () => {
58
+ await db.runWithTenant("test101", async () => {
59
+ const loadAndSaveNewPlugin = load_plugins.loadAndSaveNewPlugin;
60
+
61
+ await install_pack(
62
+ plugin_pack({
63
+ name: "html",
64
+ source: "npm",
65
+ location: "@saltcorn/html",
66
+ }),
67
+ "Todo list",
68
+ loadAndSaveNewPlugin
69
+ );
70
+ const dbPlugin = await Plugin.findOne({ name: "html" });
71
+ expect(dbPlugin).not.toBe(null);
72
+ });
73
+ });
74
+ it("cannot install unsafe plugins on tenant", async () => {
75
+ await db.runWithTenant("test101", async () => {
76
+ const loadAndSaveNewPlugin = load_plugins.loadAndSaveNewPlugin;
77
+
78
+ await install_pack(
79
+ plugin_pack({
80
+ name: "sql-list",
81
+ source: "npm",
82
+ location: "@saltcorn/sql-list",
83
+ }),
84
+ "Todo list",
85
+ loadAndSaveNewPlugin
86
+ );
87
+ const dbPlugin = await Plugin.findOne({ name: "sql-list" });
88
+ expect(dbPlugin).toBe(null);
89
+ });
90
+ });
91
+ it("can install unsafe plugins on tenant when permitted", async () => {
92
+ await getState().setConfig("tenants_unsafe_plugins", true);
93
+ await db.runWithTenant("test101", async () => {
94
+ const loadAndSaveNewPlugin = load_plugins.loadAndSaveNewPlugin;
95
+
96
+ await install_pack(
97
+ plugin_pack({
98
+ name: "sql-list",
99
+ source: "npm",
100
+ location: "@saltcorn/sql-list",
101
+ }),
102
+ "Todo list",
103
+ loadAndSaveNewPlugin
104
+ );
105
+ const dbPlugin = await Plugin.findOne({ name: "sql-list" });
106
+ expect(dbPlugin).not.toBe(null);
107
+ });
108
+ });
109
+ } else {
110
+ it("does not support tenants on SQLite", async () => {
111
+ expect(db.isSQLite).toBe(true);
112
+ });
113
+ }
114
+ });
@@ -2,13 +2,8 @@ const request = require("supertest");
2
2
  const getApp = require("../app");
3
3
  const Table = require("@saltcorn/data/models/table");
4
4
  const Plugin = require("@saltcorn/data/models/plugin");
5
- const { getState, add_tenant } = require("@saltcorn/data/db/state");
6
- const { install_pack } = require("@saltcorn/admin-models/models/pack");
7
- const {
8
- switchToTenant,
9
- insertTenant,
10
- create_tenant,
11
- } = require("@saltcorn/admin-models/models/tenant");
5
+ const { getState } = require("@saltcorn/data/db/state");
6
+
12
7
  const {
13
8
  getAdminLoginCookie,
14
9
  itShouldRedirectUnauthToLogin,
@@ -331,98 +326,3 @@ describe("config endpoints", () => {
331
326
  .expect(toInclude(">FooSiteName<"));
332
327
  });
333
328
  });
334
-
335
- const plugin_pack = (plugin) => ({
336
- tables: [],
337
- views: [],
338
- plugins: [
339
- {
340
- ...plugin,
341
- configuration: null,
342
- },
343
- ],
344
- pages: [],
345
- roles: [],
346
- library: [],
347
- triggers: [],
348
- });
349
-
350
- describe("Tenant cannot install unsafe plugins", () => {
351
- if (!db.isSQLite) {
352
- it("creates a new tenant", async () => {
353
- db.enable_multi_tenant();
354
- const loadAndSaveNewPlugin = load_plugins.loadAndSaveNewPlugin;
355
-
356
- await getState().setConfig("base_url", "http://example.com/");
357
-
358
- add_tenant("test101");
359
-
360
- await switchToTenant(
361
- await insertTenant("test101", "foo@foo.com", ""),
362
- "http://test101.example.com/"
363
- );
364
-
365
- await create_tenant({
366
- t: "test101",
367
- loadAndSaveNewPlugin,
368
- plugin_loader() {},
369
- });
370
- });
371
- it("can install safe plugins on tenant", async () => {
372
- await db.runWithTenant("test101", async () => {
373
- const loadAndSaveNewPlugin = load_plugins.loadAndSaveNewPlugin;
374
-
375
- await install_pack(
376
- plugin_pack({
377
- name: "html",
378
- source: "npm",
379
- location: "@saltcorn/html",
380
- }),
381
- "Todo list",
382
- loadAndSaveNewPlugin
383
- );
384
- const dbPlugin = await Plugin.findOne({ name: "html" });
385
- expect(dbPlugin).not.toBe(null);
386
- });
387
- });
388
- it("cannot install unsafe plugins on tenant", async () => {
389
- await db.runWithTenant("test101", async () => {
390
- const loadAndSaveNewPlugin = load_plugins.loadAndSaveNewPlugin;
391
-
392
- await install_pack(
393
- plugin_pack({
394
- name: "sql-list",
395
- source: "npm",
396
- location: "@saltcorn/sql-list",
397
- }),
398
- "Todo list",
399
- loadAndSaveNewPlugin
400
- );
401
- const dbPlugin = await Plugin.findOne({ name: "sql-list" });
402
- expect(dbPlugin).toBe(null);
403
- });
404
- });
405
- it("can install unsafe plugins on tenant when permitted", async () => {
406
- await getState().setConfig("tenants_unsafe_plugins", true);
407
- await db.runWithTenant("test101", async () => {
408
- const loadAndSaveNewPlugin = load_plugins.loadAndSaveNewPlugin;
409
-
410
- await install_pack(
411
- plugin_pack({
412
- name: "sql-list",
413
- source: "npm",
414
- location: "@saltcorn/sql-list",
415
- }),
416
- "Todo list",
417
- loadAndSaveNewPlugin
418
- );
419
- const dbPlugin = await Plugin.findOne({ name: "sql-list" });
420
- expect(dbPlugin).not.toBe(null);
421
- });
422
- });
423
- } else {
424
- it("does not support tenants on SQLite", async () => {
425
- expect(db.isSQLite).toBe(true);
426
- });
427
- }
428
- });