expressa 2.0.14 → 2.0.16

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.
Files changed (206) hide show
  1. package/.circleci/config.yml +21 -21
  2. package/.eslintrc.json +29 -29
  3. package/LICENSE +21 -21
  4. package/README.md +137 -137
  5. package/auth/index.js +76 -76
  6. package/auth/jwt.js +31 -31
  7. package/controllers/collections.js +258 -258
  8. package/controllers/install.js +105 -105
  9. package/controllers/status.js +43 -43
  10. package/controllers/users.js +52 -52
  11. package/cypress/fixtures/example.json +4 -4
  12. package/cypress/integration/0_install_spec.js +8 -8
  13. package/cypress/integration/custom_collections.js +28 -28
  14. package/cypress/integration/home.js +9 -9
  15. package/cypress/integration/settings.js +34 -34
  16. package/cypress/integration/users.js +61 -61
  17. package/cypress/plugins/index.js +17 -17
  18. package/cypress/support/commands.js +54 -54
  19. package/cypress/support/index.js +20 -20
  20. package/cypress/support/util.js +10 -10
  21. package/cypress.json +4 -4
  22. package/db/cached.js +40 -40
  23. package/db/file.js +112 -112
  24. package/db/memory.js +79 -79
  25. package/db/mongo.js +86 -86
  26. package/db/postgres.js +93 -93
  27. package/doc/authentication.md +38 -38
  28. package/doc/automatic-fields.md +12 -12
  29. package/doc/blogexample.md +16 -16
  30. package/doc/custom-endpoints.md +20 -20
  31. package/doc/database.md +23 -23
  32. package/doc/development.md +33 -33
  33. package/doc/listeners.md +103 -103
  34. package/doc/modules.md +8 -8
  35. package/doc/permissions.md +14 -14
  36. package/doc/relationships.md +65 -65
  37. package/doc/testing.md +47 -47
  38. package/doc/uploading-files.md +53 -53
  39. package/index.js +297 -297
  40. package/listeners.js +65 -65
  41. package/listeners_collection_permissions.js +29 -29
  42. package/listeners_users.js +96 -96
  43. package/listeners_validation.js +43 -43
  44. package/middleware/logging.js +30 -30
  45. package/middleware/permissions.js +41 -47
  46. package/modules/access_keys/access_keys.js +32 -32
  47. package/modules/admin/.babelrc +12 -12
  48. package/modules/admin/.editorconfig +14 -14
  49. package/modules/admin/.eslintignore +5 -5
  50. package/modules/admin/.eslintrc.js +197 -197
  51. package/modules/admin/.postcssrc.js +10 -10
  52. package/modules/admin/.travis.yml +5 -5
  53. package/modules/admin/LICENSE +21 -21
  54. package/modules/admin/README.md +88 -88
  55. package/modules/admin/build/build.js +45 -45
  56. package/modules/admin/build/check-versions.js +64 -64
  57. package/modules/admin/build/utils.js +108 -108
  58. package/modules/admin/build/vue-loader.conf.js +5 -5
  59. package/modules/admin/build/webpack.base.conf.js +108 -108
  60. package/modules/admin/build/webpack.dev.conf.js +95 -95
  61. package/modules/admin/build/webpack.prod.conf.js +171 -171
  62. package/modules/admin/config/dev.env.js +8 -8
  63. package/modules/admin/config/index.js +86 -86
  64. package/modules/admin/config/prod.env.js +5 -5
  65. package/modules/admin/index.html +16 -16
  66. package/modules/admin/package.json +98 -98
  67. package/modules/admin/src/App.vue +11 -11
  68. package/modules/admin/src/api/login.js +26 -26
  69. package/modules/admin/src/api/table.js +9 -9
  70. package/modules/admin/src/components/Breadcrumb/index.vue +79 -79
  71. package/modules/admin/src/components/Hamburger/index.vue +62 -62
  72. package/modules/admin/src/components/JSONEditor.vue +180 -180
  73. package/modules/admin/src/components/SvgIcon/index.vue +43 -43
  74. package/modules/admin/src/icons/index.js +9 -9
  75. package/modules/admin/src/icons/svgo.yml +22 -22
  76. package/modules/admin/src/permission.js +49 -49
  77. package/modules/admin/src/router/index.js +200 -200
  78. package/modules/admin/src/store/getters.js +11 -11
  79. package/modules/admin/src/store/index.js +17 -17
  80. package/modules/admin/src/store/modules/app.js +43 -43
  81. package/modules/admin/src/store/modules/user.js +104 -104
  82. package/modules/admin/src/styles/element-ui.scss +29 -29
  83. package/modules/admin/src/styles/index.scss +78 -78
  84. package/modules/admin/src/styles/mixin.scss +27 -27
  85. package/modules/admin/src/styles/sidebar.scss +133 -133
  86. package/modules/admin/src/styles/transition.scss +46 -46
  87. package/modules/admin/src/styles/variables.scss +4 -4
  88. package/modules/admin/src/utils/auth.js +15 -15
  89. package/modules/admin/src/utils/index.js +74 -74
  90. package/modules/admin/src/utils/request.js +48 -48
  91. package/modules/admin/src/utils/validate.js +31 -31
  92. package/modules/admin/src/views/404.vue +235 -235
  93. package/modules/admin/src/views/Dev.vue +33 -33
  94. package/modules/admin/src/views/EditDocument.vue +116 -116
  95. package/modules/admin/src/views/Endpoints.vue +279 -279
  96. package/modules/admin/src/views/Home.vue +84 -84
  97. package/modules/admin/src/views/Install.vue +79 -79
  98. package/modules/admin/src/views/ListDocuments.vue +72 -72
  99. package/modules/admin/src/views/ListDocuments2.vue +462 -462
  100. package/modules/admin/src/views/ManageListeners.vue +67 -67
  101. package/modules/admin/src/views/ManageMiddleware.vue +43 -43
  102. package/modules/admin/src/views/ManagePermissions.vue +75 -75
  103. package/modules/admin/src/views/ViewRequest.vue +77 -77
  104. package/modules/admin/src/views/form/index.vue +91 -91
  105. package/modules/admin/src/views/layout/Layout.vue +69 -69
  106. package/modules/admin/src/views/layout/components/AppMain.vue +41 -41
  107. package/modules/admin/src/views/layout/components/Navbar.vue +97 -97
  108. package/modules/admin/src/views/layout/components/Sidebar/Item.vue +29 -29
  109. package/modules/admin/src/views/layout/components/Sidebar/Link.vue +39 -39
  110. package/modules/admin/src/views/layout/components/Sidebar/SidebarItem.vue +103 -103
  111. package/modules/admin/src/views/layout/components/Sidebar/index.vue +62 -62
  112. package/modules/admin/src/views/layout/components/index.js +3 -3
  113. package/modules/admin/src/views/layout/mixin/ResizeHandler.js +41 -41
  114. package/modules/admin/src/views/login/index.vue +195 -195
  115. package/modules/admin/src/views/tree/index.vue +77 -77
  116. package/modules/collections/collections.js +39 -39
  117. package/modules/core/core.js +197 -197
  118. package/modules/logging/logging.js +88 -88
  119. package/modules/permissions/permissions.js +106 -106
  120. package/package.json +59 -59
  121. package/scripts/expressa +12 -12
  122. package/scripts/run_cypress_tests.sh +5 -5
  123. package/scripts/run_db_tests.sh +8 -8
  124. package/test/0-install.js +96 -96
  125. package/test/collections.js +431 -423
  126. package/test/collections.querying.js +289 -269
  127. package/test/db.js +258 -250
  128. package/test/db.strings.js +73 -73
  129. package/test/db.updating.js +144 -144
  130. package/test/logging.js +29 -29
  131. package/test/test.js +94 -94
  132. package/test/testserver.js +17 -17
  133. package/test/testutils.js +136 -136
  134. package/test/users.js +425 -425
  135. package/util.js +522 -504
  136. package/modules/admin/dist/favicon.ico +0 -0
  137. package/modules/admin/dist/index.html +0 -1
  138. package/modules/admin/dist/index.html.gz +0 -0
  139. package/modules/admin/dist/static/css/app.9dd57eea.css +0 -1
  140. package/modules/admin/dist/static/css/app.9dd57eea.css.gz +0 -0
  141. package/modules/admin/dist/static/css/chunk-5061.ce0e4bdb.css +0 -1
  142. package/modules/admin/dist/static/css/chunk-5061.ce0e4bdb.css.gz +0 -0
  143. package/modules/admin/dist/static/css/chunk-5b97.7f9cb53e.css +0 -1
  144. package/modules/admin/dist/static/css/chunk-5b97.7f9cb53e.css.gz +0 -0
  145. package/modules/admin/dist/static/css/chunk-621e.b826be8d.css +0 -1
  146. package/modules/admin/dist/static/css/chunk-621e.b826be8d.css.gz +0 -0
  147. package/modules/admin/dist/static/css/chunk-656b.9c828b00.css +0 -1
  148. package/modules/admin/dist/static/css/chunk-656b.9c828b00.css.gz +0 -0
  149. package/modules/admin/dist/static/css/chunk-7291.13514e45.css +0 -0
  150. package/modules/admin/dist/static/css/chunk-7291.13514e45.css.gz +0 -0
  151. package/modules/admin/dist/static/css/chunk-7f29.9c828b00.css +0 -1
  152. package/modules/admin/dist/static/css/chunk-7f29.9c828b00.css.gz +0 -0
  153. package/modules/admin/dist/static/css/chunk-9a95.c647a2ef.css +0 -1
  154. package/modules/admin/dist/static/css/chunk-9a95.c647a2ef.css.gz +0 -0
  155. package/modules/admin/dist/static/css/chunk-b423.5f239333.css +0 -0
  156. package/modules/admin/dist/static/css/chunk-b423.5f239333.css.gz +0 -0
  157. package/modules/admin/dist/static/css/chunk-bd5f.aefa49d7.css +0 -1
  158. package/modules/admin/dist/static/css/chunk-bd5f.aefa49d7.css.gz +0 -0
  159. package/modules/admin/dist/static/css/chunk-e374.025d58bb.css +0 -1
  160. package/modules/admin/dist/static/css/chunk-e374.025d58bb.css.gz +0 -0
  161. package/modules/admin/dist/static/css/chunk-elementUI.3d004d9f.css +0 -1
  162. package/modules/admin/dist/static/css/chunk-elementUI.3d004d9f.css.gz +0 -0
  163. package/modules/admin/dist/static/css/chunk-f075.3a895a1e.css +0 -1
  164. package/modules/admin/dist/static/css/chunk-f075.3a895a1e.css.gz +0 -0
  165. package/modules/admin/dist/static/css/chunk-libs.327ad89e.css +0 -14
  166. package/modules/admin/dist/static/css/chunk-libs.327ad89e.css.gz +0 -0
  167. package/modules/admin/dist/static/fonts/element-icons.6f0a763.ttf +0 -0
  168. package/modules/admin/dist/static/img/404.a57b6f3.png +0 -0
  169. package/modules/admin/dist/static/js/app.e9461593.js +0 -1
  170. package/modules/admin/dist/static/js/app.e9461593.js.gz +0 -0
  171. package/modules/admin/dist/static/js/chunk-09d8.561880e0.js +0 -2
  172. package/modules/admin/dist/static/js/chunk-09d8.561880e0.js.LICENSE.txt +0 -13
  173. package/modules/admin/dist/static/js/chunk-09d8.561880e0.js.LICENSE.txt.gz +0 -0
  174. package/modules/admin/dist/static/js/chunk-09d8.561880e0.js.gz +0 -0
  175. package/modules/admin/dist/static/js/chunk-5061.83e6ccfa.js +0 -1
  176. package/modules/admin/dist/static/js/chunk-5061.83e6ccfa.js.gz +0 -0
  177. package/modules/admin/dist/static/js/chunk-5b97.ac360086.js +0 -1
  178. package/modules/admin/dist/static/js/chunk-5b97.ac360086.js.gz +0 -0
  179. package/modules/admin/dist/static/js/chunk-5e17.a2b69611.js +0 -1
  180. package/modules/admin/dist/static/js/chunk-5e17.a2b69611.js.gz +0 -0
  181. package/modules/admin/dist/static/js/chunk-621e.ccf42393.js +0 -1
  182. package/modules/admin/dist/static/js/chunk-621e.ccf42393.js.gz +0 -0
  183. package/modules/admin/dist/static/js/chunk-656b.9d5b03c0.js +0 -1
  184. package/modules/admin/dist/static/js/chunk-656b.9d5b03c0.js.gz +0 -0
  185. package/modules/admin/dist/static/js/chunk-7291.590524e9.js +0 -1
  186. package/modules/admin/dist/static/js/chunk-7291.590524e9.js.gz +0 -0
  187. package/modules/admin/dist/static/js/chunk-7f29.6ffc142e.js +0 -1
  188. package/modules/admin/dist/static/js/chunk-7f29.6ffc142e.js.gz +0 -0
  189. package/modules/admin/dist/static/js/chunk-9a95.bdd76583.js +0 -1
  190. package/modules/admin/dist/static/js/chunk-9a95.bdd76583.js.gz +0 -0
  191. package/modules/admin/dist/static/js/chunk-b423.387c504c.js +0 -1
  192. package/modules/admin/dist/static/js/chunk-b423.387c504c.js.gz +0 -0
  193. package/modules/admin/dist/static/js/chunk-bd5f.e9509f10.js +0 -1
  194. package/modules/admin/dist/static/js/chunk-bd5f.e9509f10.js.gz +0 -0
  195. package/modules/admin/dist/static/js/chunk-e374.7c87a0dd.js +0 -1
  196. package/modules/admin/dist/static/js/chunk-e374.7c87a0dd.js.gz +0 -0
  197. package/modules/admin/dist/static/js/chunk-e7c3.e5df8ceb.js +0 -1
  198. package/modules/admin/dist/static/js/chunk-e7c3.e5df8ceb.js.gz +0 -0
  199. package/modules/admin/dist/static/js/chunk-elementUI.b0c99c08.js +0 -1
  200. package/modules/admin/dist/static/js/chunk-elementUI.b0c99c08.js.gz +0 -0
  201. package/modules/admin/dist/static/js/chunk-f075.315ceec1.js +0 -1
  202. package/modules/admin/dist/static/js/chunk-f075.315ceec1.js.gz +0 -0
  203. package/modules/admin/dist/static/js/chunk-libs.06c38828.js +0 -2
  204. package/modules/admin/dist/static/js/chunk-libs.06c38828.js.LICENSE.txt +0 -126
  205. package/modules/admin/dist/static/js/chunk-libs.06c38828.js.LICENSE.txt.gz +0 -0
  206. package/modules/admin/dist/static/js/chunk-libs.06c38828.js.gz +0 -0
