@formio/appserver 2.14.0 → 2.15.0
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/lib/actions/login/LoginAction.js +15 -9
- package/lib/actions/role/RoleAction.js +10 -4
- package/lib/actions/save/SaveAction.js +45 -24
- package/lib/modules/mongodb/db.d.ts +15 -1
- package/lib/modules/mongodb/db.js +122 -48
- package/lib/server.js +0 -1
- package/package.json +16 -14
|
@@ -154,17 +154,23 @@ exports.LoginAction = {
|
|
|
154
154
|
},
|
|
155
155
|
executor(actionForm, action, handler, method) {
|
|
156
156
|
return __awaiter(this, void 0, void 0, function* () {
|
|
157
|
-
var _a, _b
|
|
157
|
+
var _a, _b;
|
|
158
158
|
let loginProvider = null;
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
159
|
+
// Each login action gets its own copy of the auth config: the project's
|
|
160
|
+
// config is shared by every form, so mutating it would let the first
|
|
161
|
+
// login action's settings leak into all the others. The action's own
|
|
162
|
+
// settings take precedence; the project's fill in what it leaves out.
|
|
163
|
+
const settings = (0, lodash_1.cloneDeep)(action.settings || {});
|
|
164
|
+
const projectAuth = (0, lodash_1.cloneDeep)(((_b = (_a = actionForm.project) === null || _a === void 0 ? void 0 : _a.config) === null || _b === void 0 ? void 0 : _b.auth) || {});
|
|
165
|
+
const provider = settings.provider || 'local';
|
|
166
|
+
if (provider === 'local') {
|
|
167
|
+
// The settings form names the login fields `username` and `password`;
|
|
168
|
+
// the provider reads them as `userfield` and `passfield`.
|
|
169
|
+
const local = Object.assign(Object.assign({}, settings), { userfield: settings.userfield || settings.username, passfield: settings.passfield || settings.password });
|
|
170
|
+
loginProvider = new appserver_core_1.LocalAuthProvider(actionForm.project, (0, lodash_1.defaultsDeep)({ local }, projectAuth));
|
|
163
171
|
}
|
|
164
|
-
else if (
|
|
165
|
-
loginProvider = new appserver_core_1.OIDCProvider(actionForm.project, (0, lodash_1.defaultsDeep)(
|
|
166
|
-
oidc: action.settings
|
|
167
|
-
}));
|
|
172
|
+
else if (provider === 'oidc') {
|
|
173
|
+
loginProvider = new appserver_core_1.OIDCProvider(actionForm.project, (0, lodash_1.defaultsDeep)({ oidc: settings }, projectAuth));
|
|
168
174
|
}
|
|
169
175
|
if (loginProvider === null || loginProvider === void 0 ? void 0 : loginProvider.initialize) {
|
|
170
176
|
yield loginProvider.initialize();
|
|
@@ -107,7 +107,7 @@ exports.RoleAction = {
|
|
|
107
107
|
executor(form, action, handler, method) {
|
|
108
108
|
return __awaiter(this, void 0, void 0, function* () {
|
|
109
109
|
return (req, res, next) => __awaiter(this, void 0, void 0, function* () {
|
|
110
|
-
var _a, _b, _c;
|
|
110
|
+
var _a, _b, _c, _d;
|
|
111
111
|
debug('Role Action');
|
|
112
112
|
// Check the submission for the submissionId.
|
|
113
113
|
if (action.settings.association !== 'existing' && action.settings.association !== 'new') {
|
|
@@ -151,12 +151,18 @@ exports.RoleAction = {
|
|
|
151
151
|
if (roleIndex === -1) {
|
|
152
152
|
return next();
|
|
153
153
|
}
|
|
154
|
-
resource.roles.
|
|
154
|
+
resource.roles.splice(roleIndex, 1);
|
|
155
155
|
break;
|
|
156
156
|
}
|
|
157
157
|
debug('Updating user with roles', resource.roles);
|
|
158
|
-
// Update the user.
|
|
159
|
-
|
|
158
|
+
// Update the user through the form the resource belongs to. After a
|
|
159
|
+
// Save action to another resource, the resource is that form's
|
|
160
|
+
// submission, and saving it through this form would match nothing.
|
|
161
|
+
const resourceFormId = resource.form ? String(resource.form) : '';
|
|
162
|
+
const target = (resourceFormId && resourceFormId !== String((_d = form.form) === null || _d === void 0 ? void 0 : _d._id))
|
|
163
|
+
? (yield form.project.getFormById(resourceFormId)) || form
|
|
164
|
+
: form;
|
|
165
|
+
yield target.saveSubmission(resource, ['roles'], req.auth);
|
|
160
166
|
next();
|
|
161
167
|
});
|
|
162
168
|
});
|
|
@@ -41,7 +41,7 @@ exports.SaveAction = {
|
|
|
41
41
|
},
|
|
42
42
|
mappingComponents(form) {
|
|
43
43
|
return __awaiter(this, void 0, void 0, function* () {
|
|
44
|
-
var _a;
|
|
44
|
+
var _a, _b;
|
|
45
45
|
const components = [
|
|
46
46
|
{
|
|
47
47
|
type: 'select',
|
|
@@ -89,8 +89,11 @@ exports.SaveAction = {
|
|
|
89
89
|
customConditional: `show = row.mapType === 'simple' && row.resource === "${key}"`,
|
|
90
90
|
components: []
|
|
91
91
|
};
|
|
92
|
+
// Each resource field is mapped from a field of this form, so the
|
|
93
|
+
// options are this form's fields (childSubmission reads them from
|
|
94
|
+
// the submitted data by these keys).
|
|
92
95
|
const toFields = [];
|
|
93
|
-
appserver_core_1.Utils.eachComponent(
|
|
96
|
+
appserver_core_1.Utils.eachComponent(((_b = form.form) === null || _b === void 0 ? void 0 : _b.components) || [], (component) => {
|
|
94
97
|
if (['button'].indexOf(component.type) !== -1) {
|
|
95
98
|
return;
|
|
96
99
|
}
|
|
@@ -220,6 +223,14 @@ exports.SaveAction = {
|
|
|
220
223
|
catch (err) {
|
|
221
224
|
console.log(`Error in mapping transform: ${err.message}`);
|
|
222
225
|
}
|
|
226
|
+
// A script that fails, or never assigns `body`, yields nothing usable.
|
|
227
|
+
if (!childSub || typeof childSub !== 'object') {
|
|
228
|
+
error('The advanced mapping did not produce a body; saving an empty resource submission.');
|
|
229
|
+
childSub = { data: {} };
|
|
230
|
+
}
|
|
231
|
+
if (!childSub.data || typeof childSub.data !== 'object') {
|
|
232
|
+
childSub.data = {};
|
|
233
|
+
}
|
|
223
234
|
}
|
|
224
235
|
else {
|
|
225
236
|
(0, each_1.default)(action.settings.fields, function (field, key) {
|
|
@@ -234,7 +245,13 @@ exports.SaveAction = {
|
|
|
234
245
|
childSub.metadata = submission.metadata;
|
|
235
246
|
if (action.settings.transform) {
|
|
236
247
|
try {
|
|
237
|
-
|
|
248
|
+
// `submission` is the original request; `data` is the data
|
|
249
|
+
// already produced by the mappings above.
|
|
250
|
+
const transformed = appserver_core_1.Utils.evaluate(action.settings.transform, Object.assign(Object.assign({}, appserver_core_1.Utils.evalContext(form, req.submission || req.body, req.auth, req.headers, req.findQuery)), { data: childSub.data }), 'data');
|
|
251
|
+
// A failed transform leaves the mapped data as it was.
|
|
252
|
+
if (transformed && typeof transformed === 'object') {
|
|
253
|
+
childSub.data = transformed;
|
|
254
|
+
}
|
|
238
255
|
}
|
|
239
256
|
catch (err) {
|
|
240
257
|
console.log(`Error in submission transform: ${err.message}`);
|
|
@@ -285,28 +302,32 @@ exports.SaveAction = {
|
|
|
285
302
|
if ((_a = action.settings) === null || _a === void 0 ? void 0 : _a.resource) {
|
|
286
303
|
debug('Saving to resource: ' + action.settings.resource);
|
|
287
304
|
const saveTo = yield form.project.getForm(action.settings.resource);
|
|
288
|
-
if (saveTo) {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
return next(err);
|
|
304
|
-
}
|
|
305
|
-
// Set the child response based on the response of the subform.
|
|
306
|
-
exports.SaveAction.childResponse(action, req, res, saveTo, childReq.body);
|
|
307
|
-
next();
|
|
308
|
-
});
|
|
305
|
+
if (!saveTo) {
|
|
306
|
+
// Never fall back to saving into this form: the data was
|
|
307
|
+
// meant for another resource.
|
|
308
|
+
error(`Resource "${action.settings.resource}" not found for the save action.`);
|
|
309
|
+
return next(`Resource "${action.settings.resource}" not found.`);
|
|
310
|
+
}
|
|
311
|
+
if (!saveTo.handlers) {
|
|
312
|
+
error('Cannot find resource handlers.');
|
|
313
|
+
return next('Cannot find resource handlers.');
|
|
314
|
+
}
|
|
315
|
+
const handlers = req.method === 'PUT' ? saveTo.handlers.update : saveTo.handlers.create;
|
|
316
|
+
const childReq = appserver_core_1.Utils.childRequest(req);
|
|
317
|
+
if (!childReq) {
|
|
318
|
+
error('Failed to create child request');
|
|
319
|
+
return next('Failed to create child request');
|
|
309
320
|
}
|
|
321
|
+
childReq.body = exports.SaveAction.childSubmission(action, req, res, saveTo, childReq.body);
|
|
322
|
+
return async_1.default.series(handlers.map((handler) => async_1.default.apply(handler, childReq, res)), (err) => {
|
|
323
|
+
if (err) {
|
|
324
|
+
error(err);
|
|
325
|
+
return next(err);
|
|
326
|
+
}
|
|
327
|
+
// Set the child response based on the response of the subform.
|
|
328
|
+
exports.SaveAction.childResponse(action, req, res, saveTo, childReq.body);
|
|
329
|
+
next();
|
|
330
|
+
});
|
|
310
331
|
}
|
|
311
332
|
// Save the submission.
|
|
312
333
|
req.saveSubmission = true;
|
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
import { Collection, Document, Db, MongoClient, FindOneOptions } from 'mongodb';
|
|
2
2
|
import { Component, Submission } from '@formio/core';
|
|
3
3
|
import { ServerDB, DBConfig, AppServerForm, DatabaseIndexResponse, IndexQuery } from '@formio/appserver-core';
|
|
4
|
+
/**
|
|
5
|
+
* Whether a URL filter names a field. Mongo operators (`$where`, `$expr`, ...)
|
|
6
|
+
* and prototype keys are never fields, and would let a caller run JavaScript in
|
|
7
|
+
* the database or change the query's structure.
|
|
8
|
+
*/
|
|
9
|
+
export declare function isFilterField(name: string): boolean;
|
|
4
10
|
export declare class Database implements ServerDB {
|
|
5
11
|
config: DBConfig;
|
|
6
12
|
db: Db | null;
|
|
7
13
|
client: MongoClient | null;
|
|
8
14
|
currentCollectionName: string;
|
|
9
15
|
currentCollection: Collection<Document> | null;
|
|
16
|
+
/** Collections for forms with `settings.collection`, created and indexed once each. */
|
|
17
|
+
formCollections: Map<string, Collection<Document>>;
|
|
10
18
|
defaultCollection: Collection<Document> | null;
|
|
11
19
|
prefix: string;
|
|
12
20
|
constructor(config: DBConfig);
|
|
@@ -88,7 +96,13 @@ export declare class Database implements ServerDB {
|
|
|
88
96
|
* @param {*} id
|
|
89
97
|
* @returns
|
|
90
98
|
*/
|
|
91
|
-
|
|
99
|
+
/**
|
|
100
|
+
* Scope a query to the form and project. Including deleted records drops only
|
|
101
|
+
* the `deleted` filter, never the form or project scope: every form writes to
|
|
102
|
+
* the shared submissions collection. `scope = false` is for nested
|
|
103
|
+
* `$or`/`$and` clauses, which only need their ids converted.
|
|
104
|
+
*/
|
|
105
|
+
query(form: AppServerForm, query?: any, includeDeleted?: boolean, scope?: boolean): any;
|
|
92
106
|
/**
|
|
93
107
|
* Find many records that match a query.
|
|
94
108
|
*/
|
|
@@ -21,14 +21,30 @@ var __rest = (this && this.__rest) || function (s, e) {
|
|
|
21
21
|
};
|
|
22
22
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
23
|
exports.Database = void 0;
|
|
24
|
+
exports.isFilterField = isFilterField;
|
|
24
25
|
const mongodb_1 = require("mongodb");
|
|
25
26
|
const lodash_1 = require("lodash");
|
|
26
27
|
const core_1 = require("@formio/core");
|
|
27
28
|
const debug = require('debug')('formio:db');
|
|
28
29
|
const error = require('debug')('formio:error');
|
|
30
|
+
/** The operators a URL filter may use (`data.age__gt=3`). */
|
|
31
|
+
const FILTER_SELECTORS = ['eq', 'ne', 'gt', 'gte', 'lt', 'lte', 'in', 'nin', 'exists', 'regex'];
|
|
32
|
+
/** The default and largest number of records an index request returns. */
|
|
33
|
+
const DEFAULT_LIMIT = 10;
|
|
34
|
+
const MAX_LIMIT = 1000;
|
|
35
|
+
/**
|
|
36
|
+
* Whether a URL filter names a field. Mongo operators (`$where`, `$expr`, ...)
|
|
37
|
+
* and prototype keys are never fields, and would let a caller run JavaScript in
|
|
38
|
+
* the database or change the query's structure.
|
|
39
|
+
*/
|
|
40
|
+
function isFilterField(name) {
|
|
41
|
+
return !!name && name.split('.').every((segment) => !!segment && !segment.startsWith('$') && !['__proto__', 'constructor', 'prototype'].includes(segment));
|
|
42
|
+
}
|
|
29
43
|
class Database {
|
|
30
44
|
constructor(config) {
|
|
31
45
|
this.config = config;
|
|
46
|
+
/** Collections for forms with `settings.collection`, created and indexed once each. */
|
|
47
|
+
this.formCollections = new Map();
|
|
32
48
|
this.prefix = '';
|
|
33
49
|
this.db = null;
|
|
34
50
|
this.client = null; // Add this line
|
|
@@ -37,8 +53,18 @@ class Database {
|
|
|
37
53
|
this.defaultCollection = null;
|
|
38
54
|
}
|
|
39
55
|
ObjectId(id) {
|
|
40
|
-
|
|
41
|
-
|
|
56
|
+
// Operator objects from URL filters (`_id__in`, `owner__ne`, ...) convert
|
|
57
|
+
// their operand(s), so the comparison is against stored ObjectIds.
|
|
58
|
+
// Every operator is converted: dropping one (e.g. the `$eq` of
|
|
59
|
+
// `owner=x&owner__ne=y`) would widen the query.
|
|
60
|
+
if ((0, lodash_1.isPlainObject)(id) && Object.keys(id).some((key) => key.startsWith('$'))) {
|
|
61
|
+
const converted = {};
|
|
62
|
+
for (const [op, value] of Object.entries(id)) {
|
|
63
|
+
converted[op] = ['$in', '$nin'].includes(op) && Array.isArray(value)
|
|
64
|
+
? value.map((_id) => this.ObjectId(_id))
|
|
65
|
+
: (['$eq', '$ne'].includes(op) ? this.ObjectId(value) : value);
|
|
66
|
+
}
|
|
67
|
+
return converted;
|
|
42
68
|
}
|
|
43
69
|
try {
|
|
44
70
|
return id ? (new mongodb_1.ObjectId(id)) : null;
|
|
@@ -69,14 +95,15 @@ class Database {
|
|
|
69
95
|
if (this.config.dropOnConnect) {
|
|
70
96
|
yield this.db.dropDatabase();
|
|
71
97
|
}
|
|
72
|
-
this.addIndex(this.collection('project'), 'name');
|
|
98
|
+
yield this.addIndex(this.collection('project'), 'name');
|
|
73
99
|
this.defaultCollection = this.collection('submissions');
|
|
74
100
|
yield this.setupIndexes(this.defaultCollection);
|
|
75
101
|
debug('Connected to database');
|
|
76
102
|
}
|
|
77
103
|
catch (err) {
|
|
78
104
|
error(err.message);
|
|
79
|
-
|
|
105
|
+
// A non-zero code, so supervisors treat a failed connection as a failure.
|
|
106
|
+
process.exit(1);
|
|
80
107
|
}
|
|
81
108
|
});
|
|
82
109
|
}
|
|
@@ -219,24 +246,38 @@ class Database {
|
|
|
219
246
|
Object.entries(filters).forEach(([name, value]) => {
|
|
220
247
|
// Get the filter object.
|
|
221
248
|
const filter = (0, lodash_1.zipObject)(['name', 'selector'], name.split('__'));
|
|
249
|
+
// Only field filters with a known operator reach the database.
|
|
250
|
+
if (!isFilterField(filter.name) || (filter.selector && !FILTER_SELECTORS.includes(filter.selector))) {
|
|
251
|
+
debug(`Ignoring query filter "${name}"`);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
222
254
|
if (filter.selector) {
|
|
223
255
|
var filterQuery = dbQuery[filter.name];
|
|
224
|
-
// Init the filter.
|
|
225
|
-
|
|
256
|
+
// Init the filter. A plain filter on the same field (`?a=5&a__gt=3`)
|
|
257
|
+
// is kept as `$eq` alongside the operator.
|
|
258
|
+
if (filterQuery === undefined) {
|
|
226
259
|
filterQuery = {};
|
|
227
260
|
}
|
|
261
|
+
else if (!(0, lodash_1.isPlainObject)(filterQuery)) {
|
|
262
|
+
filterQuery = { $eq: filterQuery };
|
|
263
|
+
}
|
|
228
264
|
if (filter.selector === 'exists') {
|
|
229
265
|
value = ((value === 'true') || (value === '1')) ? true : value;
|
|
230
266
|
value = ((value === 'false') || (value === '0')) ? false : value;
|
|
231
267
|
value = !!value;
|
|
232
268
|
}
|
|
233
269
|
else if (filter.selector === 'regex') {
|
|
234
|
-
|
|
235
|
-
|
|
270
|
+
// Either `/pattern/flags` or a bare pattern. The pattern is always
|
|
271
|
+
// matched literally: login builds these filters from user input,
|
|
272
|
+
// so unescaped patterns would allow regex injection.
|
|
273
|
+
const delimited = String(value).match(/^\/([\s\S]*)\/([a-z]*)$/);
|
|
274
|
+
const pattern = delimited ? delimited[1] : String(value);
|
|
275
|
+
const flags = delimited && /^[gimsuy]*$/.test(delimited[2]) && delimited[2] ? delimited[2] : 'i';
|
|
276
|
+
if (!pattern) {
|
|
236
277
|
return;
|
|
237
278
|
}
|
|
238
279
|
try {
|
|
239
|
-
value = new RegExp((0, lodash_1.escapeRegExp)(
|
|
280
|
+
value = new RegExp((0, lodash_1.escapeRegExp)(pattern), flags);
|
|
240
281
|
}
|
|
241
282
|
catch (err) {
|
|
242
283
|
error(err);
|
|
@@ -257,9 +298,15 @@ class Database {
|
|
|
257
298
|
return;
|
|
258
299
|
}
|
|
259
300
|
else {
|
|
260
|
-
// Set the find query to this value.
|
|
301
|
+
// Set the find query to this value. When an operator filter on the
|
|
302
|
+
// same field came first, keep it and add this value as `$eq`.
|
|
261
303
|
value = this.getQueryValue(filter.name, value, filter.selector);
|
|
262
|
-
dbQuery[filter.name]
|
|
304
|
+
if ((0, lodash_1.isPlainObject)(dbQuery[filter.name])) {
|
|
305
|
+
dbQuery[filter.name].$eq = value;
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
dbQuery[filter.name] = value;
|
|
309
|
+
}
|
|
263
310
|
return;
|
|
264
311
|
}
|
|
265
312
|
});
|
|
@@ -294,22 +341,31 @@ class Database {
|
|
|
294
341
|
formCollection(form) {
|
|
295
342
|
return __awaiter(this, void 0, void 0, function* () {
|
|
296
343
|
if (this.db && form && form.settings && form.settings.collection) {
|
|
297
|
-
|
|
298
|
-
|
|
344
|
+
const name = form.settings.collection;
|
|
345
|
+
const cached = this.formCollections.get(name);
|
|
346
|
+
if (cached) {
|
|
347
|
+
this.currentCollection = cached;
|
|
348
|
+
this.currentCollectionName = name;
|
|
349
|
+
return cached;
|
|
299
350
|
}
|
|
300
|
-
let collection = null;
|
|
301
351
|
try {
|
|
302
|
-
debug('db.createCollection()',
|
|
303
|
-
yield this.db.createCollection(this.collectionName(
|
|
304
|
-
collection = this.collection(form.settings.collection);
|
|
305
|
-
this.setupIndexes(collection, form);
|
|
352
|
+
debug('db.createCollection()', name);
|
|
353
|
+
yield this.db.createCollection(this.collectionName(name));
|
|
306
354
|
}
|
|
307
355
|
catch (err) {
|
|
308
|
-
|
|
356
|
+
// 48 = NamespaceExists: the collection is already there, which is fine.
|
|
357
|
+
if ((err === null || err === void 0 ? void 0 : err.code) !== 48) {
|
|
358
|
+
error(err.message);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
const collection = this.collection(name);
|
|
362
|
+
if (collection) {
|
|
363
|
+
yield this.setupIndexes(collection, form);
|
|
364
|
+
this.formCollections.set(name, collection);
|
|
309
365
|
}
|
|
310
|
-
this.currentCollection = collection
|
|
311
|
-
this.currentCollectionName =
|
|
312
|
-
return
|
|
366
|
+
this.currentCollection = collection;
|
|
367
|
+
this.currentCollectionName = name;
|
|
368
|
+
return collection;
|
|
313
369
|
}
|
|
314
370
|
return this.defaultCollection;
|
|
315
371
|
});
|
|
@@ -322,14 +378,14 @@ class Database {
|
|
|
322
378
|
if (!collection) {
|
|
323
379
|
return;
|
|
324
380
|
}
|
|
325
|
-
this.addIndex(collection, 'project');
|
|
326
|
-
this.addIndex(collection, 'form');
|
|
327
|
-
this.addIndex(collection, 'deleted');
|
|
328
|
-
this.addIndex(collection, 'modified');
|
|
381
|
+
yield this.addIndex(collection, 'project');
|
|
382
|
+
yield this.addIndex(collection, 'form');
|
|
383
|
+
yield this.addIndex(collection, 'deleted');
|
|
384
|
+
yield this.addIndex(collection, 'modified');
|
|
329
385
|
// If TTL index is needed add that here.
|
|
330
386
|
if (this.config.ttl) {
|
|
331
387
|
try {
|
|
332
|
-
collection.createIndex({
|
|
388
|
+
yield collection.createIndex({
|
|
333
389
|
created: 1
|
|
334
390
|
}, {
|
|
335
391
|
background: true,
|
|
@@ -341,7 +397,7 @@ class Database {
|
|
|
341
397
|
}
|
|
342
398
|
}
|
|
343
399
|
else {
|
|
344
|
-
this.addIndex(collection, 'created');
|
|
400
|
+
yield this.addIndex(collection, 'created');
|
|
345
401
|
}
|
|
346
402
|
if (form && form.components) {
|
|
347
403
|
core_1.Utils.eachComponent(form.components, (component, path) => {
|
|
@@ -364,7 +420,7 @@ class Database {
|
|
|
364
420
|
index[path] = 1;
|
|
365
421
|
try {
|
|
366
422
|
debug('db.addIndex()', path);
|
|
367
|
-
collection.createIndex(index, { background: true });
|
|
423
|
+
yield collection.createIndex(index, { background: true });
|
|
368
424
|
}
|
|
369
425
|
catch (err) {
|
|
370
426
|
error(`Cannot add index ${path}`, err.message);
|
|
@@ -425,9 +481,10 @@ class Database {
|
|
|
425
481
|
* @returns
|
|
426
482
|
*/
|
|
427
483
|
indexQuery(urlQuery) {
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
484
|
+
// Bounded, so one request can't read (or make the server hold) everything.
|
|
485
|
+
const maxLimit = Number(this.config.maxLimit) || MAX_LIMIT;
|
|
486
|
+
const limit = Math.min(Math.max(parseInt(urlQuery.limit) || DEFAULT_LIMIT, 1), maxLimit);
|
|
487
|
+
const skip = Math.max(parseInt(urlQuery.skip) || 0, 0);
|
|
431
488
|
let sort = { created: -1 };
|
|
432
489
|
if (urlQuery.sort) {
|
|
433
490
|
if (typeof urlQuery.sort === 'string') {
|
|
@@ -443,7 +500,8 @@ class Database {
|
|
|
443
500
|
sort = urlQuery.sort;
|
|
444
501
|
}
|
|
445
502
|
}
|
|
446
|
-
|
|
503
|
+
// Only an explicit "true" includes deleted records ("false" is truthy).
|
|
504
|
+
const includeDeleted = urlQuery.includeDeleted === true || urlQuery.includeDeleted === 'true';
|
|
447
505
|
let project = {};
|
|
448
506
|
if (urlQuery.select) {
|
|
449
507
|
if (typeof urlQuery.select === 'string') {
|
|
@@ -525,7 +583,7 @@ class Database {
|
|
|
525
583
|
if (form && form._id) {
|
|
526
584
|
record.form = this.ObjectId(form._id);
|
|
527
585
|
}
|
|
528
|
-
if (form
|
|
586
|
+
if (form === null || form === void 0 ? void 0 : form.project) {
|
|
529
587
|
record.project = this.ObjectId(form.project);
|
|
530
588
|
}
|
|
531
589
|
if (record.owner) {
|
|
@@ -563,29 +621,37 @@ class Database {
|
|
|
563
621
|
* @param {*} id
|
|
564
622
|
* @returns
|
|
565
623
|
*/
|
|
566
|
-
|
|
624
|
+
/**
|
|
625
|
+
* Scope a query to the form and project. Including deleted records drops only
|
|
626
|
+
* the `deleted` filter, never the form or project scope: every form writes to
|
|
627
|
+
* the shared submissions collection. `scope = false` is for nested
|
|
628
|
+
* `$or`/`$and` clauses, which only need their ids converted.
|
|
629
|
+
*/
|
|
630
|
+
query(form, query = {}, includeDeleted = false, scope = true) {
|
|
567
631
|
if (query._id) {
|
|
568
632
|
query._id = this.ObjectId(query._id);
|
|
569
633
|
}
|
|
570
634
|
if (query.owner) {
|
|
571
635
|
query.owner = this.ObjectId(query.owner);
|
|
572
636
|
}
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
else if (!includeAll && form && form._id) {
|
|
637
|
+
// The form scope always wins over a form filter in the query, exactly as
|
|
638
|
+
// the project scope does: every form shares the submissions collection.
|
|
639
|
+
if (scope && form && form._id) {
|
|
577
640
|
query.form = this.ObjectId(form._id);
|
|
578
641
|
}
|
|
579
|
-
if (
|
|
642
|
+
else if (query.form) {
|
|
643
|
+
query.form = this.ObjectId(query.form);
|
|
644
|
+
}
|
|
645
|
+
if (scope && (form === null || form === void 0 ? void 0 : form.project)) {
|
|
580
646
|
query.project = this.ObjectId(form.project);
|
|
581
647
|
}
|
|
582
|
-
if (!
|
|
648
|
+
if (scope && !includeDeleted) {
|
|
583
649
|
query.deleted = { $eq: null };
|
|
584
650
|
}
|
|
585
651
|
// Handle nested queries.
|
|
586
652
|
['$or', '$and'].forEach((key) => {
|
|
587
653
|
if (query[key]) {
|
|
588
|
-
query[key].forEach((subQuery) => this.query(form, subQuery, true));
|
|
654
|
+
query[key].forEach((subQuery) => this.query(form, subQuery, true, false));
|
|
589
655
|
}
|
|
590
656
|
});
|
|
591
657
|
return query;
|
|
@@ -809,16 +875,23 @@ class Database {
|
|
|
809
875
|
else if ((0, lodash_1.isArray)(value)) {
|
|
810
876
|
this.addPathQueryParams({ $all: value }, query, path);
|
|
811
877
|
}
|
|
812
|
-
else
|
|
878
|
+
else {
|
|
879
|
+
// Numbers, objects, booleans: match the exact value. Without a value
|
|
880
|
+
// predicate the query would match any submitted record.
|
|
813
881
|
this.addPathQueryParams({ $eq: value }, query, path);
|
|
814
882
|
}
|
|
815
883
|
// Only search for non-deleted items
|
|
816
884
|
query.deleted = { $eq: null };
|
|
817
885
|
query.state = 'submitted';
|
|
818
|
-
// Perform the query.
|
|
886
|
+
// Perform the query. Errors must reach the catch below (findOne() swallows
|
|
887
|
+
// them), or a failed lookup would be reported as unique.
|
|
888
|
+
const lookup = (dbQuery, findOptions) => __awaiter(this, void 0, void 0, function* () {
|
|
889
|
+
const collection = yield this.formCollection(form);
|
|
890
|
+
return collection ? yield collection.findOne(this.query(form, dbQuery), findOptions) : null;
|
|
891
|
+
});
|
|
819
892
|
let result = null;
|
|
820
893
|
try {
|
|
821
|
-
result = yield
|
|
894
|
+
result = yield lookup(query, options);
|
|
822
895
|
if (!(result === null || result === void 0 ? void 0 : result._id) || ((result === null || result === void 0 ? void 0 : result._id.toString()) === (submission === null || submission === void 0 ? void 0 : submission._id))) {
|
|
823
896
|
return true;
|
|
824
897
|
}
|
|
@@ -835,13 +908,14 @@ class Database {
|
|
|
835
908
|
$options: 'i'
|
|
836
909
|
}, query, path);
|
|
837
910
|
try {
|
|
838
|
-
result = yield
|
|
911
|
+
result = yield lookup(query);
|
|
839
912
|
if (!(result === null || result === void 0 ? void 0 : result._id) || ((result === null || result === void 0 ? void 0 : result._id.toString()) === (submission === null || submission === void 0 ? void 0 : submission._id))) {
|
|
840
913
|
return true;
|
|
841
914
|
}
|
|
842
915
|
else {
|
|
843
|
-
|
|
844
|
-
|
|
916
|
+
const conflictId = result._id.toString();
|
|
917
|
+
component.conflictId = conflictId;
|
|
918
|
+
return conflictId;
|
|
845
919
|
}
|
|
846
920
|
}
|
|
847
921
|
catch (err) {
|
package/lib/server.js
CHANGED
|
@@ -24,7 +24,6 @@ const appserver_core_1 = require("@formio/appserver-core");
|
|
|
24
24
|
const modules_1 = require("./modules");
|
|
25
25
|
const actions_1 = __importDefault(require("./actions"));
|
|
26
26
|
const defaultsDeep_1 = __importDefault(require("lodash/defaultsDeep"));
|
|
27
|
-
``;
|
|
28
27
|
const packageJson = require('../package.json');
|
|
29
28
|
const appCorePackage = require('@formio/appserver-core/package.json');
|
|
30
29
|
__exportStar(require("@formio/appserver-core"), exports);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@formio/appserver",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.15.0",
|
|
4
4
|
"description": "A highly performant and extensible Application Server for Form.io Multi-Tenant Deployments.",
|
|
5
5
|
"main": "lib/server.js",
|
|
6
6
|
"types": "lib/server.d.ts",
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
"build:lib": "npm run clean && tsc; true",
|
|
14
14
|
"test": "mocha",
|
|
15
15
|
"dev": "tsc --watch --preserveWatchOutput",
|
|
16
|
-
"prepack": "npm run build"
|
|
16
|
+
"prepack": "npm run build",
|
|
17
|
+
"test:coverage": "c8 --all --reporter=text-summary --reporter=html --include \"src/**/*.ts\" --exclude \"src/**/*.spec.ts\" mocha"
|
|
17
18
|
},
|
|
18
19
|
"repository": {
|
|
19
20
|
"type": "git",
|
|
@@ -33,33 +34,34 @@
|
|
|
33
34
|
],
|
|
34
35
|
"homepage": "https://github.com/formio/appserver#readme",
|
|
35
36
|
"dependencies": {
|
|
36
|
-
"@formio/appserver-core": "^2.
|
|
37
|
-
"@formio/core": "2.
|
|
37
|
+
"@formio/appserver-core": "^2.15.0",
|
|
38
|
+
"@formio/core": "2.9.0",
|
|
38
39
|
"async": "^3.2.6",
|
|
39
40
|
"bcryptjs": "^3.0.3",
|
|
40
41
|
"debug": "^4.4.3",
|
|
41
|
-
"dotenv": "^
|
|
42
|
-
"express": "^5.
|
|
42
|
+
"dotenv": "^18.0.8",
|
|
43
|
+
"express": "^5.3.0",
|
|
43
44
|
"jsonwebtoken": "^9.0.3",
|
|
44
45
|
"lodash": "^4.18.1",
|
|
45
|
-
"mongodb": "^7.
|
|
46
|
+
"mongodb": "^7.7.0"
|
|
46
47
|
},
|
|
47
48
|
"devDependencies": {
|
|
48
|
-
"@types/async": "^3.2.
|
|
49
|
+
"@types/async": "^3.2.26",
|
|
49
50
|
"@types/bcryptjs": "^3.0.0",
|
|
50
51
|
"@types/chai": "^5.2.3",
|
|
51
52
|
"@types/express": "^5.0.6",
|
|
52
53
|
"@types/jsonwebtoken": "^9.0.10",
|
|
53
|
-
"@types/lodash": "^4.17.
|
|
54
|
+
"@types/lodash": "^4.17.26",
|
|
54
55
|
"@types/mocha": "^10.0.10",
|
|
55
|
-
"@types/node": "^26.
|
|
56
|
-
"
|
|
56
|
+
"@types/node": "^26.6.5",
|
|
57
|
+
"c8": "^12.0.0",
|
|
58
|
+
"chai": "^6.3.0",
|
|
57
59
|
"cross-env": "^10.1.0",
|
|
58
|
-
"mocha": "^
|
|
59
|
-
"mongodb-memory-server": "^11.
|
|
60
|
+
"mocha": "^12.0.3",
|
|
61
|
+
"mongodb-memory-server": "^11.3.0",
|
|
60
62
|
"ts-node": "^10.9.2",
|
|
61
63
|
"tsconfig-paths": "^4.2.0",
|
|
62
|
-
"tsx": "^4.23.
|
|
64
|
+
"tsx": "^4.23.15",
|
|
63
65
|
"typescript": "^7.0.2"
|
|
64
66
|
}
|
|
65
67
|
}
|