expressa 1.4.6 → 1.4.7

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