expressa 1.4.5 → 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.
- package/controllers/collections.js +18 -3
- package/controllers/users.js +52 -50
- package/listeners_users.js +4 -0
- package/listeners_validation.js +3 -36
- package/package.json +2 -2
- package/util.js +461 -419
|
@@ -92,13 +92,15 @@ exports.get = async function (req) {
|
|
|
92
92
|
exports.insert = async function (req) {
|
|
93
93
|
assertValidCollection(req)
|
|
94
94
|
const data = req.body
|
|
95
|
+
req.customResponseData = req.customResponseData || {}
|
|
95
96
|
await util.notify('post', req, req.params.collection, data)
|
|
96
97
|
|
|
97
98
|
const id = await req.db[req.params.collection].create(data)
|
|
98
99
|
await util.notify('changed', req, req.params.collection, data)
|
|
99
100
|
return {
|
|
100
101
|
status: 'OK',
|
|
101
|
-
id: id
|
|
102
|
+
id: id,
|
|
103
|
+
...req.customResponseData,
|
|
102
104
|
}
|
|
103
105
|
}
|
|
104
106
|
|
|
@@ -113,6 +115,7 @@ exports.getById = async function (req) {
|
|
|
113
115
|
|
|
114
116
|
exports.replaceById = async function (req) {
|
|
115
117
|
assertValidCollection(req)
|
|
118
|
+
req.customResponseData = req.customResponseData || {}
|
|
116
119
|
let oldDoc = {}
|
|
117
120
|
try {
|
|
118
121
|
oldDoc = await req.db[req.params.collection].get(req.params.id)
|
|
@@ -130,12 +133,14 @@ exports.replaceById = async function (req) {
|
|
|
130
133
|
await util.notify('changed', req, req.params.collection, req.body)
|
|
131
134
|
return {
|
|
132
135
|
status: 'OK',
|
|
133
|
-
id: data._id
|
|
136
|
+
id: data._id,
|
|
137
|
+
...req.customResponseData,
|
|
134
138
|
}
|
|
135
139
|
}
|
|
136
140
|
|
|
137
141
|
exports.updateById = async function (req) {
|
|
138
142
|
assertValidCollection(req)
|
|
143
|
+
req.customResponseData = req.customResponseData || {}
|
|
139
144
|
const modifier = req.body
|
|
140
145
|
|
|
141
146
|
const doc = await req.db[req.params.collection].get(req.params.id)
|
|
@@ -152,15 +157,25 @@ exports.updateById = async function (req) {
|
|
|
152
157
|
req.body.meta.owner = newOwner
|
|
153
158
|
await req.db[req.params.collection].update(req.params.id, req.body)
|
|
154
159
|
await util.notify('changed', req, req.params.collection, req.body)
|
|
160
|
+
if (Object.keys(req.customResponseData)) {
|
|
161
|
+
return {
|
|
162
|
+
...doc,
|
|
163
|
+
...req.customResponseData,
|
|
164
|
+
}
|
|
165
|
+
}
|
|
155
166
|
return doc
|
|
156
167
|
}
|
|
157
168
|
|
|
158
169
|
exports.deleteById = async function (req) {
|
|
159
170
|
assertValidCollection(req)
|
|
171
|
+
req.customResponseData = req.customResponseData || {}
|
|
160
172
|
const doc = await req.db[req.params.collection].get(req.params.id)
|
|
161
173
|
await util.notify('delete', req, req.params.collection, doc)
|
|
162
174
|
|
|
163
175
|
await req.db[req.params.collection].delete(req.params.id)
|
|
164
176
|
await util.notify('deleted', req, req.params.collection, doc)
|
|
165
|
-
return {
|
|
177
|
+
return {
|
|
178
|
+
status: 'OK',
|
|
179
|
+
...req.customResponseData,
|
|
180
|
+
}
|
|
166
181
|
}
|
package/controllers/users.js
CHANGED
|
@@ -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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
req.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
+
}
|
package/listeners_users.js
CHANGED
|
@@ -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) {
|
package/listeners_validation.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"dot-object": "^2.1.4",
|
|
16
16
|
"express": "^4.17.1",
|
|
17
17
|
"jfs": "^0.3.0",
|
|
18
|
-
"jsonwebtoken": "^
|
|
18
|
+
"jsonwebtoken": "^9.0.0",
|
|
19
19
|
"mongo-query": "^0.5.7",
|
|
20
20
|
"mongo-query-to-postgres-jsonb": "^0.2.9",
|
|
21
21
|
"mongo-querystring": "4.1.1",
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
function
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
return
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
exports.
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
})
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
}
|
|
198
|
-
return
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
const
|
|
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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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
|
+
}
|