expressa 2.0.18 → 2.0.20

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 (258) 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 +260 -260
  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 +30 -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 +69 -54
  19. package/cypress/support/index.js +20 -20
  20. package/cypress/support/util.js +10 -10
  21. package/cypress.config.js +18 -0
  22. package/cypress.json +4 -4
  23. package/db/cached.js +40 -40
  24. package/db/file.js +112 -112
  25. package/db/memory.js +79 -79
  26. package/db/mongo.js +86 -86
  27. package/db/postgres.js +93 -93
  28. package/doc/authentication.md +38 -38
  29. package/doc/automatic-fields.md +12 -12
  30. package/doc/blogexample.md +16 -16
  31. package/doc/custom-endpoints.md +20 -20
  32. package/doc/database.md +23 -23
  33. package/doc/development.md +33 -33
  34. package/doc/listeners.md +103 -103
  35. package/doc/modules.md +8 -8
  36. package/doc/permissions.md +14 -14
  37. package/doc/relationships.md +65 -65
  38. package/doc/testing.md +47 -47
  39. package/doc/uploading-files.md +53 -53
  40. package/index.js +297 -297
  41. package/listeners.js +65 -65
  42. package/listeners_collection_permissions.js +29 -29
  43. package/listeners_users.js +96 -96
  44. package/listeners_validation.js +43 -43
  45. package/middleware/logging.js +30 -30
  46. package/middleware/permissions.js +41 -41
  47. package/modules/access_keys/access_keys.js +32 -32
  48. package/modules/admin/.babelrc +19 -19
  49. package/modules/admin/.editorconfig +14 -14
  50. package/modules/admin/.eslintignore +5 -5
  51. package/modules/admin/.eslintrc.js +197 -197
  52. package/modules/admin/.postcssrc.js +10 -10
  53. package/modules/admin/.travis.yml +5 -5
  54. package/modules/admin/LICENSE +21 -21
  55. package/modules/admin/README.md +88 -88
  56. package/modules/admin/build/build.js +46 -45
  57. package/modules/admin/build/check-versions.js +64 -64
  58. package/modules/admin/build/utils.js +109 -108
  59. package/modules/admin/build/vue-loader.conf.js +5 -5
  60. package/modules/admin/build/webpack.base.conf.js +112 -112
  61. package/modules/admin/build/webpack.dev.conf.js +95 -95
  62. package/modules/admin/build/webpack.prod.conf.js +112 -112
  63. package/modules/admin/config/dev.env.js +8 -8
  64. package/modules/admin/config/index.js +86 -86
  65. package/modules/admin/config/prod.env.js +5 -5
  66. package/modules/admin/dist/css/197.7a90435e.css +7 -0
  67. package/modules/admin/dist/css/197.7a90435e.css.gz +0 -0
  68. package/modules/admin/dist/css/430.8c9b0482.css +8 -0
  69. package/modules/admin/dist/css/430.8c9b0482.css.gz +0 -0
  70. package/modules/admin/dist/css/451.c6646d20.css +1 -0
  71. package/modules/admin/dist/css/451.c6646d20.css.gz +0 -0
  72. package/modules/admin/dist/css/566.7a90435e.css +7 -0
  73. package/modules/admin/dist/css/566.7a90435e.css.gz +0 -0
  74. package/modules/admin/dist/css/569.f7d5c7d5.css +37 -0
  75. package/modules/admin/dist/css/569.f7d5c7d5.css.gz +0 -0
  76. package/modules/admin/dist/css/572.74d8e965.css +34 -0
  77. package/modules/admin/dist/css/572.74d8e965.css.gz +0 -0
  78. package/modules/admin/dist/css/651.e946bda7.css +6 -0
  79. package/modules/admin/dist/css/651.e946bda7.css.gz +0 -0
  80. package/modules/admin/dist/css/750.4086915d.css +2 -0
  81. package/modules/admin/dist/css/750.4086915d.css.gz +0 -0
  82. package/modules/admin/dist/css/950.1fd9d57c.css +7 -0
  83. package/modules/admin/dist/css/950.1fd9d57c.css.gz +0 -0
  84. package/modules/admin/dist/css/app.eba5f5e2.css +592 -0
  85. package/modules/admin/dist/css/app.eba5f5e2.css.gz +0 -0
  86. package/modules/admin/dist/index.html +1 -1
  87. package/modules/admin/dist/static/fonts/element-icons.313f7da.woff +0 -0
  88. package/modules/admin/dist/static/fonts/element-icons.4520188.ttf +0 -0
  89. package/modules/admin/dist/static/js/197.e511d91e.js +1 -0
  90. package/modules/admin/dist/static/js/197.e511d91e.js.gz +0 -0
  91. package/modules/admin/dist/static/js/430.b3941f30.js +1 -0
  92. package/modules/admin/dist/static/js/430.b3941f30.js.gz +0 -0
  93. package/modules/admin/dist/static/js/451.645974a8.js +1 -0
  94. package/modules/admin/dist/static/js/451.645974a8.js.gz +0 -0
  95. package/modules/admin/dist/static/js/460.962d5bc0.js +1 -0
  96. package/modules/admin/dist/static/js/460.962d5bc0.js.gz +0 -0
  97. package/modules/admin/dist/static/js/550.f27feb23.js +1 -0
  98. package/modules/admin/dist/static/js/550.f27feb23.js.gz +0 -0
  99. package/modules/admin/dist/static/js/556.1e08866a.js +1 -0
  100. package/modules/admin/dist/static/js/556.1e08866a.js.gz +0 -0
  101. package/modules/admin/dist/static/js/569.0fecfbee.js +1 -0
  102. package/modules/admin/dist/static/js/569.0fecfbee.js.gz +0 -0
  103. package/modules/admin/dist/static/js/572.13392e1d.js +1 -0
  104. package/modules/admin/dist/static/js/572.13392e1d.js.gz +0 -0
  105. package/modules/admin/dist/static/js/603.7b34a650.js +1 -0
  106. package/modules/admin/dist/static/js/603.7b34a650.js.gz +0 -0
  107. package/modules/admin/dist/static/js/651.564b24b6.js +1 -0
  108. package/modules/admin/dist/static/js/651.564b24b6.js.gz +0 -0
  109. package/modules/admin/dist/static/js/653.02ec1098.js +1 -0
  110. package/modules/admin/dist/static/js/653.02ec1098.js.gz +0 -0
  111. package/modules/admin/dist/static/js/750.50c950da.js +1 -0
  112. package/modules/admin/dist/static/js/750.50c950da.js.gz +0 -0
  113. package/modules/admin/dist/static/js/950.d4ff0a18.js +1 -0
  114. package/modules/admin/dist/static/js/950.d4ff0a18.js.gz +0 -0
  115. package/modules/admin/dist/static/js/app.ea86ae11.js +2 -0
  116. package/modules/admin/dist/static/js/{chunk-libs.06c38828.js.LICENSE.txt → app.ea86ae11.js.LICENSE.txt} +15 -18
  117. package/modules/admin/dist/static/js/app.ea86ae11.js.LICENSE.txt.gz +0 -0
  118. package/modules/admin/dist/static/js/app.ea86ae11.js.gz +0 -0
  119. package/modules/admin/index.html +16 -16
  120. package/modules/admin/package.json +101 -101
  121. package/modules/admin/src/App.vue +11 -11
  122. package/modules/admin/src/api/login.js +26 -26
  123. package/modules/admin/src/api/table.js +9 -9
  124. package/modules/admin/src/components/Breadcrumb/index.vue +79 -79
  125. package/modules/admin/src/components/Hamburger/index.vue +62 -62
  126. package/modules/admin/src/components/JSONEditor.vue +180 -180
  127. package/modules/admin/src/components/SvgIcon/index.vue +43 -43
  128. package/modules/admin/src/icons/index.js +9 -9
  129. package/modules/admin/src/icons/svgo.yml +22 -22
  130. package/modules/admin/src/main.js +42 -42
  131. package/modules/admin/src/permission.js +49 -49
  132. package/modules/admin/src/router/index.js +200 -200
  133. package/modules/admin/src/store/getters.js +11 -11
  134. package/modules/admin/src/store/index.js +17 -17
  135. package/modules/admin/src/store/modules/app.js +43 -43
  136. package/modules/admin/src/store/modules/user.js +104 -104
  137. package/modules/admin/src/styles/element-ui.scss +29 -29
  138. package/modules/admin/src/styles/index.scss +78 -78
  139. package/modules/admin/src/styles/mixin.scss +27 -27
  140. package/modules/admin/src/styles/sidebar.scss +133 -133
  141. package/modules/admin/src/styles/transition.scss +46 -46
  142. package/modules/admin/src/styles/variables.scss +4 -4
  143. package/modules/admin/src/utils/auth.js +15 -15
  144. package/modules/admin/src/utils/index.js +74 -74
  145. package/modules/admin/src/utils/request.js +48 -48
  146. package/modules/admin/src/utils/validate.js +31 -31
  147. package/modules/admin/src/views/404.vue +235 -235
  148. package/modules/admin/src/views/Dev.vue +33 -33
  149. package/modules/admin/src/views/EditDocument.vue +121 -116
  150. package/modules/admin/src/views/Endpoints.vue +279 -279
  151. package/modules/admin/src/views/Home.vue +84 -84
  152. package/modules/admin/src/views/Install.vue +79 -79
  153. package/modules/admin/src/views/ListDocuments.vue +72 -72
  154. package/modules/admin/src/views/ListDocuments2.vue +467 -462
  155. package/modules/admin/src/views/ManageListeners.vue +67 -67
  156. package/modules/admin/src/views/ManageMiddleware.vue +43 -43
  157. package/modules/admin/src/views/ManagePermissions.vue +75 -75
  158. package/modules/admin/src/views/ViewRequest.vue +77 -77
  159. package/modules/admin/src/views/form/index.vue +91 -91
  160. package/modules/admin/src/views/layout/Layout.vue +69 -69
  161. package/modules/admin/src/views/layout/components/AppMain.vue +41 -41
  162. package/modules/admin/src/views/layout/components/Navbar.vue +97 -97
  163. package/modules/admin/src/views/layout/components/Sidebar/Item.vue +29 -29
  164. package/modules/admin/src/views/layout/components/Sidebar/Link.vue +39 -39
  165. package/modules/admin/src/views/layout/components/Sidebar/SidebarItem.vue +103 -103
  166. package/modules/admin/src/views/layout/components/Sidebar/index.vue +62 -62
  167. package/modules/admin/src/views/layout/components/index.js +3 -3
  168. package/modules/admin/src/views/layout/mixin/ResizeHandler.js +41 -41
  169. package/modules/admin/src/views/login/index.vue +195 -195
  170. package/modules/admin/src/views/tree/index.vue +77 -77
  171. package/modules/collections/collections.js +39 -39
  172. package/modules/core/core.js +197 -197
  173. package/modules/logging/logging.js +88 -88
  174. package/modules/permissions/permissions.js +106 -106
  175. package/package.json +59 -59
  176. package/scripts/expressa +12 -12
  177. package/scripts/run_cypress_tests.sh +36 -6
  178. package/scripts/run_db_tests.sh +8 -8
  179. package/test/0-install.js +96 -96
  180. package/test/collections.js +431 -431
  181. package/test/collections.querying.js +288 -288
  182. package/test/db.js +258 -258
  183. package/test/db.strings.js +73 -73
  184. package/test/db.updating.js +144 -144
  185. package/test/logging.js +29 -29
  186. package/test/test.js +93 -93
  187. package/test/testserver.js +17 -17
  188. package/test/testutils.js +136 -136
  189. package/test/users.js +425 -425
  190. package/util.js +522 -522
  191. package/modules/admin/dist/index.html.gz +0 -0
  192. package/modules/admin/dist/static/css/app.9dd57eea.css +0 -1
  193. package/modules/admin/dist/static/css/app.9dd57eea.css.gz +0 -0
  194. package/modules/admin/dist/static/css/chunk-5061.ce0e4bdb.css +0 -1
  195. package/modules/admin/dist/static/css/chunk-5061.ce0e4bdb.css.gz +0 -0
  196. package/modules/admin/dist/static/css/chunk-5b97.7f9cb53e.css +0 -1
  197. package/modules/admin/dist/static/css/chunk-5b97.7f9cb53e.css.gz +0 -0
  198. package/modules/admin/dist/static/css/chunk-621e.b826be8d.css +0 -1
  199. package/modules/admin/dist/static/css/chunk-621e.b826be8d.css.gz +0 -0
  200. package/modules/admin/dist/static/css/chunk-656b.9c828b00.css +0 -1
  201. package/modules/admin/dist/static/css/chunk-656b.9c828b00.css.gz +0 -0
  202. package/modules/admin/dist/static/css/chunk-7291.13514e45.css +0 -0
  203. package/modules/admin/dist/static/css/chunk-7291.13514e45.css.gz +0 -0
  204. package/modules/admin/dist/static/css/chunk-7f29.9c828b00.css +0 -1
  205. package/modules/admin/dist/static/css/chunk-7f29.9c828b00.css.gz +0 -0
  206. package/modules/admin/dist/static/css/chunk-9a95.c647a2ef.css +0 -1
  207. package/modules/admin/dist/static/css/chunk-9a95.c647a2ef.css.gz +0 -0
  208. package/modules/admin/dist/static/css/chunk-b423.5f239333.css +0 -0
  209. package/modules/admin/dist/static/css/chunk-b423.5f239333.css.gz +0 -0
  210. package/modules/admin/dist/static/css/chunk-bd5f.aefa49d7.css +0 -1
  211. package/modules/admin/dist/static/css/chunk-bd5f.aefa49d7.css.gz +0 -0
  212. package/modules/admin/dist/static/css/chunk-e374.025d58bb.css +0 -1
  213. package/modules/admin/dist/static/css/chunk-e374.025d58bb.css.gz +0 -0
  214. package/modules/admin/dist/static/css/chunk-elementUI.3d004d9f.css +0 -1
  215. package/modules/admin/dist/static/css/chunk-elementUI.3d004d9f.css.gz +0 -0
  216. package/modules/admin/dist/static/css/chunk-f075.3a895a1e.css +0 -1
  217. package/modules/admin/dist/static/css/chunk-f075.3a895a1e.css.gz +0 -0
  218. package/modules/admin/dist/static/css/chunk-libs.327ad89e.css +0 -14
  219. package/modules/admin/dist/static/css/chunk-libs.327ad89e.css.gz +0 -0
  220. package/modules/admin/dist/static/fonts/element-icons.6f0a763.ttf +0 -0
  221. package/modules/admin/dist/static/js/app.e9461593.js +0 -1
  222. package/modules/admin/dist/static/js/app.e9461593.js.gz +0 -0
  223. package/modules/admin/dist/static/js/chunk-09d8.561880e0.js +0 -2
  224. package/modules/admin/dist/static/js/chunk-09d8.561880e0.js.LICENSE.txt +0 -13
  225. package/modules/admin/dist/static/js/chunk-09d8.561880e0.js.LICENSE.txt.gz +0 -0
  226. package/modules/admin/dist/static/js/chunk-09d8.561880e0.js.gz +0 -0
  227. package/modules/admin/dist/static/js/chunk-5061.83e6ccfa.js +0 -1
  228. package/modules/admin/dist/static/js/chunk-5061.83e6ccfa.js.gz +0 -0
  229. package/modules/admin/dist/static/js/chunk-5b97.ac360086.js +0 -1
  230. package/modules/admin/dist/static/js/chunk-5b97.ac360086.js.gz +0 -0
  231. package/modules/admin/dist/static/js/chunk-5e17.a2b69611.js +0 -1
  232. package/modules/admin/dist/static/js/chunk-5e17.a2b69611.js.gz +0 -0
  233. package/modules/admin/dist/static/js/chunk-621e.ccf42393.js +0 -1
  234. package/modules/admin/dist/static/js/chunk-621e.ccf42393.js.gz +0 -0
  235. package/modules/admin/dist/static/js/chunk-656b.9d5b03c0.js +0 -1
  236. package/modules/admin/dist/static/js/chunk-656b.9d5b03c0.js.gz +0 -0
  237. package/modules/admin/dist/static/js/chunk-7291.590524e9.js +0 -1
  238. package/modules/admin/dist/static/js/chunk-7291.590524e9.js.gz +0 -0
  239. package/modules/admin/dist/static/js/chunk-7f29.6ffc142e.js +0 -1
  240. package/modules/admin/dist/static/js/chunk-7f29.6ffc142e.js.gz +0 -0
  241. package/modules/admin/dist/static/js/chunk-9a95.bdd76583.js +0 -1
  242. package/modules/admin/dist/static/js/chunk-9a95.bdd76583.js.gz +0 -0
  243. package/modules/admin/dist/static/js/chunk-b423.387c504c.js +0 -1
  244. package/modules/admin/dist/static/js/chunk-b423.387c504c.js.gz +0 -0
  245. package/modules/admin/dist/static/js/chunk-bd5f.e9509f10.js +0 -1
  246. package/modules/admin/dist/static/js/chunk-bd5f.e9509f10.js.gz +0 -0
  247. package/modules/admin/dist/static/js/chunk-e374.7c87a0dd.js +0 -1
  248. package/modules/admin/dist/static/js/chunk-e374.7c87a0dd.js.gz +0 -0
  249. package/modules/admin/dist/static/js/chunk-e7c3.e5df8ceb.js +0 -1
  250. package/modules/admin/dist/static/js/chunk-e7c3.e5df8ceb.js.gz +0 -0
  251. package/modules/admin/dist/static/js/chunk-elementUI.b0c99c08.js +0 -1
  252. package/modules/admin/dist/static/js/chunk-elementUI.b0c99c08.js.gz +0 -0
  253. package/modules/admin/dist/static/js/chunk-f075.315ceec1.js +0 -1
  254. package/modules/admin/dist/static/js/chunk-f075.315ceec1.js.gz +0 -0
  255. package/modules/admin/dist/static/js/chunk-libs.06c38828.js +0 -2
  256. package/modules/admin/dist/static/js/chunk-libs.06c38828.js.LICENSE.txt.gz +0 -0
  257. package/modules/admin/dist/static/js/chunk-libs.06c38828.js.gz +0 -0
  258. /package/modules/admin/dist/static/img/{404.a57b6f3.png → 404.4cf6930.png} +0 -0
package/util.js CHANGED
@@ -1,522 +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
- 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
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