expressa 1.4.7 → 2.0.1
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/.eslintrc.json +1 -1
- package/controllers/collections.js +66 -8
- package/listeners.js +5 -10
- package/listeners_collection_permissions.js +1 -4
- package/listeners_users.js +92 -92
- package/listeners_validation.js +43 -45
- package/modules/collections/collections.js +10 -2
- package/modules/permissions/permissions.js +2 -8
- package/package.json +1 -1
- package/test/0-install.js +11 -0
- package/test/collections.querying.js +10 -10
- package/test/test.js +1 -1
- package/test/testutils.js +54 -2
- package/test/users.js +119 -0
- package/util.js +423 -461
package/util.js
CHANGED
|
@@ -1,461 +1,423 @@
|
|
|
1
|
-
const randomstring = require('randomstring')
|
|
2
|
-
const {v4} = require('uuid')
|
|
3
|
-
const debug = require('debug')('expressa')
|
|
4
|
-
const crypto = require('crypto')
|
|
5
|
-
const pg = require('pg')
|
|
6
|
-
const pgPools = {}
|
|
7
|
-
const dot = require('dot-object')
|
|
8
|
-
const mongoQuery = require('mongo-query')
|
|
9
|
-
const Ajv = require('ajv')
|
|
10
|
-
const ajv = new Ajv({
|
|
11
|
-
allErrors: true,
|
|
12
|
-
strict: 'log',
|
|
13
|
-
strictSchema: 'log',
|
|
14
|
-
validateFormats: false,
|
|
15
|
-
allowUnionTypes: true,
|
|
16
|
-
})
|
|
17
|
-
ajv.addKeyword({
|
|
18
|
-
keyword: 'links',
|
|
19
|
-
type: 'string',
|
|
20
|
-
schemaType: 'array',
|
|
21
|
-
})
|
|
22
|
-
ajv.addKeyword({
|
|
23
|
-
keyword: 'media',
|
|
24
|
-
type: 'string',
|
|
25
|
-
schemaType: 'object',
|
|
26
|
-
})
|
|
27
|
-
ajv.addKeyword({
|
|
28
|
-
keyword: 'propertyOrder',
|
|
29
|
-
schemaType: 'number',
|
|
30
|
-
})
|
|
31
|
-
const formatKey = ajv.getKeyword('format')
|
|
32
|
-
formatKey.type = formatKey.type.concat(['array', 'boolean', 'object'])
|
|
33
|
-
|
|
34
|
-
const schemaValidators = {}
|
|
35
|
-
|
|
36
|
-
exports.addSchemaValidator = function(collection, schema) {
|
|
37
|
-
schemaValidators[collection] = ajv.compile(schema)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
exports.validateSchema = function(collection, doc) {
|
|
41
|
-
if (!schemaValidators[collection]) {
|
|
42
|
-
console.error(`missing schema validator for collection ${collection}`)
|
|
43
|
-
return true
|
|
44
|
-
}
|
|
45
|
-
const isValid = schemaValidators[collection](doc)
|
|
46
|
-
if (!isValid) {
|
|
47
|
-
throw new ApiError(400, schemaValidators[collection].errors.map((err) => `${err.instancePath} ${err.message}: ${JSON.stringify(err.params)}`).join(', '))
|
|
48
|
-
}
|
|
49
|
-
return true
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
exports.orderBy = function (data, orderby) {
|
|
53
|
-
data.sort(function compare (a, b) {
|
|
54
|
-
for (let i = 0; i < orderby.length; i++) {
|
|
55
|
-
const ordering = orderby[i]
|
|
56
|
-
const key = ordering[0]
|
|
57
|
-
if (exports.getPath(a, key) > exports.getPath(b, key)) {
|
|
58
|
-
return ordering[1]
|
|
59
|
-
} else if (exports.getPath(a, key) < exports.getPath(b, key)) {
|
|
60
|
-
return -ordering[1]
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
return 0
|
|
64
|
-
})
|
|
65
|
-
return data
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
exports.getArrayPaths = function (key, value) {
|
|
69
|
-
if (!value)
|
|
70
|
-
return []
|
|
71
|
-
if (value.type === 'array')
|
|
72
|
-
return [key].concat(exports.getArrayPaths(key, value.items)).filter(el => el)
|
|
73
|
-
if (value.type === 'object') {
|
|
74
|
-
return Object.entries(value.properties)
|
|
75
|
-
.map(([key2, value]) => {
|
|
76
|
-
return exports.getArrayPaths(key ? key + '.' + key2 : key2, value)
|
|
77
|
-
})
|
|
78
|
-
.flat()
|
|
79
|
-
.filter(el => el)
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function isObject(obj) {
|
|
84
|
-
return typeof obj === 'object'
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Returns an excerpt of the selected document based on an include projection.
|
|
89
|
-
*
|
|
90
|
-
* Note:
|
|
91
|
-
* It includes the fields listed in the projection with a value of 1. the
|
|
92
|
-
* unknown fields or the fields with a value other than 1 are ignored.
|
|
93
|
-
* @param {Object} the selected document,
|
|
94
|
-
* @param {Object} the projection to apply,
|
|
95
|
-
*/
|
|
96
|
-
function _include(obj, source) {
|
|
97
|
-
const data = {}
|
|
98
|
-
for (const prop in source) {
|
|
99
|
-
if (typeof obj[prop] !== 'undefined') {
|
|
100
|
-
if (isObject(source[prop])) {
|
|
101
|
-
if (Array.isArray(obj[prop])) {
|
|
102
|
-
data[prop] = obj[prop].map((element) => _include(element, source[prop]))
|
|
103
|
-
} else {
|
|
104
|
-
data[prop] = _include(obj[prop], source[prop])
|
|
105
|
-
}
|
|
106
|
-
} else if (source[prop] == 1) {
|
|
107
|
-
if (isObject(obj[prop])) {
|
|
108
|
-
data[prop] = exports.clone(obj[prop])
|
|
109
|
-
} else if (typeof obj[prop] !== 'undefined') {
|
|
110
|
-
data[prop] = obj[prop]
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
return data
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Returns an excerpt of the selected document based on an exclude projection.
|
|
120
|
-
*
|
|
121
|
-
* Note:
|
|
122
|
-
* It excludes the fields listed in the projection with a value of 0. The
|
|
123
|
-
* unspecified fields are kept.
|
|
124
|
-
*/
|
|
125
|
-
function _exclude(obj, source) {
|
|
126
|
-
const data = {}
|
|
127
|
-
for (const prop in obj) {
|
|
128
|
-
if (source[prop] !== undefined && !source[prop]) {
|
|
129
|
-
if (isObject(source[prop])) {
|
|
130
|
-
data[prop] = {}
|
|
131
|
-
_exclude(obj[prop], source[prop], data[prop])
|
|
132
|
-
}
|
|
133
|
-
} else if (isObject(obj[prop])) {
|
|
134
|
-
data[prop] = exports.clone(obj[prop])
|
|
135
|
-
} else {
|
|
136
|
-
data[prop] = obj[prop]
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
return data
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
exports.mongoProject = function(record, projection) {
|
|
143
|
-
if (!projection || Object.keys(projection).length < 1) {
|
|
144
|
-
return record
|
|
145
|
-
}
|
|
146
|
-
const isInclude = projection[Object.keys(projection)[0]]
|
|
147
|
-
if (typeof projection._id === 'undefined') {
|
|
148
|
-
projection = {...projection, _id: 1}
|
|
149
|
-
}
|
|
150
|
-
const expanded = dot.object(projection)
|
|
151
|
-
const result = isInclude ? _include(record, expanded) : _exclude(record, expanded)
|
|
152
|
-
return result
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
exports.mongoUpdate = function(doc, update) {
|
|
156
|
-
return mongoQuery(doc, {}, update)
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
exports.normalizeOrderBy = function(orderby) {
|
|
160
|
-
if (Array.isArray(orderby)) {
|
|
161
|
-
orderby = orderby.map(function (ordering) {
|
|
162
|
-
if (typeof ordering === 'string') {
|
|
163
|
-
return [ordering, 1]
|
|
164
|
-
} else if (Array.isArray(ordering)) {
|
|
165
|
-
if (ordering.length === 1) {
|
|
166
|
-
return [ordering[0], 1] // add 1 (default to ascending sort)
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
return ordering
|
|
170
|
-
})
|
|
171
|
-
} else if (typeof orderby === 'object') {
|
|
172
|
-
const arr = []
|
|
173
|
-
for (const key in orderby) {
|
|
174
|
-
arr.push([key, orderby[key]])
|
|
175
|
-
}
|
|
176
|
-
orderby = arr
|
|
177
|
-
} else {
|
|
178
|
-
throw exports.ApiError(400, 'orderby param must be array or object')
|
|
179
|
-
}
|
|
180
|
-
return orderby
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
exports.getPath = (obj, path, defaultValue) => {
|
|
184
|
-
const result = String.prototype.split.call(path, /[,[\].]+?/)
|
|
185
|
-
.filter(Boolean)
|
|
186
|
-
.reduce((res, key) => (res !== null && res !== undefined) ? res[key] : res, obj)
|
|
187
|
-
return (result === undefined || result === obj) ? defaultValue : result
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
exports.castArray = function(arr) {
|
|
191
|
-
return Array.isArray(arr) ? arr : [arr]
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
exports.clone = function (obj) {
|
|
195
|
-
if (!obj) {
|
|
196
|
-
return obj
|
|
197
|
-
}
|
|
198
|
-
return JSON.parse(JSON.stringify(obj))
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
exports.createSecureRandomId = function() {
|
|
202
|
-
return crypto.randomBytes(24).toString('hex')
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
const
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
:
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
exports.
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
exports.
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
function
|
|
407
|
-
const
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
exports.createPagination = function createPagination (data, page, limit) {
|
|
427
|
-
const pagination = {
|
|
428
|
-
page: parseInt(page),
|
|
429
|
-
itemsTotal: data.length,
|
|
430
|
-
itemsPerPage: limit,
|
|
431
|
-
pages: Math.ceil(data.length / limit)
|
|
432
|
-
}
|
|
433
|
-
pagination.page = pagination.page > pagination.pages ? pagination.pages + 1 : pagination.page
|
|
434
|
-
if (pagination.page < pagination.pages) {
|
|
435
|
-
pagination.pageNext = pagination.page + 1
|
|
436
|
-
}
|
|
437
|
-
if (pagination.page - 1 > 0) {
|
|
438
|
-
pagination.pagePrev = pagination.page - 1
|
|
439
|
-
}
|
|
440
|
-
pagination.data = data.splice((pagination.page - 1) * limit, limit)
|
|
441
|
-
return pagination
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
exports.createPagePagination = function createPagePagination (pageData, page, pageItems, totalItems) {
|
|
445
|
-
const pages = totalItems >= 0 ? Math.ceil(totalItems / pageItems) : undefined
|
|
446
|
-
page = parseInt(page)
|
|
447
|
-
page = page > pages ? pages + 1 : page
|
|
448
|
-
return {
|
|
449
|
-
data: pageData,
|
|
450
|
-
page: page > pages ? pages + 1 : page,
|
|
451
|
-
itemsPerPage: pageItems,
|
|
452
|
-
itemsTotal: totalItems,
|
|
453
|
-
pages,
|
|
454
|
-
pageNext: page < pages ? page + 1 : undefined,
|
|
455
|
-
pagePrev: page - 1 > 0 ? page - 1 : undefined,
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
exports.getDatabaseOffset = function getDatabaseOffset(page, itemsPerPage) {
|
|
460
|
-
return (page - 1) * itemsPerPage
|
|
461
|
-
}
|
|
1
|
+
const randomstring = require('randomstring')
|
|
2
|
+
const {v4} = require('uuid')
|
|
3
|
+
const debug = require('debug')('expressa')
|
|
4
|
+
const crypto = require('crypto')
|
|
5
|
+
const pg = require('pg')
|
|
6
|
+
const pgPools = {}
|
|
7
|
+
const dot = require('dot-object')
|
|
8
|
+
const mongoQuery = require('mongo-query')
|
|
9
|
+
const Ajv = require('ajv')
|
|
10
|
+
const ajv = new Ajv({
|
|
11
|
+
allErrors: true,
|
|
12
|
+
strict: 'log',
|
|
13
|
+
strictSchema: 'log',
|
|
14
|
+
validateFormats: false,
|
|
15
|
+
allowUnionTypes: true,
|
|
16
|
+
})
|
|
17
|
+
ajv.addKeyword({
|
|
18
|
+
keyword: 'links',
|
|
19
|
+
type: 'string',
|
|
20
|
+
schemaType: 'array',
|
|
21
|
+
})
|
|
22
|
+
ajv.addKeyword({
|
|
23
|
+
keyword: 'media',
|
|
24
|
+
type: 'string',
|
|
25
|
+
schemaType: 'object',
|
|
26
|
+
})
|
|
27
|
+
ajv.addKeyword({
|
|
28
|
+
keyword: 'propertyOrder',
|
|
29
|
+
schemaType: 'number',
|
|
30
|
+
})
|
|
31
|
+
const formatKey = ajv.getKeyword('format')
|
|
32
|
+
formatKey.type = formatKey.type.concat(['array', 'boolean', 'object'])
|
|
33
|
+
|
|
34
|
+
const schemaValidators = {}
|
|
35
|
+
|
|
36
|
+
exports.addSchemaValidator = function(collection, schema) {
|
|
37
|
+
schemaValidators[collection] = ajv.compile(schema)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
exports.validateSchema = function(collection, doc) {
|
|
41
|
+
if (!schemaValidators[collection]) {
|
|
42
|
+
console.error(`missing schema validator for collection ${collection}`)
|
|
43
|
+
return true
|
|
44
|
+
}
|
|
45
|
+
const isValid = schemaValidators[collection](doc)
|
|
46
|
+
if (!isValid) {
|
|
47
|
+
throw new ApiError(400, schemaValidators[collection].errors.map((err) => `${err.instancePath} ${err.message}: ${JSON.stringify(err.params)}`).join(', '))
|
|
48
|
+
}
|
|
49
|
+
return true
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
exports.orderBy = function (data, orderby) {
|
|
53
|
+
data.sort(function compare (a, b) {
|
|
54
|
+
for (let i = 0; i < orderby.length; i++) {
|
|
55
|
+
const ordering = orderby[i]
|
|
56
|
+
const key = ordering[0]
|
|
57
|
+
if (exports.getPath(a, key) > exports.getPath(b, key)) {
|
|
58
|
+
return ordering[1]
|
|
59
|
+
} else if (exports.getPath(a, key) < exports.getPath(b, key)) {
|
|
60
|
+
return -ordering[1]
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return 0
|
|
64
|
+
})
|
|
65
|
+
return data
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
exports.getArrayPaths = function (key, value) {
|
|
69
|
+
if (!value)
|
|
70
|
+
return []
|
|
71
|
+
if (value.type === 'array')
|
|
72
|
+
return [key].concat(exports.getArrayPaths(key, value.items)).filter(el => el)
|
|
73
|
+
if (value.type === 'object') {
|
|
74
|
+
return Object.entries(value.properties)
|
|
75
|
+
.map(([key2, value]) => {
|
|
76
|
+
return exports.getArrayPaths(key ? key + '.' + key2 : key2, value)
|
|
77
|
+
})
|
|
78
|
+
.flat()
|
|
79
|
+
.filter(el => el)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function isObject(obj) {
|
|
84
|
+
return typeof obj === 'object'
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Returns an excerpt of the selected document based on an include projection.
|
|
89
|
+
*
|
|
90
|
+
* Note:
|
|
91
|
+
* It includes the fields listed in the projection with a value of 1. the
|
|
92
|
+
* unknown fields or the fields with a value other than 1 are ignored.
|
|
93
|
+
* @param {Object} the selected document,
|
|
94
|
+
* @param {Object} the projection to apply,
|
|
95
|
+
*/
|
|
96
|
+
function _include(obj, source) {
|
|
97
|
+
const data = {}
|
|
98
|
+
for (const prop in source) {
|
|
99
|
+
if (typeof obj[prop] !== 'undefined') {
|
|
100
|
+
if (isObject(source[prop])) {
|
|
101
|
+
if (Array.isArray(obj[prop])) {
|
|
102
|
+
data[prop] = obj[prop].map((element) => _include(element, source[prop]))
|
|
103
|
+
} else {
|
|
104
|
+
data[prop] = _include(obj[prop], source[prop])
|
|
105
|
+
}
|
|
106
|
+
} else if (source[prop] == 1) {
|
|
107
|
+
if (isObject(obj[prop])) {
|
|
108
|
+
data[prop] = exports.clone(obj[prop])
|
|
109
|
+
} else if (typeof obj[prop] !== 'undefined') {
|
|
110
|
+
data[prop] = obj[prop]
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return data
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Returns an excerpt of the selected document based on an exclude projection.
|
|
120
|
+
*
|
|
121
|
+
* Note:
|
|
122
|
+
* It excludes the fields listed in the projection with a value of 0. The
|
|
123
|
+
* unspecified fields are kept.
|
|
124
|
+
*/
|
|
125
|
+
function _exclude(obj, source) {
|
|
126
|
+
const data = {}
|
|
127
|
+
for (const prop in obj) {
|
|
128
|
+
if (source[prop] !== undefined && !source[prop]) {
|
|
129
|
+
if (isObject(source[prop])) {
|
|
130
|
+
data[prop] = {}
|
|
131
|
+
_exclude(obj[prop], source[prop], data[prop])
|
|
132
|
+
}
|
|
133
|
+
} else if (isObject(obj[prop])) {
|
|
134
|
+
data[prop] = exports.clone(obj[prop])
|
|
135
|
+
} else {
|
|
136
|
+
data[prop] = obj[prop]
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return data
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
exports.mongoProject = function(record, projection) {
|
|
143
|
+
if (!projection || Object.keys(projection).length < 1) {
|
|
144
|
+
return record
|
|
145
|
+
}
|
|
146
|
+
const isInclude = projection[Object.keys(projection)[0]]
|
|
147
|
+
if (typeof projection._id === 'undefined') {
|
|
148
|
+
projection = {...projection, _id: 1}
|
|
149
|
+
}
|
|
150
|
+
const expanded = dot.object(projection)
|
|
151
|
+
const result = isInclude ? _include(record, expanded) : _exclude(record, expanded)
|
|
152
|
+
return result
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
exports.mongoUpdate = function(doc, update) {
|
|
156
|
+
return mongoQuery(doc, {}, update)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
exports.normalizeOrderBy = function(orderby) {
|
|
160
|
+
if (Array.isArray(orderby)) {
|
|
161
|
+
orderby = orderby.map(function (ordering) {
|
|
162
|
+
if (typeof ordering === 'string') {
|
|
163
|
+
return [ordering, 1]
|
|
164
|
+
} else if (Array.isArray(ordering)) {
|
|
165
|
+
if (ordering.length === 1) {
|
|
166
|
+
return [ordering[0], 1] // add 1 (default to ascending sort)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return ordering
|
|
170
|
+
})
|
|
171
|
+
} else if (typeof orderby === 'object') {
|
|
172
|
+
const arr = []
|
|
173
|
+
for (const key in orderby) {
|
|
174
|
+
arr.push([key, orderby[key]])
|
|
175
|
+
}
|
|
176
|
+
orderby = arr
|
|
177
|
+
} else {
|
|
178
|
+
throw exports.ApiError(400, 'orderby param must be array or object')
|
|
179
|
+
}
|
|
180
|
+
return orderby
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
exports.getPath = (obj, path, defaultValue) => {
|
|
184
|
+
const result = String.prototype.split.call(path, /[,[\].]+?/)
|
|
185
|
+
.filter(Boolean)
|
|
186
|
+
.reduce((res, key) => (res !== null && res !== undefined) ? res[key] : res, obj)
|
|
187
|
+
return (result === undefined || result === obj) ? defaultValue : result
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
exports.castArray = function(arr) {
|
|
191
|
+
return Array.isArray(arr) ? arr : [arr]
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
exports.clone = function (obj) {
|
|
195
|
+
if (!obj) {
|
|
196
|
+
return obj
|
|
197
|
+
}
|
|
198
|
+
return JSON.parse(JSON.stringify(obj))
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
exports.createSecureRandomId = function() {
|
|
202
|
+
return crypto.randomBytes(24).toString('hex')
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const severities = ['critical', 'error', 'warning', 'notice', 'info', 'debug']
|
|
206
|
+
exports.getLogSeverity = function (status) {
|
|
207
|
+
const severity = status >= 500 ? 'error'
|
|
208
|
+
: status >= 400 ? 'warning'
|
|
209
|
+
: status >= 300 ? 'notice'
|
|
210
|
+
: status >= 200 ? 'info'
|
|
211
|
+
: 'debug'
|
|
212
|
+
return severity
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
exports.shouldLogRequest = function (req, res) {
|
|
216
|
+
const severity = exports.getLogSeverity(res.statusCode)
|
|
217
|
+
const severityLoggingIndex = severities.indexOf(req.getSetting('logging_level') || 'warning')
|
|
218
|
+
const severityIndex = severities.indexOf(severity)
|
|
219
|
+
return severityIndex <= severityLoggingIndex
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function filterHeaders(req) {
|
|
223
|
+
const headers = req.headers || {}
|
|
224
|
+
return {
|
|
225
|
+
'user-agent': headers['user-agent'],
|
|
226
|
+
origin: headers['origin'],
|
|
227
|
+
referer: headers['referer'],
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
exports.createLogEntry = function (req, res) {
|
|
232
|
+
const severity = exports.getLogSeverity(res.statusCode)
|
|
233
|
+
return {
|
|
234
|
+
severity: severity,
|
|
235
|
+
user: req.uid,
|
|
236
|
+
user_collection: req.ucollection,
|
|
237
|
+
url: decodeURI(req.originalUrl || req.url),
|
|
238
|
+
method: req.method,
|
|
239
|
+
referer: req.headers['referer'],
|
|
240
|
+
req: {
|
|
241
|
+
ip: req.ip,
|
|
242
|
+
headers: filterHeaders(req),
|
|
243
|
+
},
|
|
244
|
+
res: {
|
|
245
|
+
statusCode: res.statusCode,
|
|
246
|
+
requestId: res.getHeader('x-request-id'),
|
|
247
|
+
headers: res.getHeaders(),
|
|
248
|
+
message: req.returnedError ? req.returnedError.result || req.returnedError.message : undefined,
|
|
249
|
+
tokenMessage: req.uerror,
|
|
250
|
+
},
|
|
251
|
+
meta: {
|
|
252
|
+
created: new Date().toISOString(),
|
|
253
|
+
updated: new Date().toISOString()
|
|
254
|
+
},
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
exports.notify = async function (event, req, collection, data) {
|
|
259
|
+
const listeners = req.eventListeners[event] || []
|
|
260
|
+
debug('notifying ' + listeners.length + ' of ' + event + ' for ' + collection)
|
|
261
|
+
let result
|
|
262
|
+
for (const listener of listeners) {
|
|
263
|
+
if (listener.collections && !listener.collections.includes(collection)) {
|
|
264
|
+
continue // skip since it's not relevant
|
|
265
|
+
}
|
|
266
|
+
debug('calling ' + listener.name + ' ' + (result ? '(skipped)' : ''))
|
|
267
|
+
try {
|
|
268
|
+
result = result || await listener(req, collection, data, { event })
|
|
269
|
+
} catch (e) {
|
|
270
|
+
if (e && e.message) {
|
|
271
|
+
console.error(e)
|
|
272
|
+
}
|
|
273
|
+
// If a listener has already allowed the request, do not error
|
|
274
|
+
if (!result) {
|
|
275
|
+
throw e
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return result || result === undefined
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
class ApiError extends Error {
|
|
283
|
+
constructor (status, message) {
|
|
284
|
+
super(message)
|
|
285
|
+
this.name = this.constructor.name
|
|
286
|
+
Error.captureStackTrace(this, this.constructor)
|
|
287
|
+
this.status = status || this.constructor.status || 500
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
exports.ApiError = ApiError
|
|
291
|
+
|
|
292
|
+
exports.asyncMiddleware = fn =>
|
|
293
|
+
(req, res, next) => {
|
|
294
|
+
Promise.resolve(fn(req, res, next))
|
|
295
|
+
.catch(next)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
exports.resolve = async function resolve (handler, app) {
|
|
299
|
+
if (typeof handler === 'function') {
|
|
300
|
+
return handler(app)
|
|
301
|
+
}
|
|
302
|
+
return handler
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg
|
|
306
|
+
const ARGUMENT_NAMES = /([^\s,]+)/g
|
|
307
|
+
exports.getFunctionParamNames = function getFunctionParamNames (func) {
|
|
308
|
+
const fnStr = func.toString().replace(STRIP_COMMENTS, '')
|
|
309
|
+
let result = fnStr.slice(fnStr.indexOf('(')+1, fnStr.indexOf(')')).match(ARGUMENT_NAMES)
|
|
310
|
+
if(result === null)
|
|
311
|
+
result = []
|
|
312
|
+
return result
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
exports.friendlyDuration = function friendlyDuration (seconds) {
|
|
316
|
+
if (seconds > 86400) {
|
|
317
|
+
return Math.round(seconds / 86400) + ' hours'
|
|
318
|
+
}
|
|
319
|
+
if (seconds > 3600) {
|
|
320
|
+
return Math.round(seconds / 3600) + ' hours'
|
|
321
|
+
}
|
|
322
|
+
if (seconds > 60) {
|
|
323
|
+
return Math.round(seconds / 60) + ' minutes'
|
|
324
|
+
}
|
|
325
|
+
return Math.round(seconds) + ' seconds'
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
exports.getPgPool = function getPgPool(connectionString) {
|
|
329
|
+
if (!pgPools[connectionString]) {
|
|
330
|
+
pgPools[connectionString] = new pg.Pool({ connectionString: connectionString })
|
|
331
|
+
}
|
|
332
|
+
return pgPools[connectionString]
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
exports.generateDocumentId = function generateDocumentId() {
|
|
336
|
+
return v4()
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
exports.addIdIfMissing = function addIdIfMissing (document) {
|
|
340
|
+
if (!document._id) {
|
|
341
|
+
document._id = exports.generateDocumentId()
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
exports.sortObjectKeys = function sortObjectKeys(object) {
|
|
346
|
+
if (typeof object != 'object' || object instanceof Array || !object) { // Do not sort the array
|
|
347
|
+
return object
|
|
348
|
+
}
|
|
349
|
+
const keys = Object.keys(object)
|
|
350
|
+
keys.sort()
|
|
351
|
+
const newObject = {}
|
|
352
|
+
for (let i = 0; i < keys.length; i++){
|
|
353
|
+
newObject[keys[i]] = exports.sortObjectKeys(object[keys[i]])
|
|
354
|
+
}
|
|
355
|
+
return newObject
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
exports.getLoginCollections = async function(api) {
|
|
359
|
+
const all = await api.db.collection.all()
|
|
360
|
+
return all.length > 0 ? all.filter((coll) => isValidLoginCollection(coll)) : [{
|
|
361
|
+
_id: 'users',
|
|
362
|
+
enableLogin: true
|
|
363
|
+
}]
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// really being over cautious here to prevent collections
|
|
367
|
+
// unkowingly creating insecure access to database
|
|
368
|
+
function isValidLoginCollection(collection) {
|
|
369
|
+
const name = collection._id
|
|
370
|
+
if(collection.enableLogin === true) {
|
|
371
|
+
const properties = collection.schema?.properties
|
|
372
|
+
if (properties && properties.password && properties.roles) {
|
|
373
|
+
const required = collection.schema?.required
|
|
374
|
+
if (required?.includes('password')) {
|
|
375
|
+
return true
|
|
376
|
+
}
|
|
377
|
+
else {
|
|
378
|
+
console.error(`Login Collection Failed: "${name}" schema properties email and password must be listed as 'required'`)
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
console.error(`Login Collection Failed: "${name}" email, password and roles are mandatory schema properties`)
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return false
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
exports.createPagination = function createPagination (data, page, limit) {
|
|
389
|
+
const pagination = {
|
|
390
|
+
page: parseInt(page),
|
|
391
|
+
itemsTotal: data.length,
|
|
392
|
+
itemsPerPage: limit,
|
|
393
|
+
pages: Math.ceil(data.length / limit)
|
|
394
|
+
}
|
|
395
|
+
pagination.page = pagination.page > pagination.pages ? pagination.pages + 1 : pagination.page
|
|
396
|
+
if (pagination.page < pagination.pages) {
|
|
397
|
+
pagination.pageNext = pagination.page + 1
|
|
398
|
+
}
|
|
399
|
+
if (pagination.page - 1 > 0) {
|
|
400
|
+
pagination.pagePrev = pagination.page - 1
|
|
401
|
+
}
|
|
402
|
+
pagination.data = data.splice((pagination.page - 1) * limit, limit)
|
|
403
|
+
return pagination
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
exports.createPagePagination = function createPagePagination (pageData, page, pageItems, totalItems) {
|
|
407
|
+
const pages = totalItems >= 0 ? Math.ceil(totalItems / pageItems) : undefined
|
|
408
|
+
page = parseInt(page)
|
|
409
|
+
page = page > pages ? pages + 1 : page
|
|
410
|
+
return {
|
|
411
|
+
data: pageData,
|
|
412
|
+
page: page > pages ? pages + 1 : page,
|
|
413
|
+
itemsPerPage: pageItems,
|
|
414
|
+
itemsTotal: totalItems,
|
|
415
|
+
pages,
|
|
416
|
+
pageNext: page < pages ? page + 1 : undefined,
|
|
417
|
+
pagePrev: page - 1 > 0 ? page - 1 : undefined,
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
exports.getDatabaseOffset = function getDatabaseOffset(page, itemsPerPage) {
|
|
422
|
+
return (page - 1) * itemsPerPage
|
|
423
|
+
}
|