package/util.js CHANGED
@@ -1,504 +1,522 @@
1
- const {v4} = require('uuid')
2
- const debug = require('debug')('expressa')
3
- const crypto = require('crypto')
4
- const pg = require('pg')
5
- const pgPools = {}
6
- const dot = require('dot-object')
7
- const mongoQuery = require('mongo-query')
8
- const mongoToPostgres = require('mongo-query-to-postgres-jsonb')
9
- const sift = require('sift')
10
- const auth = require('./auth/index')
11
- const Ajv = require('ajv')
12
- const ajv = new Ajv({
13
- allErrors: true,
14
- strict: 'log',
15
- strictSchema: 'log',
16
- validateFormats: false,
17
- allowUnionTypes: true,
18
- })
19
- ajv.addKeyword({
20
- keyword: 'links',
21
- type: 'string',
22
- schemaType: 'array',
23
- })
24
- ajv.addKeyword({
25
- keyword: 'media',
26
- type: 'string',
27
- schemaType: 'object',
28
- })
29
- ajv.addKeyword({
30
- keyword: 'propertyOrder',
31
- schemaType: 'number',
32
- })
33
- const formatKey = ajv.getKeyword('format')
34
- formatKey.type = formatKey.type.concat(['array', 'boolean', 'object'])
35
-
36
- const schemas = {}
37
- const schemaValidators = {}
38
-
39
- exports.addSchema = function(collection, schema) {
40
- schema.$id = collection
41
- ajv.removeSchema(collection)
42
- schemas[collection] = schema
43
- schemaValidators[collection] = ajv.compile(schema)
44
- delete schema.$id
45
- }
46
-
47
- exports.validateSchema = function(collection, doc) {
48
- if (!schemaValidators[collection]) {
49
- console.error(`missing schema validator for collection ${collection}`)
50
- return true
51
- }
52
- const isValid = schemaValidators[collection](doc)
53
- if (!isValid) {
54
- throw new ApiError(400, schemaValidators[collection].errors.map((err) => `${err.instancePath} ${err.message}: ${JSON.stringify(err.params)}`).join(', '))
55
- }
56
- return true
57
- }
58
-
59
- exports.validateSchemaProperty = function(collection, path, value) {
60
- if (!schemaValidators[collection]) {
61
- console.error(`missing schema validator for collection ${collection}`)
62
- return true
63
- }
64
- const $ref = `${collection}#/properties/${path.split('.').join('/properties/')}`
65
- const isValid = ajv.validate({ $ref }, value)
66
- if (!isValid) {
67
- throw new ApiError(400, ajv.errors.map((err) => `${err.instancePath} ${err.message}: ${JSON.stringify(err.params)}`).join(', '))
68
- }
69
- return true
70
- }
71
-
72
- exports.getSchemaProperty = function(collection, path) {
73
- const parts = path.split('.')
74
- let obj = schemas[collection]
75
- for (const part of parts) {
76
- if (obj.properties && obj.properties[part]) {
77
- obj = obj.properties[part]
78
- } else if (obj.items && obj.items.properties && obj.items.properties[part]) {
79
- obj = obj.items.properties[part]
80
- } else {
81
- return
82
- }
83
- }
84
- return obj
85
- }
86
-
87
- exports.orderBy = function (data, orderby) {
88
- data.sort(function compare (a, b) {
89
- for (let i = 0; i < orderby.length; i++) {
90
- const ordering = orderby[i]
91
- const key = ordering[0]
92
- if (exports.getPath(a, key) > exports.getPath(b, key)) {
93
- return ordering[1]
94
- } else if (exports.getPath(a, key) < exports.getPath(b, key)) {
95
- return -ordering[1]
96
- }
97
- }
98
- return 0
99
- })
100
- return data
101
- }
102
-
103
- exports.getArrayPaths = function (key, value) {
104
- if (!value)
105
- return []
106
- if (value.type === 'array')
107
- return [key].concat(exports.getArrayPaths(key, value.items)).filter(el => el)
108
- if (value.type === 'object') {
109
- return Object.entries(value.properties)
110
- .map(([key2, value]) => {
111
- return exports.getArrayPaths(key ? key + '.' + key2 : key2, value)
112
- })
113
- .flat()
114
- .filter(el => el)
115
- }
116
- }
117
-
118
- function isObject(obj) {
119
- return typeof obj === 'object'
120
- }
121
-
122
- /**
123
- * Returns an excerpt of the selected document based on an include projection.
124
- *
125
- * Note:
126
- * It includes the fields listed in the projection with a value of 1. the
127
- * unknown fields or the fields with a value other than 1 are ignored.
128
- * @param {Object} the selected document,
129
- * @param {Object} the projection to apply,
130
- */
131
- function _include(obj, source) {
132
- const data = {}
133
- for (const prop in source) {
134
- if (typeof obj[prop] !== 'undefined') {
135
- if (isObject(source[prop])) {
136
- if (Array.isArray(obj[prop])) {
137
- data[prop] = obj[prop].map((element) => _include(element, source[prop]))
138
- } else {
139
- data[prop] = _include(obj[prop], source[prop])
140
- }
141
- } else if (source[prop] == 1) {
142
- if (isObject(obj[prop])) {
143
- data[prop] = exports.clone(obj[prop])
144
- } else if (typeof obj[prop] !== 'undefined') {
145
- data[prop] = obj[prop]
146
- }
147
- }
148
- }
149
- }
150
- return data
151
- }
152
-
153
- /**
154
- * Returns an excerpt of the selected document based on an exclude projection.
155
- *
156
- * Note:
157
- * It excludes the fields listed in the projection with a value of 0. The
158
- * unspecified fields are kept.
159
- */
160
- function _exclude(obj, source) {
161
- const data = {}
162
- for (const prop in obj) {
163
- if (source[prop] !== undefined && !source[prop]) {
164
- if (isObject(source[prop])) {
165
- data[prop] = {}
166
- _exclude(obj[prop], source[prop], data[prop])
167
- }
168
- } else if (isObject(obj[prop])) {
169
- data[prop] = exports.clone(obj[prop])
170
- } else {
171
- data[prop] = obj[prop]
172
- }
173
- }
174
- return data
175
- }
176
-
177
- exports.mongoToPostgresSelect = function(collection, fields) {
178
- const arrayFields = exports.getArrayPaths('', schemas[collection])
179
- return fields ? mongoToPostgres.convertSelect('data', fields, arrayFields) : '*'
180
- }
181
-
182
- exports.mongoToPostgresUpdate = function(collection, query) {
183
- return mongoToPostgres.convertUpdate('data', query,false)
184
- }
185
-
186
- exports.mongoToPostgresWhere = function(collection, query) {
187
- const arrayFields = exports.getArrayPaths('', schemas[collection])
188
- return mongoToPostgres('data', query || {}, arrayFields)
189
- }
190
-
191
- exports.mongoToPostgresOrderBy = function(collection, orderby) {
192
- const normalizedOrderBy = exports.normalizeOrderBy(orderby)
193
- return normalizedOrderBy.map((ordering) => {
194
- return mongoToPostgres.convertDotNotation('data', ordering[0]) + (ordering[1] > 0 ? ' ASC' : ' DESC')
195
- }).join(', ')
196
- }
197
-
198
- exports.mongoProject = function(record, projection) {
199
- if (!projection || Object.keys(projection).length < 1) {
200
- return record
201
- }
202
- const isInclude = projection[Object.keys(projection)[0]]
203
- if (typeof projection._id === 'undefined') {
204
- projection = {...projection, _id: 1}
205
- }
206
- const expanded = dot.object(projection)
207
- const result = isInclude ? _include(record, expanded) : _exclude(record, expanded)
208
- return result
209
- }
210
-
211
- exports.mongoSearch = function(docs, query) {
212
- return sift(query || {}, docs)
213
- }
214
-
215
- exports.mongoUpdate = function(doc, update) {
216
- return mongoQuery(doc, {}, update)
217
- }
218
-
219
- exports.mongoToPostgres = mongoToPostgres
220
-
221
- // paging requires orderby to include a field that is known to be unique and constant
222
- // to ensure consistent results from database with offset and limit. Chosen field should
223
- // also represent a preferred ordering when doing final sort
224
- exports.orderByForPagedRequests = function(orderby, finalSortField = 'meta.created', finalSortDirection = 1) {
225
- orderby = exports.normalizeOrderBy(orderby || {})
226
- const allFields = orderby.map(([field]) => field)
227
- if (!allFields.includes(finalSortField)) {
228
- orderby.push([finalSortField, finalSortDirection])
229
- }
230
- return orderby
231
- }
232
-
233
- exports.normalizeOrderBy = function(orderby) {
234
- if (Array.isArray(orderby)) {
235
- orderby = orderby.map(function (ordering) {
236
- if (typeof ordering === 'string') {
237
- return [ordering, 1]
238
- } else if (Array.isArray(ordering)) {
239
- if (ordering.length === 1) {
240
- return [ordering[0], 1] // add 1 (default to ascending sort)
241
- }
242
- }
243
- // if reaches here, is already normalized
244
- return ordering
245
- })
246
- } else if (typeof orderby === 'object') {
247
- const arr = []
248
- for (const key in orderby) {
249
- arr.push([key, orderby[key]])
250
- }
251
- orderby = arr
252
- } else {
253
- throw exports.ApiError(400, 'orderby param must be array or object')
254
- }
255
- return orderby
256
- }
257
-
258
- exports.getPath = (obj, path, defaultValue) => {
259
- const result = String.prototype.split.call(path, /[,[\].]+?/)
260
- .filter(Boolean)
261
- .reduce((res, key) => (res !== null && res !== undefined) ? res[key] : res, obj)
262
- return (result === undefined || result === obj) ? defaultValue : result
263
- }
264
-
265
- exports.castArray = function(arr) {
266
- return Array.isArray(arr) ? arr : [arr]
267
- }
268
-
269
- exports.clone = function (obj) {
270
- if (!obj) {
271
- return obj
272
- }
273
- return JSON.parse(JSON.stringify(obj))
274
- }
275
-
276
- exports.createSecureRandomId = function() {
277
- return crypto.randomBytes(24).toString('hex')
278
- }
279
-
280
- const severities = ['critical', 'error', 'warning', 'notice', 'info', 'debug']
281
- exports.getLogSeverity = function (status) {
282
- const severity = status >= 500 ? 'error'
283
- : status >= 400 ? 'warning'
284
- : status >= 300 ? 'notice'
285
- : status >= 200 ? 'info'
286
- : 'debug'
287
- return severity
288
- }
289
-
290
- exports.shouldLogRequest = function (req, res) {
291
- const severity = exports.getLogSeverity(res.statusCode)
292
- const severityLoggingIndex = severities.indexOf(req.getSetting('logging_level') || 'warning')
293
- const severityIndex = severities.indexOf(severity)
294
- return severityIndex <= severityLoggingIndex
295
- }
296
-
297
- function filterHeaders(req) {
298
- const headers = req.headers || {}
299
- return {
300
- 'user-agent': headers['user-agent'],
301
- origin: headers['origin'],
302
- referer: headers['referer'],
303
- }
304
- }
305
-
306
- exports.createLogEntry = function (req, res) {
307
- const severity = exports.getLogSeverity(res.statusCode)
308
- return {
309
- severity: severity,
310
- user: req.uid,
311
- user_collection: req.ucollection,
312
- url: decodeURI(req.originalUrl || req.url),
313
- method: req.method,
314
- referer: req.headers['referer'],
315
- req: {
316
- ip: req.ip,
317
- headers: filterHeaders(req),
318
- },
319
- res: {
320
- statusCode: res.statusCode,
321
- requestId: res.getHeader('x-request-id'),
322
- headers: res.getHeaders(),
323
- message: req.returnedError ? req.returnedError.result || req.returnedError.message : undefined,
324
- tokenMessage: req.uerror,
325
- },
326
- meta: {
327
- created: new Date().toISOString(),
328
- updated: new Date().toISOString()
329
- },
330
- }
331
- }
332
-
333
- exports.notify = async function (event, req, collection, data) {
334
- const listeners = req.eventListeners[event] || []
335
- debug('notifying ' + listeners.length + ' of ' + event + ' for ' + collection)
336
- let result
337
- for (const listener of listeners) {
338
- if (listener.collections && !listener.collections.includes(collection)) {
339
- continue // skip since it's not relevant
340
- }
341
- debug('calling ' + listener.name + ' ' + (result ? '(skipped)' : ''))
342
- try {
343
- result = result || await listener(req, collection, data, { event })
344
- } catch (e) {
345
- if (e && e.message) {
346
- console.error(e)
347
- }
348
- // If a listener has already allowed the request, do not error
349
- if (!result) {
350
- throw e
351
- }
352
- }
353
- }
354
- return result || result === undefined
355
- }
356
-
357
- class ApiError extends Error {
358
- constructor (status, message) {
359
- super(message)
360
- this.name = this.constructor.name
361
- Error.captureStackTrace(this, this.constructor)
362
- this.status = status || this.constructor.status || 500
363
- }
364
- }
365
- exports.ApiError = ApiError
366
-
367
- exports.asyncMiddleware = fn =>
368
- (req, res, next) => {
369
- Promise.resolve(fn(req, res, next))
370
- .catch(next)
371
- }
372
-
373
- exports.resolve = async function resolve (handler, app) {
374
- if (typeof handler === 'function') {
375
- return handler(app)
376
- }
377
- return handler
378
- }
379
-
380
- const STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg
381
- const ARGUMENT_NAMES = /([^\s,]+)/g
382
- exports.getFunctionParamNames = function getFunctionParamNames (func) {
383
- const fnStr = func.toString().replace(STRIP_COMMENTS, '')
384
- let result = fnStr.slice(fnStr.indexOf('(')+1, fnStr.indexOf(')')).match(ARGUMENT_NAMES)
385
- if(result === null)
386
- result = []
387
- return result
388
- }
389
-
390
- exports.friendlyDuration = function friendlyDuration (seconds) {
391
- if (seconds > 86400) {
392
- return Math.round(seconds / 86400) + ' hours'
393
- }
394
- if (seconds > 3600) {
395
- return Math.round(seconds / 3600) + ' hours'
396
- }
397
- if (seconds > 60) {
398
- return Math.round(seconds / 60) + ' minutes'
399
- }
400
- return Math.round(seconds) + ' seconds'
401
- }
402
-
403
- exports.getPgPool = function getPgPool(connectionString) {
404
- if (!pgPools[connectionString]) {
405
- pgPools[connectionString] = new pg.Pool({ connectionString: connectionString })
406
- }
407
- return pgPools[connectionString]
408
- }
409
-
410
- exports.generateDocumentId = function generateDocumentId() {
411
- return v4()
412
- }
413
-
414
- exports.addIdIfMissing = function addIdIfMissing (document) {
415
- if (!document._id) {
416
- document._id = exports.generateDocumentId().trim()
417
- } else {
418
- document._id = document._id.trim()
419
- }
420
- }
421
-
422
- exports.sortObjectKeys = function sortObjectKeys(object) {
423
- if (typeof object != 'object' || object instanceof Array || !object) { // Do not sort the array
424
- return object
425
- }
426
- const keys = Object.keys(object)
427
- keys.sort()
428
- const newObject = {}
429
- for (let i = 0; i < keys.length; i++){
430
- newObject[keys[i]] = exports.sortObjectKeys(object[keys[i]])
431
- }
432
- return newObject
433
- }
434
-
435
- exports.getLoginCollections = async function(api) {
436
- const all = await api.db.collection.all()
437
- return all.length > 0 ? all.filter((coll) => isValidLoginCollection(coll)) : [{
438
- _id: 'users',
439
- enableLogin: true
440
- }]
441
- }
442
-
443
- // really being over cautious here to prevent collections
444
- // unkowingly creating insecure access to database
445
- function isValidLoginCollection(collection) {
446
- const name = collection._id
447
- if(collection.enableLogin === true) {
448
- const properties = collection.schema?.properties
449
- if (properties && properties.password && properties.roles) {
450
- const required = collection.schema?.required
451
- if (required?.includes('password')) {
452
- return true
453
- }
454
- else {
455
- console.error(`Login Collection Failed: "${name}" schema properties email and password must be listed as 'required'`)
456
- }
457
- }
458
- else {
459
- console.error(`Login Collection Failed: "${name}" email, password and roles are mandatory schema properties`)
460
- }
461
- }
462
- return false
463
- }
464
-
465
- exports.createPagination = function createPagination (data, page, limit) {
466
- const pagination = {
467
- page: parseInt(page),
468
- itemsTotal: data.length,
469
- itemsPerPage: limit,
470
- pages: Math.ceil(data.length / limit)
471
- }
472
- pagination.page = pagination.page > pagination.pages ? pagination.pages + 1 : pagination.page
473
- if (pagination.page < pagination.pages) {
474
- pagination.pageNext = pagination.page + 1
475
- }
476
- if (pagination.page - 1 > 0) {
477
- pagination.pagePrev = pagination.page - 1
478
- }
479
- pagination.data = data.splice((pagination.page - 1) * limit, limit)
480
- return pagination
481
- }
482
-
483
- exports.createPagePagination = function createPagePagination (pageData, page, pageItems, totalItems) {
484
- const pages = totalItems >= 0 ? Math.ceil(totalItems / pageItems) : undefined
485
- page = parseInt(page)
486
- page = page > pages ? pages + 1 : page
487
- return {
488
- data: pageData,
489
- page: page > pages ? pages + 1 : page,
490
- itemsPerPage: pageItems,
491
- itemsTotal: totalItems,
492
- pages,
493
- pageNext: page < pages ? page + 1 : undefined,
494
- pagePrev: page - 1 > 0 ? page - 1 : undefined,
495
- }
496
- }
497
-
498
- exports.getDatabaseOffset = function getDatabaseOffset(page, itemsPerPage) {
499
- return (page - 1) * itemsPerPage
500
- }
501
-
502
- exports.createHash = auth.createHash
503
- exports.isHashed = auth.isHashed
504
- exports.doLogin = auth.doLogin
1
+ const {v4} = require('uuid')
2
+ const debug = require('debug')('expressa')
3
+ const crypto = require('crypto')
4
+ const pg = require('pg')
5
+ const pgPools = {}
6
+ const dot = require('dot-object')
7
+ const mongoQuery = require('mongo-query')
8
+ const mongoToPostgres = require('mongo-query-to-postgres-jsonb')
9
+ const sift = require('sift')
10
+ const auth = require('./auth/index')
11
+ const Ajv = require('ajv')
12
+ const ajv = new Ajv({
13
+ allErrors: true,
14
+ strict: 'log',
15
+ strictSchema: 'log',
16
+ validateFormats: false,
17
+ allowUnionTypes: true,
18
+ })
19
+ ajv.addKeyword({
20
+ keyword: 'links',
21
+ type: 'string',
22
+ schemaType: 'array',
23
+ })
24
+ ajv.addKeyword({
25
+ keyword: 'media',
26
+ type: 'string',
27
+ schemaType: 'object',
28
+ })
29
+ ajv.addKeyword({
30
+ keyword: 'propertyOrder',
31
+ schemaType: 'number',
32
+ })
33
+ const formatKey = ajv.getKeyword('format')
34
+ formatKey.type = formatKey.type.concat(['array', 'boolean', 'object'])
35
+
36
+ const schemas = {}
37
+ const schemaValidators = {}
38
+
39
+ exports.addSchema = function(collection, schema) {
40
+ schema.$id = collection
41
+ ajv.removeSchema(collection)
42
+ schemas[collection] = schema
43
+ schemaValidators[collection] = ajv.compile(schema)
44
+ delete schema.$id
45
+ }
46
+
47
+ exports.validateSchema = function(collection, doc) {
48
+ if (!schemaValidators[collection]) {
49
+ console.error(`missing schema validator for collection ${collection}`)
50
+ return true
51
+ }
52
+ const isValid = schemaValidators[collection](doc)
53
+ if (!isValid) {
54
+ throw new ApiError(400, schemaValidators[collection].errors.map((err) => `${err.instancePath} ${err.message}: ${JSON.stringify(err.params)}`).join(', '))
55
+ }
56
+ return true
57
+ }
58
+
59
+ exports.validateSchemaProperty = function(collection, path, value) {
60
+ if (!schemaValidators[collection]) {
61
+ console.error(`missing schema validator for collection ${collection}`)
62
+ return true
63
+ }
64
+ const $ref = `${collection}#/properties/${path.split('.').join('/properties/')}`
65
+ const isValid = ajv.validate({ $ref }, value)
66
+ if (!isValid) {
67
+ throw new ApiError(400, ajv.errors.map((err) => `${err.instancePath} ${err.message}: ${JSON.stringify(err.params)}`).join(', '))
68
+ }
69
+ return true
70
+ }
71
+
72
+ exports.getSchemaProperty = function(collection, path) {
73
+ const parts = path.split('.')
74
+ let obj = schemas[collection]
75
+ for (const part of parts) {
76
+ if (obj.properties && obj.properties[part]) {
77
+ obj = obj.properties[part]
78
+ } else if (obj.items && obj.items.properties && obj.items.properties[part]) {
79
+ obj = obj.items.properties[part]
80
+ } else {
81
+ return
82
+ }
83
+ }
84
+ return obj
85
+ }
86
+
87
+ exports.orderBy = function (data, orderby) {
88
+ data.sort(function compare (a, b) {
89
+ for (let i = 0; i < orderby.length; i++) {
90
+ const ordering = orderby[i]
91
+ const key = ordering[0]
92
+ if (exports.getPath(a, key) > exports.getPath(b, key)) {
93
+ return ordering[1]
94
+ } else if (exports.getPath(a, key) < exports.getPath(b, key)) {
95
+ return -ordering[1]
96
+ }
97
+ }
98
+ return 0
99
+ })
100
+ return data
101
+ }
102
+
103
+ exports.getArrayPaths = function (key, value) {
104
+ if (!value)
105
+ return []
106
+ if (value.type === 'array')
107
+ return [key].concat(exports.getArrayPaths(key, value.items)).filter(el => el)
108
+ if (value.type === 'object') {
109
+ return Object.entries(value.properties)
110
+ .map(([key2, value]) => {
111
+ return exports.getArrayPaths(key ? key + '.' + key2 : key2, value)
112
+ })
113
+ .flat()
114
+ .filter(el => el)
115
+ }
116
+ }
117
+
118
+ function isObject(obj) {
119
+ return typeof obj === 'object'
120
+ }
121
+
122
+ /**
123
+ * Returns an excerpt of the selected document based on an include projection.
124
+ *
125
+ * Note:
126
+ * It includes the fields listed in the projection with a value of 1. the
127
+ * unknown fields or the fields with a value other than 1 are ignored.
128
+ * @param {Object} the selected document,
129
+ * @param {Object} the projection to apply,
130
+ */
131
+ function _include(obj, source) {
132
+ const data = {}
133
+ for (const prop in source) {
134
+ if (typeof obj[prop] !== 'undefined') {
135
+ if (isObject(source[prop])) {
136
+ if (Array.isArray(obj[prop])) {
137
+ data[prop] = obj[prop].map((element) => _include(element, source[prop]))
138
+ } else {
139
+ data[prop] = _include(obj[prop], source[prop])
140
+ }
141
+ } else if (source[prop] == 1) {
142
+ if (isObject(obj[prop])) {
143
+ data[prop] = exports.clone(obj[prop])
144
+ } else if (typeof obj[prop] !== 'undefined') {
145
+ data[prop] = obj[prop]
146
+ }
147
+ }
148
+ }
149
+ }
150
+ return data
151
+ }
152
+
153
+ /**
154
+ * Returns an excerpt of the selected document based on an exclude projection.
155
+ *
156
+ * Note:
157
+ * It excludes the fields listed in the projection with a value of 0. The
158
+ * unspecified fields are kept.
159
+ */
160
+ function _exclude(obj, source) {
161
+ const data = {}
162
+ for (const prop in obj) {
163
+ if (source[prop] !== undefined && !source[prop]) {
164
+ if (isObject(source[prop])) {
165
+ data[prop] = {}
166
+ _exclude(obj[prop], source[prop], data[prop])
167
+ }
168
+ } else if (isObject(obj[prop])) {
169
+ data[prop] = exports.clone(obj[prop])
170
+ } else {
171
+ data[prop] = obj[prop]
172
+ }
173
+ }
174
+ return data
175
+ }
176
+
177
+ exports.mongoToPostgresSelect = function(collection, fields) {
178
+ const arrayFields = exports.getArrayPaths('', schemas[collection])
179
+ return fields ? mongoToPostgres.convertSelect('data', fields, arrayFields) : '*'
180
+ }
181
+
182
+ exports.mongoToPostgresUpdate = function(collection, query) {
183
+ return mongoToPostgres.convertUpdate('data', query,false)
184
+ }
185
+
186
+ exports.mongoToPostgresWhere = function(collection, query) {
187
+ const arrayFields = exports.getArrayPaths('', schemas[collection])
188
+ return mongoToPostgres('data', query || {}, arrayFields)
189
+ }
190
+
191
+ exports.mongoToPostgresOrderBy = function(collection, orderby) {
192
+ const normalizedOrderBy = exports.normalizeOrderBy(orderby)
193
+ return normalizedOrderBy.map((ordering) => {
194
+ const field = mongoToPostgres.pathToText(['data'].concat(ordering[0].split('.')), false)
195
+ return field + (ordering[1] > 0 ? ' ASC' : ' DESC')
196
+ }).join(', ')
197
+ }
198
+
199
+ exports.mongoProject = function(record, projection) {
200
+ if (!projection || Object.keys(projection).length < 1) {
201
+ return record
202
+ }
203
+ const isInclude = projection[Object.keys(projection)[0]]
204
+ if (typeof projection._id === 'undefined') {
205
+ projection = {...projection, _id: 1}
206
+ }
207
+ const expanded = dot.object(projection)
208
+ const result = isInclude ? _include(record, expanded) : _exclude(record, expanded)
209
+ return result
210
+ }
211
+
212
+ exports.mongoSearch = function(docs, query) {
213
+ return sift(query || {}, docs)
214
+ }
215
+
216
+ exports.mongoUpdate = function(doc, update) {
217
+ return mongoQuery(doc, {}, update)
218
+ }
219
+
220
+ exports.mongoToPostgres = mongoToPostgres
221
+
222
+ // paging requires orderby to include a field that is known to be unique and constant
223
+ // to ensure consistent results from database with offset and limit. Chosen field should
224
+ // also represent a preferred ordering when doing final sort
225
+ exports.orderByForPagedRequests = function(orderby, finalSortField = 'meta.created', finalSortDirection = 1) {
226
+ orderby = exports.normalizeOrderBy(orderby || {})
227
+ const allFields = orderby.map(([field]) => field)
228
+ if (!allFields.includes(finalSortField)) {
229
+ orderby.push([finalSortField, finalSortDirection])
230
+ }
231
+ return orderby
232
+ }
233
+
234
+ exports.normalizeOrderBy = function(orderby) {
235
+ if (Array.isArray(orderby)) {
236
+ orderby = orderby.map(function (ordering) {
237
+ if (typeof ordering === 'string') {
238
+ return [ordering, 1]
239
+ } else if (Array.isArray(ordering)) {
240
+ if (ordering.length === 1) {
241
+ return [ordering[0], 1] // add 1 (default to ascending sort)
242
+ }
243
+ }
244
+ // if reaches here, is already normalized
245
+ return ordering
246
+ })
247
+ } else if (typeof orderby === 'object') {
248
+ const arr = []
249
+ for (const key in orderby) {
250
+ arr.push([key, orderby[key]])
251
+ }
252
+ orderby = arr
253
+ } else {
254
+ throw exports.ApiError(400, 'orderby param must be array or object')
255
+ }
256
+ return orderby
257
+ }
258
+
259
+ exports.getPath = (obj, path, defaultValue) => {
260
+ const result = String.prototype.split.call(path, /[,[\].]+?/)
261
+ .filter(Boolean)
262
+ .reduce((res, key) => (res !== null && res !== undefined) ? res[key] : res, obj)
263
+ return (result === undefined || result === obj) ? defaultValue : result
264
+ }
265
+
266
+ exports.castArray = function(arr) {
267
+ return Array.isArray(arr) ? arr : [arr]
268
+ }
269
+
270
+ exports.clone = function (obj) {
271
+ if (!obj) {
272
+ return obj
273
+ }
274
+ return JSON.parse(JSON.stringify(obj))
275
+ }
276
+
277
+ exports.createSecureRandomId = function() {
278
+ return crypto.randomBytes(24).toString('hex')
279
+ }
280
+
281
+ const severities = ['critical', 'error', 'warning', 'notice', 'info', 'debug']
282
+ exports.getLogSeverity = function (status) {
283
+ const severity = status >= 500 ? 'error'
284
+ : status >= 400 ? 'warning'
285
+ : status >= 300 ? 'notice'
286
+ : status >= 200 ? 'info'
287
+ : 'debug'
288
+ return severity
289
+ }
290
+
291
+ exports.shouldLogRequest = function (req, res) {
292
+ const severity = exports.getLogSeverity(res.statusCode)
293
+ const severityLoggingIndex = severities.indexOf(req.getSetting('logging_level') || 'warning')
294
+ const severityIndex = severities.indexOf(severity)
295
+ return severityIndex <= severityLoggingIndex
296
+ }
297
+
298
+ function filterHeaders(req) {
299
+ const headers = req.headers || {}
300
+ return {
301
+ 'user-agent': headers['user-agent'],
302
+ origin: headers['origin'],
303
+ referer: headers['referer'],
304
+ }
305
+ }
306
+
307
+ exports.createLogEntry = function (req, res) {
308
+ const severity = exports.getLogSeverity(res.statusCode)
309
+ return {
310
+ severity: severity,
311
+ user: req.uid,
312
+ user_collection: req.ucollection,
313
+ url: decodeURI(req.originalUrl || req.url),
314
+ method: req.method,
315
+ referer: req.headers['referer'],
316
+ req: {
317
+ ip: req.ip,
318
+ headers: filterHeaders(req),
319
+ },
320
+ res: {
321
+ statusCode: res.statusCode,
322
+ requestId: res.getHeader('x-request-id'),
323
+ headers: res.getHeaders(),
324
+ message: req.returnedError ? req.returnedError.result || req.returnedError.message : undefined,
325
+ tokenMessage: req.uerror,
326
+ },
327
+ meta: {
328
+ created: new Date().toISOString(),
329
+ updated: new Date().toISOString()
330
+ },
331
+ }
332
+ }
333
+
334
+ exports.notify = async function (event, req, collection, data) {
335
+ const listeners = req.eventListeners[event] || []
336
+ debug('notifying ' + listeners.length + ' of ' + event + ' for ' + collection)
337
+ let result
338
+ for (const listener of listeners) {
339
+ if (listener.collections && !listener.collections.includes(collection)) {
340
+ continue // skip since it's not relevant
341
+ }
342
+ debug('calling ' + listener.name + ' ' + (result ? '(skipped)' : ''))
343
+ try {
344
+ result = result || await listener(req, collection, data, { event })
345
+ } catch (e) {
346
+ if (e && e.message) {
347
+ console.error(e)
348
+ }
349
+ // If a listener has already allowed the request, do not error
350
+ if (!result) {
351
+ throw e
352
+ }
353
+ }
354
+ }
355
+ return result || result === undefined
356
+ }
357
+
358
+ class ApiError extends Error {
359
+ constructor (status, message) {
360
+ super(message)
361
+ this.name = this.constructor.name
362
+ Error.captureStackTrace(this, this.constructor)
363
+ this.status = status || this.constructor.status || 500
364
+ }
365
+ }
366
+ exports.ApiError = ApiError
367
+
368
+ exports.asyncMiddleware = fn =>
369
+ (req, res, next) => {
370
+ Promise.resolve(fn(req, res, next))
371
+ .catch(next)
372
+ }
373
+
374
+ exports.resolve = async function resolve (handler, app) {
375
+ if (typeof handler === 'function') {
376
+ return handler(app)
377
+ }
378
+ return handler
379
+ }
380
+
381
+ const STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg
382
+ const ARGUMENT_NAMES = /([^\s,]+)/g
383
+ exports.getFunctionParamNames = function getFunctionParamNames (func) {
384
+ const fnStr = func.toString().replace(STRIP_COMMENTS, '')
385
+ let result = fnStr.slice(fnStr.indexOf('(')+1, fnStr.indexOf(')')).match(ARGUMENT_NAMES)
386
+ if(result === null)
387
+ result = []
388
+ return result
389
+ }
390
+
391
+ exports.friendlyDuration = function friendlyDuration (seconds) {
392
+ if (seconds > 86400) {
393
+ return Math.round(seconds / 86400) + ' hours'
394
+ }
395
+ if (seconds > 3600) {
396
+ return Math.round(seconds / 3600) + ' hours'
397
+ }
398
+ if (seconds > 60) {
399
+ return Math.round(seconds / 60) + ' minutes'
400
+ }
401
+ return Math.round(seconds) + ' seconds'
402
+ }
403
+
404
+ exports.getPgPool = function getPgPool(connectionString) {
405
+ if (!pgPools[connectionString]) {
406
+ pgPools[connectionString] = new pg.Pool({ connectionString: connectionString })
407
+ }
408
+ return pgPools[connectionString]
409
+ }
410
+
411
+ exports.generateDocumentId = function generateDocumentId() {
412
+ return v4()
413
+ }
414
+
415
+ exports.addIdIfMissing = function addIdIfMissing (document) {
416
+ if (!document._id) {
417
+ document._id = exports.generateDocumentId().trim()
418
+ } else {
419
+ document._id = document._id.trim()
420
+ }
421
+ }
422
+
423
+ exports.sortObjectKeys = function sortObjectKeys(object) {
424
+ if (typeof object != 'object' || object instanceof Array || !object) { // Do not sort the array
425
+ return object
426
+ }
427
+ const keys = Object.keys(object)
428
+ keys.sort()
429
+ const newObject = {}
430
+ for (let i = 0; i < keys.length; i++){
431
+ newObject[keys[i]] = exports.sortObjectKeys(object[keys[i]])
432
+ }
433
+ return newObject
434
+ }
435
+
436
+ exports.getLoginCollections = async function(api) {
437
+ const all = await api.db.collection.all()
438
+ return all.length > 0 ? all.filter((coll) => isValidLoginCollection(coll)) : [{
439
+ _id: 'users',
440
+ enableLogin: true
441
+ }]
442
+ }
443
+
444
+ // really being over cautious here to prevent collections
445
+ // unkowingly creating insecure access to database
446
+ function isValidLoginCollection(collection) {
447
+ const name = collection._id
448
+ if(collection.enableLogin === true) {
449
+ const properties = collection.schema?.properties
450
+ if (properties && properties.password && properties.roles) {
451
+ const required = collection.schema?.required
452
+ if (required?.includes('password')) {
453
+ return true
454
+ }
455
+ else {
456
+ console.error(`Login Collection Failed: "${name}" schema properties email and password must be listed as 'required'`)
457
+ }
458
+ }
459
+ else {
460
+ console.error(`Login Collection Failed: "${name}" email, password and roles are mandatory schema properties`)
461
+ }
462
+ }
463
+ return false
464
+ }
465
+
466
+ exports.createPagination = function createPagination (data, page, limit) {
467
+ const pagination = {
468
+ page: parseInt(page),
469
+ itemsTotal: data.length,
470
+ itemsPerPage: limit,
471
+ pages: Math.ceil(data.length / limit)
472
+ }
473
+ pagination.page = pagination.page > pagination.pages ? pagination.pages + 1 : pagination.page
474
+ if (pagination.page < pagination.pages) {
475
+ pagination.pageNext = pagination.page + 1
476
+ }
477
+ if (pagination.page - 1 > 0) {
478
+ pagination.pagePrev = pagination.page - 1
479
+ }
480
+ pagination.data = data.splice((pagination.page - 1) * limit, limit)
481
+ return pagination
482
+ }
483
+
484
+ exports.createPagePagination = function createPagePagination (pageData, page, pageItems, totalItems) {
485
+ const pages = totalItems >= 0 ? Math.ceil(totalItems / pageItems) : undefined
486
+ page = parseInt(page)
487
+ page = page > pages ? pages + 1 : page
488
+ return {
489
+ data: pageData,
490
+ page: page > pages ? pages + 1 : page,
491
+ itemsPerPage: pageItems,
492
+ itemsTotal: totalItems,
493
+ pages,
494
+ pageNext: page < pages ? page + 1 : undefined,
495
+ pagePrev: page - 1 > 0 ? page - 1 : undefined,
496
+ }
497
+ }
498
+
499
+ exports.getDatabaseOffset = function getDatabaseOffset(page, itemsPerPage) {
500
+ return (page - 1) * itemsPerPage
501
+ }
502
+
503
+ exports.hasPermission = function hasPermission(permissions, permission) {
504
+ return permissions && permissions[permission]
505
+ }
506
+
507
+ exports.getEffectivePermissionsForRoles = async function getEffectivePermissionsForRoles(roles, db) {
508
+ const permissions = {}
509
+ const roleDocs = await Promise.all(roles.map((name) => db.role.get(name)))
510
+ roleDocs.forEach((roleDoc) => {
511
+ for (const permission in roleDoc.permissions) {
512
+ if (roleDoc.permissions[permission]) {
513
+ permissions[permission] = true
514
+ }
515
+ }
516
+ })
517
+ return permissions
518
+ }
519
+
520
+ exports.createHash = auth.createHash
521
+ exports.isHashed = auth.isHashed
522
+ exports.doLogin = auth.doLogin