expressa 2.0.17 → 2.0.18

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 (281) 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 -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 -41
  46. package/modules/access_keys/access_keys.js +32 -32
  47. package/modules/admin/.babelrc +19 -19
  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 +112 -112
  60. package/modules/admin/build/webpack.dev.conf.js +95 -95
  61. package/modules/admin/build/webpack.prod.conf.js +112 -112
  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/dist/index.html +1 -1
  66. package/modules/admin/dist/index.html.gz +0 -0
  67. package/modules/admin/dist/static/css/app.9dd57eea.css +1 -0
  68. package/modules/admin/dist/static/css/app.9dd57eea.css.gz +0 -0
  69. package/modules/admin/dist/static/css/chunk-5061.ce0e4bdb.css +1 -0
  70. package/modules/admin/dist/static/css/chunk-5061.ce0e4bdb.css.gz +0 -0
  71. package/modules/admin/dist/static/css/chunk-5b97.7f9cb53e.css +1 -0
  72. package/modules/admin/dist/static/css/chunk-5b97.7f9cb53e.css.gz +0 -0
  73. package/modules/admin/dist/static/css/chunk-621e.b826be8d.css +1 -0
  74. package/modules/admin/dist/static/css/chunk-621e.b826be8d.css.gz +0 -0
  75. package/modules/admin/dist/static/css/chunk-656b.9c828b00.css +1 -0
  76. package/modules/admin/dist/static/css/chunk-656b.9c828b00.css.gz +0 -0
  77. package/modules/admin/dist/static/css/chunk-7291.13514e45.css +0 -0
  78. package/modules/admin/dist/static/css/chunk-7291.13514e45.css.gz +0 -0
  79. package/modules/admin/dist/static/css/chunk-7f29.9c828b00.css +1 -0
  80. package/modules/admin/dist/static/css/chunk-7f29.9c828b00.css.gz +0 -0
  81. package/modules/admin/dist/static/css/chunk-9a95.c647a2ef.css +1 -0
  82. package/modules/admin/dist/static/css/chunk-9a95.c647a2ef.css.gz +0 -0
  83. package/modules/admin/dist/static/css/chunk-b423.5f239333.css +0 -0
  84. package/modules/admin/dist/static/css/chunk-b423.5f239333.css.gz +0 -0
  85. package/modules/admin/dist/static/css/chunk-bd5f.aefa49d7.css +1 -0
  86. package/modules/admin/dist/static/css/chunk-bd5f.aefa49d7.css.gz +0 -0
  87. package/modules/admin/dist/static/css/chunk-e374.025d58bb.css +1 -0
  88. package/modules/admin/dist/static/css/chunk-e374.025d58bb.css.gz +0 -0
  89. package/modules/admin/dist/static/css/chunk-elementUI.3d004d9f.css +1 -0
  90. package/modules/admin/dist/static/css/chunk-elementUI.3d004d9f.css.gz +0 -0
  91. package/modules/admin/dist/static/css/chunk-f075.3a895a1e.css +1 -0
  92. package/modules/admin/dist/static/css/chunk-f075.3a895a1e.css.gz +0 -0
  93. package/modules/admin/dist/static/css/chunk-libs.327ad89e.css +14 -0
  94. package/modules/admin/dist/static/css/chunk-libs.327ad89e.css.gz +0 -0
  95. package/modules/admin/dist/static/fonts/element-icons.6f0a763.ttf +0 -0
  96. package/modules/admin/dist/static/js/app.e9461593.js +1 -0
  97. package/modules/admin/dist/static/js/app.e9461593.js.gz +0 -0
  98. package/modules/admin/dist/static/js/chunk-09d8.561880e0.js +2 -0
  99. package/modules/admin/dist/static/js/chunk-09d8.561880e0.js.LICENSE.txt +13 -0
  100. package/modules/admin/dist/static/js/chunk-09d8.561880e0.js.LICENSE.txt.gz +0 -0
  101. package/modules/admin/dist/static/js/chunk-09d8.561880e0.js.gz +0 -0
  102. package/modules/admin/dist/static/js/chunk-5061.83e6ccfa.js +1 -0
  103. package/modules/admin/dist/static/js/chunk-5061.83e6ccfa.js.gz +0 -0
  104. package/modules/admin/dist/static/js/chunk-5b97.ac360086.js +1 -0
  105. package/modules/admin/dist/static/js/chunk-5b97.ac360086.js.gz +0 -0
  106. package/modules/admin/dist/static/js/chunk-5e17.a2b69611.js +1 -0
  107. package/modules/admin/dist/static/js/chunk-5e17.a2b69611.js.gz +0 -0
  108. package/modules/admin/dist/static/js/chunk-621e.ccf42393.js +1 -0
  109. package/modules/admin/dist/static/js/chunk-621e.ccf42393.js.gz +0 -0
  110. package/modules/admin/dist/static/js/chunk-656b.9d5b03c0.js +1 -0
  111. package/modules/admin/dist/static/js/chunk-656b.9d5b03c0.js.gz +0 -0
  112. package/modules/admin/dist/static/js/chunk-7291.590524e9.js +1 -0
  113. package/modules/admin/dist/static/js/chunk-7291.590524e9.js.gz +0 -0
  114. package/modules/admin/dist/static/js/chunk-7f29.6ffc142e.js +1 -0
  115. package/modules/admin/dist/static/js/chunk-7f29.6ffc142e.js.gz +0 -0
  116. package/modules/admin/dist/static/js/chunk-9a95.bdd76583.js +1 -0
  117. package/modules/admin/dist/static/js/chunk-9a95.bdd76583.js.gz +0 -0
  118. package/modules/admin/dist/static/js/chunk-b423.387c504c.js +1 -0
  119. package/modules/admin/dist/static/js/chunk-b423.387c504c.js.gz +0 -0
  120. package/modules/admin/dist/static/js/chunk-bd5f.e9509f10.js +1 -0
  121. package/modules/admin/dist/static/js/chunk-bd5f.e9509f10.js.gz +0 -0
  122. package/modules/admin/dist/static/js/chunk-e374.7c87a0dd.js +1 -0
  123. package/modules/admin/dist/static/js/chunk-e374.7c87a0dd.js.gz +0 -0
  124. package/modules/admin/dist/static/js/chunk-e7c3.e5df8ceb.js +1 -0
  125. package/modules/admin/dist/static/js/chunk-e7c3.e5df8ceb.js.gz +0 -0
  126. package/modules/admin/dist/static/js/chunk-elementUI.b0c99c08.js +1 -0
  127. package/modules/admin/dist/static/js/chunk-elementUI.b0c99c08.js.gz +0 -0
  128. package/modules/admin/dist/static/js/chunk-f075.315ceec1.js +1 -0
  129. package/modules/admin/dist/static/js/chunk-f075.315ceec1.js.gz +0 -0
  130. package/modules/admin/dist/static/js/chunk-libs.06c38828.js +2 -0
  131. package/modules/admin/dist/static/js/{app.981e71b3.js.LICENSE.txt → chunk-libs.06c38828.js.LICENSE.txt} +18 -15
  132. package/modules/admin/dist/static/js/chunk-libs.06c38828.js.LICENSE.txt.gz +0 -0
  133. package/modules/admin/dist/static/js/chunk-libs.06c38828.js.gz +0 -0
  134. package/modules/admin/index.html +16 -16
  135. package/modules/admin/package.json +101 -101
  136. package/modules/admin/src/App.vue +11 -11
  137. package/modules/admin/src/api/login.js +26 -26
  138. package/modules/admin/src/api/table.js +9 -9
  139. package/modules/admin/src/components/Breadcrumb/index.vue +79 -79
  140. package/modules/admin/src/components/Hamburger/index.vue +62 -62
  141. package/modules/admin/src/components/JSONEditor.vue +180 -180
  142. package/modules/admin/src/components/SvgIcon/index.vue +43 -43
  143. package/modules/admin/src/icons/index.js +9 -9
  144. package/modules/admin/src/icons/svgo.yml +22 -22
  145. package/modules/admin/src/main.js +42 -42
  146. package/modules/admin/src/permission.js +49 -49
  147. package/modules/admin/src/router/index.js +200 -200
  148. package/modules/admin/src/store/getters.js +11 -11
  149. package/modules/admin/src/store/index.js +17 -17
  150. package/modules/admin/src/store/modules/app.js +43 -43
  151. package/modules/admin/src/store/modules/user.js +104 -104
  152. package/modules/admin/src/styles/element-ui.scss +29 -29
  153. package/modules/admin/src/styles/index.scss +78 -78
  154. package/modules/admin/src/styles/mixin.scss +27 -27
  155. package/modules/admin/src/styles/sidebar.scss +133 -133
  156. package/modules/admin/src/styles/transition.scss +46 -46
  157. package/modules/admin/src/styles/variables.scss +4 -4
  158. package/modules/admin/src/utils/auth.js +15 -15
  159. package/modules/admin/src/utils/index.js +74 -74
  160. package/modules/admin/src/utils/request.js +48 -48
  161. package/modules/admin/src/utils/validate.js +31 -31
  162. package/modules/admin/src/views/404.vue +235 -235
  163. package/modules/admin/src/views/Dev.vue +33 -33
  164. package/modules/admin/src/views/EditDocument.vue +116 -116
  165. package/modules/admin/src/views/Endpoints.vue +279 -279
  166. package/modules/admin/src/views/Home.vue +84 -84
  167. package/modules/admin/src/views/Install.vue +79 -79
  168. package/modules/admin/src/views/ListDocuments.vue +72 -72
  169. package/modules/admin/src/views/ListDocuments2.vue +462 -462
  170. package/modules/admin/src/views/ManageListeners.vue +67 -67
  171. package/modules/admin/src/views/ManageMiddleware.vue +43 -43
  172. package/modules/admin/src/views/ManagePermissions.vue +75 -75
  173. package/modules/admin/src/views/ViewRequest.vue +77 -77
  174. package/modules/admin/src/views/form/index.vue +91 -91
  175. package/modules/admin/src/views/layout/Layout.vue +69 -69
  176. package/modules/admin/src/views/layout/components/AppMain.vue +41 -41
  177. package/modules/admin/src/views/layout/components/Navbar.vue +97 -97
  178. package/modules/admin/src/views/layout/components/Sidebar/Item.vue +29 -29
  179. package/modules/admin/src/views/layout/components/Sidebar/Link.vue +39 -39
  180. package/modules/admin/src/views/layout/components/Sidebar/SidebarItem.vue +103 -103
  181. package/modules/admin/src/views/layout/components/Sidebar/index.vue +62 -62
  182. package/modules/admin/src/views/layout/components/index.js +3 -3
  183. package/modules/admin/src/views/layout/mixin/ResizeHandler.js +41 -41
  184. package/modules/admin/src/views/login/index.vue +195 -195
  185. package/modules/admin/src/views/tree/index.vue +77 -77
  186. package/modules/collections/collections.js +39 -39
  187. package/modules/core/core.js +197 -197
  188. package/modules/logging/logging.js +88 -88
  189. package/modules/permissions/permissions.js +106 -106
  190. package/package.json +59 -59
  191. package/scripts/expressa +12 -12
  192. package/scripts/run_cypress_tests.sh +5 -5
  193. package/scripts/run_db_tests.sh +8 -8
  194. package/test/0-install.js +96 -96
  195. package/test/collections.js +431 -431
  196. package/test/collections.querying.js +288 -288
  197. package/test/db.js +258 -258
  198. package/test/db.strings.js +73 -73
  199. package/test/db.updating.js +144 -144
  200. package/test/logging.js +29 -29
  201. package/test/test.js +93 -93
  202. package/test/testserver.js +17 -17
  203. package/test/testutils.js +136 -136
  204. package/test/users.js +425 -425
  205. package/util.js +522 -522
  206. package/modules/admin/dist/164c6167c1f3c497c07c.woff +0 -1
  207. package/modules/admin/dist/42ac07abc8473600c41f.ttf +0 -1
  208. package/modules/admin/dist/86dccb18a7eb4be8ef1a.woff +0 -1
  209. package/modules/admin/dist/c45297c558e0318fc9c5.ttf +0 -1
  210. package/modules/admin/dist/c717c9930715e24fa12c.woff +0 -1
  211. package/modules/admin/dist/ca6f07ef23f24cc2738a.ttf +0 -1
  212. package/modules/admin/dist/css/269.f7d5c7d5.css +0 -37
  213. package/modules/admin/dist/css/269.f7d5c7d5.css.gz +0 -0
  214. package/modules/admin/dist/css/451.c6646d20.css +0 -1
  215. package/modules/admin/dist/css/451.c6646d20.css.gz +0 -0
  216. package/modules/admin/dist/css/572.74d8e965.css +0 -34
  217. package/modules/admin/dist/css/572.74d8e965.css.gz +0 -0
  218. package/modules/admin/dist/css/651.e946bda7.css +0 -6
  219. package/modules/admin/dist/css/651.e946bda7.css.gz +0 -0
  220. package/modules/admin/dist/css/699.d1f90954.css +0 -8
  221. package/modules/admin/dist/css/699.d1f90954.css.gz +0 -0
  222. package/modules/admin/dist/css/750.4086915d.css +0 -2
  223. package/modules/admin/dist/css/750.4086915d.css.gz +0 -0
  224. package/modules/admin/dist/css/864.506917a4.css +0 -7
  225. package/modules/admin/dist/css/864.506917a4.css.gz +0 -0
  226. package/modules/admin/dist/css/899.506917a4.css +0 -7
  227. package/modules/admin/dist/css/899.506917a4.css.gz +0 -0
  228. package/modules/admin/dist/css/950.1fd9d57c.css +0 -7
  229. package/modules/admin/dist/css/950.1fd9d57c.css.gz +0 -0
  230. package/modules/admin/dist/css/app.11cc6245.css +0 -39
  231. package/modules/admin/dist/css/app.11cc6245.css.gz +0 -0
  232. package/modules/admin/dist/css/app.2e1b0d79.css +0 -591
  233. package/modules/admin/dist/css/app.2e1b0d79.css.gz +0 -0
  234. package/modules/admin/dist/css/app.4d43d0fd.css +0 -592
  235. package/modules/admin/dist/css/app.4d43d0fd.css.gz +0 -0
  236. package/modules/admin/dist/css/app.abace109.css +0 -1
  237. package/modules/admin/dist/css/app.abace109.css.gz +0 -0
  238. package/modules/admin/dist/css/chunk-elementUI.67ad86d7.css +0 -1
  239. package/modules/admin/dist/css/chunk-elementUI.67ad86d7.css.gz +0 -0
  240. package/modules/admin/dist/css/chunk-elementUI.9e660bfd.css +0 -1
  241. package/modules/admin/dist/css/chunk-elementUI.9e660bfd.css.gz +0 -0
  242. package/modules/admin/dist/css/chunk-elementUI.c77cf142.css +0 -1
  243. package/modules/admin/dist/css/chunk-elementUI.c77cf142.css.gz +0 -0
  244. package/modules/admin/dist/css/chunk-elementUI.f1625b35.css +0 -1
  245. package/modules/admin/dist/css/chunk-elementUI.f1625b35.css.gz +0 -0
  246. package/modules/admin/dist/css/chunk-elementUI.f25dfb9b.css +0 -1
  247. package/modules/admin/dist/css/chunk-elementUI.f25dfb9b.css.gz +0 -0
  248. package/modules/admin/dist/css/chunk-libs.de0d8484.css +0 -551
  249. package/modules/admin/dist/css/chunk-libs.de0d8484.css.gz +0 -0
  250. package/modules/admin/dist/static/fonts/element-icons.313f7da.woff +0 -0
  251. package/modules/admin/dist/static/fonts/element-icons.4520188.ttf +0 -0
  252. package/modules/admin/dist/static/js/269.7245ceaf.js +0 -1
  253. package/modules/admin/dist/static/js/269.7245ceaf.js.gz +0 -0
  254. package/modules/admin/dist/static/js/451.645974a8.js +0 -1
  255. package/modules/admin/dist/static/js/451.645974a8.js.gz +0 -0
  256. package/modules/admin/dist/static/js/460.962d5bc0.js +0 -1
  257. package/modules/admin/dist/static/js/460.962d5bc0.js.gz +0 -0
  258. package/modules/admin/dist/static/js/556.1e08866a.js +0 -1
  259. package/modules/admin/dist/static/js/556.1e08866a.js.gz +0 -0
  260. package/modules/admin/dist/static/js/572.13392e1d.js +0 -1
  261. package/modules/admin/dist/static/js/572.13392e1d.js.gz +0 -0
  262. package/modules/admin/dist/static/js/603.7b34a650.js +0 -1
  263. package/modules/admin/dist/static/js/603.7b34a650.js.gz +0 -0
  264. package/modules/admin/dist/static/js/637.26413baa.js +0 -1
  265. package/modules/admin/dist/static/js/637.26413baa.js.gz +0 -0
  266. package/modules/admin/dist/static/js/651.564b24b6.js +0 -1
  267. package/modules/admin/dist/static/js/651.564b24b6.js.gz +0 -0
  268. package/modules/admin/dist/static/js/653.02ec1098.js +0 -1
  269. package/modules/admin/dist/static/js/653.02ec1098.js.gz +0 -0
  270. package/modules/admin/dist/static/js/699.c7564884.js +0 -1
  271. package/modules/admin/dist/static/js/699.c7564884.js.gz +0 -0
  272. package/modules/admin/dist/static/js/750.50c950da.js +0 -1
  273. package/modules/admin/dist/static/js/750.50c950da.js.gz +0 -0
  274. package/modules/admin/dist/static/js/864.4f43847d.js +0 -1
  275. package/modules/admin/dist/static/js/864.4f43847d.js.gz +0 -0
  276. package/modules/admin/dist/static/js/950.d4ff0a18.js +0 -1
  277. package/modules/admin/dist/static/js/950.d4ff0a18.js.gz +0 -0
  278. package/modules/admin/dist/static/js/app.981e71b3.js +0 -2
  279. package/modules/admin/dist/static/js/app.981e71b3.js.LICENSE.txt.gz +0 -0
  280. package/modules/admin/dist/static/js/app.981e71b3.js.gz +0 -0
  281. /package/modules/admin/dist/static/img/{404.4cf6930.png → 404.a57b6f3.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