expressa 1.4.7 → 2.0.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/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
- exports.getUserWithPermissions = async function (api, permissions) {
206
- if (typeof permissions === 'string') {
207
- permissions = [permissions]
208
- }
209
- permissions = permissions || []
210
- const permissionsMap = {}
211
- permissions.forEach(function (permission) {
212
- permissionsMap[permission] = true
213
- })
214
- const randId = randomstring.generate(12)
215
- const roleName = 'role' + randId
216
- const now = new Date().toISOString()
217
- const user = {
218
- email: 'test' + randId + '@example.com',
219
- password: '123',
220
- collection: 'users',
221
- roles: [roleName],
222
- meta: {
223
- created: now,
224
- updated: now,
225
- password_last_updated_at: now,
226
- }
227
- }
228
- await api.db.role.cache.create({
229
- _id: roleName,
230
- permissions: permissionsMap
231
- })
232
- const result = await api.db.users.create(user)
233
- user._id = result
234
- const payload = api.util.doLogin({
235
- id: user._id,
236
- collection: 'users',
237
- timestamp: user.meta.password_last_updated_at,
238
- jwt_secret: api.settings.jwt_secret,
239
- })
240
- return payload.token
241
- }
242
-
243
- const severities = ['critical', 'error', 'warning', 'notice', 'info', 'debug']
244
- exports.getLogSeverity = function (status) {
245
- const severity = status >= 500 ? 'error'
246
- : status >= 400 ? 'warning'
247
- : status >= 300 ? 'notice'
248
- : status >= 200 ? 'info'
249
- : 'debug'
250
- return severity
251
- }
252
-
253
- exports.shouldLogRequest = function (req, res) {
254
- const severity = exports.getLogSeverity(res.statusCode)
255
- const severityLoggingIndex = severities.indexOf(req.getSetting('logging_level') || 'warning')
256
- const severityIndex = severities.indexOf(severity)
257
- return severityIndex <= severityLoggingIndex
258
- }
259
-
260
- function filterHeaders(req) {
261
- const headers = req.headers || {}
262
- return {
263
- 'user-agent': headers['user-agent'],
264
- origin: headers['origin'],
265
- referer: headers['referer'],
266
- }
267
- }
268
-
269
- exports.createLogEntry = function (req, res) {
270
- const severity = exports.getLogSeverity(res.statusCode)
271
- return {
272
- severity: severity,
273
- user: req.uid,
274
- user_collection: req.ucollection,
275
- url: decodeURI(req.originalUrl || req.url),
276
- method: req.method,
277
- referer: req.headers['referer'],
278
- req: {
279
- ip: req.ip,
280
- headers: filterHeaders(req),
281
- },
282
- res: {
283
- statusCode: res.statusCode,
284
- requestId: res.getHeader('x-request-id'),
285
- headers: res.getHeaders(),
286
- message: req.returnedError ? req.returnedError.result || req.returnedError.message : undefined,
287
- tokenMessage: req.uerror,
288
- },
289
- meta: {
290
- created: new Date().toISOString(),
291
- updated: new Date().toISOString()
292
- },
293
- }
294
- }
295
-
296
- exports.notify = async function (event, req, collection, data) {
297
- const listeners = req.eventListeners[event] || []
298
- debug('notifying ' + listeners.length + ' of ' + event + ' for ' + collection)
299
- let result
300
- for (const listener of listeners) {
301
- if (listener.collections && !listener.collections.includes(collection)) {
302
- continue // skip since it's not relevant
303
- }
304
- debug('calling ' + listener.name + ' ' + (result ? '(skipped)' : ''))
305
- try {
306
- result = result || await listener(req, collection, data, { event })
307
- } catch (e) {
308
- if (e && e.message) {
309
- console.error(e)
310
- }
311
- // If a listener has already allowed the request, do not error
312
- if (!result) {
313
- throw e
314
- }
315
- }
316
- }
317
- return result || result === undefined
318
- }
319
-
320
- class ApiError extends Error {
321
- constructor (status, message) {
322
- super(message)
323
- this.name = this.constructor.name
324
- Error.captureStackTrace(this, this.constructor)
325
- this.status = status || this.constructor.status || 500
326
- }
327
- }
328
- exports.ApiError = ApiError
329
-
330
- exports.asyncMiddleware = fn =>
331
- (req, res, next) => {
332
- Promise.resolve(fn(req, res, next))
333
- .catch(next)
334
- }
335
-
336
- exports.resolve = async function resolve (handler, app) {
337
- if (typeof handler === 'function') {
338
- return handler(app)
339
- }
340
- return handler
341
- }
342
-
343
- const STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg
344
- const ARGUMENT_NAMES = /([^\s,]+)/g
345
- exports.getFunctionParamNames = function getFunctionParamNames (func) {
346
- const fnStr = func.toString().replace(STRIP_COMMENTS, '')
347
- let result = fnStr.slice(fnStr.indexOf('(')+1, fnStr.indexOf(')')).match(ARGUMENT_NAMES)
348
- if(result === null)
349
- result = []
350
- return result
351
- }
352
-
353
- exports.friendlyDuration = function friendlyDuration (seconds) {
354
- if (seconds > 86400) {
355
- return Math.round(seconds / 86400) + ' hours'
356
- }
357
- if (seconds > 3600) {
358
- return Math.round(seconds / 3600) + ' hours'
359
- }
360
- if (seconds > 60) {
361
- return Math.round(seconds / 60) + ' minutes'
362
- }
363
- return Math.round(seconds) + ' seconds'
364
- }
365
-
366
- exports.getPgPool = function getPgPool(connectionString) {
367
- if (!pgPools[connectionString]) {
368
- pgPools[connectionString] = new pg.Pool({ connectionString: connectionString })
369
- }
370
- return pgPools[connectionString]
371
- }
372
-
373
- exports.generateDocumentId = function generateDocumentId() {
374
- return v4()
375
- }
376
-
377
- exports.addIdIfMissing = function addIdIfMissing (document) {
378
- if (!document._id) {
379
- document._id = exports.generateDocumentId()
380
- }
381
- }
382
-
383
- exports.sortObjectKeys = function sortObjectKeys(object) {
384
- if (typeof object != 'object' || object instanceof Array) { // Do not sort the array
385
- return object
386
- }
387
- const keys = Object.keys(object)
388
- keys.sort()
389
- const newObject = {}
390
- for (let i = 0; i < keys.length; i++){
391
- newObject[keys[i]] = exports.sortObjectKeys(object[keys[i]])
392
- }
393
- return newObject
394
- }
395
-
396
- exports.getLoginCollections = async function(api) {
397
- const all = await api.db.collection.all()
398
- return all.length > 0 ? all.filter((coll) => isValidLoginCollection(coll)) : [{
399
- _id: 'users',
400
- enableLogin: true
401
- }]
402
- }
403
-
404
- // really being over cautious here to prevent collections
405
- // unkowingly creating insecure access to database
406
- function isValidLoginCollection(collection) {
407
- const name = collection._id
408
- if(collection.enableLogin === true) {
409
- const properties = collection.schema && collection.schema.properties
410
- if (properties && properties.email && properties.password && properties.roles) {
411
- const required = collection.schema && collection.schema.required
412
- if (required && required.includes('email') && required.includes('password')) {
413
- return true
414
- }
415
- else {
416
- console.error(`Login Collection Failed: "${name}" schema properties email and password must be listed as 'required'`)
417
- }
418
- }
419
- else {
420
- console.error(`Login Collection Failed: "${name}" email, password and roles are mandatory schema properties`)
421
- }
422
- }
423
- return false
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
+ }