@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/auth/routes.js +6 -5
- package/load_plugins.js +8 -1
- package/locales/en.json +22 -1
- package/locales/ru.json +148 -100
- package/package.json +8 -8
- package/public/saltcorn.js +44 -1
- package/routes/actions.js +4 -3
- package/routes/admin.js +98 -2
- package/routes/fields.js +5 -9
- package/routes/menu.js +16 -9
- package/routes/scapi.js +1 -1
- package/routes/sync.js +293 -57
- package/routes/tables.js +19 -1
- package/tests/plugin_install.test.js +114 -0
- package/tests/plugins.test.js +2 -102
- package/tests/sync.test.js +451 -75
- package/tests/view.test.js +2 -0
- package/tests/viewedit.test.js +2 -0
- package/wrapper.js +6 -4
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
"/
|
|
174
|
+
"/deletes",
|
|
40
175
|
error_catcher(async (req, res) => {
|
|
41
|
-
|
|
42
|
-
|
|
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(
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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(
|
|
195
|
+
res.json(result);
|
|
75
196
|
} catch (error) {
|
|
76
197
|
await client.query("ROLLBACK");
|
|
77
|
-
getState().log(2, `POST /sync/
|
|
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
|
-
|
|
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
|
+
});
|
package/tests/plugins.test.js
CHANGED
|
@@ -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
|
|
6
|
-
|
|
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
|
-
});
|