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.
- package/.circleci/config.yml +21 -21
- package/.eslintrc.json +29 -29
- package/LICENSE +21 -21
- package/README.md +137 -137
- package/auth/index.js +76 -76
- package/auth/jwt.js +31 -31
- package/controllers/collections.js +260 -260
- package/controllers/install.js +105 -105
- package/controllers/status.js +43 -43
- package/controllers/users.js +52 -52
- package/cypress/fixtures/example.json +4 -4
- package/cypress/integration/0_install_spec.js +8 -8
- package/cypress/integration/custom_collections.js +30 -28
- package/cypress/integration/home.js +9 -9
- package/cypress/integration/settings.js +34 -34
- package/cypress/integration/users.js +61 -61
- package/cypress/plugins/index.js +17 -17
- package/cypress/support/commands.js +69 -54
- package/cypress/support/index.js +20 -20
- package/cypress/support/util.js +10 -10
- package/cypress.config.js +18 -0
- package/cypress.json +4 -4
- package/db/cached.js +40 -40
- package/db/file.js +112 -112
- package/db/memory.js +79 -79
- package/db/mongo.js +86 -86
- package/db/postgres.js +93 -93
- package/doc/authentication.md +38 -38
- package/doc/automatic-fields.md +12 -12
- package/doc/blogexample.md +16 -16
- package/doc/custom-endpoints.md +20 -20
- package/doc/database.md +23 -23
- package/doc/development.md +33 -33
- package/doc/listeners.md +103 -103
- package/doc/modules.md +8 -8
- package/doc/permissions.md +14 -14
- package/doc/relationships.md +65 -65
- package/doc/testing.md +47 -47
- package/doc/uploading-files.md +53 -53
- package/index.js +297 -297
- package/listeners.js +65 -65
- package/listeners_collection_permissions.js +29 -29
- package/listeners_users.js +96 -96
- package/listeners_validation.js +43 -43
- package/middleware/logging.js +30 -30
- package/middleware/permissions.js +41 -41
- package/modules/access_keys/access_keys.js +32 -32
- package/modules/admin/.babelrc +19 -19
- package/modules/admin/.editorconfig +14 -14
- package/modules/admin/.eslintignore +5 -5
- package/modules/admin/.eslintrc.js +197 -197
- package/modules/admin/.postcssrc.js +10 -10
- package/modules/admin/.travis.yml +5 -5
- package/modules/admin/LICENSE +21 -21
- package/modules/admin/README.md +88 -88
- package/modules/admin/build/build.js +46 -45
- package/modules/admin/build/check-versions.js +64 -64
- package/modules/admin/build/utils.js +109 -108
- package/modules/admin/build/vue-loader.conf.js +5 -5
- package/modules/admin/build/webpack.base.conf.js +112 -112
- package/modules/admin/build/webpack.dev.conf.js +95 -95
- package/modules/admin/build/webpack.prod.conf.js +112 -112
- package/modules/admin/config/dev.env.js +8 -8
- package/modules/admin/config/index.js +86 -86
- package/modules/admin/config/prod.env.js +5 -5
- package/modules/admin/dist/css/197.7a90435e.css +7 -0
- package/modules/admin/dist/css/197.7a90435e.css.gz +0 -0
- package/modules/admin/dist/css/430.8c9b0482.css +8 -0
- package/modules/admin/dist/css/430.8c9b0482.css.gz +0 -0
- package/modules/admin/dist/css/451.c6646d20.css +1 -0
- package/modules/admin/dist/css/451.c6646d20.css.gz +0 -0
- package/modules/admin/dist/css/566.7a90435e.css +7 -0
- package/modules/admin/dist/css/566.7a90435e.css.gz +0 -0
- package/modules/admin/dist/css/569.f7d5c7d5.css +37 -0
- package/modules/admin/dist/css/569.f7d5c7d5.css.gz +0 -0
- package/modules/admin/dist/css/572.74d8e965.css +34 -0
- package/modules/admin/dist/css/572.74d8e965.css.gz +0 -0
- package/modules/admin/dist/css/651.e946bda7.css +6 -0
- package/modules/admin/dist/css/651.e946bda7.css.gz +0 -0
- package/modules/admin/dist/css/750.4086915d.css +2 -0
- package/modules/admin/dist/css/750.4086915d.css.gz +0 -0
- package/modules/admin/dist/css/950.1fd9d57c.css +7 -0
- package/modules/admin/dist/css/950.1fd9d57c.css.gz +0 -0
- package/modules/admin/dist/css/app.eba5f5e2.css +592 -0
- package/modules/admin/dist/css/app.eba5f5e2.css.gz +0 -0
- package/modules/admin/dist/index.html +1 -1
- package/modules/admin/dist/static/fonts/element-icons.313f7da.woff +0 -0
- package/modules/admin/dist/static/fonts/element-icons.4520188.ttf +0 -0
- package/modules/admin/dist/static/js/197.e511d91e.js +1 -0
- package/modules/admin/dist/static/js/197.e511d91e.js.gz +0 -0
- package/modules/admin/dist/static/js/430.b3941f30.js +1 -0
- package/modules/admin/dist/static/js/430.b3941f30.js.gz +0 -0
- package/modules/admin/dist/static/js/451.645974a8.js +1 -0
- package/modules/admin/dist/static/js/451.645974a8.js.gz +0 -0
- package/modules/admin/dist/static/js/460.962d5bc0.js +1 -0
- package/modules/admin/dist/static/js/460.962d5bc0.js.gz +0 -0
- package/modules/admin/dist/static/js/550.f27feb23.js +1 -0
- package/modules/admin/dist/static/js/550.f27feb23.js.gz +0 -0
- package/modules/admin/dist/static/js/556.1e08866a.js +1 -0
- package/modules/admin/dist/static/js/556.1e08866a.js.gz +0 -0
- package/modules/admin/dist/static/js/569.0fecfbee.js +1 -0
- package/modules/admin/dist/static/js/569.0fecfbee.js.gz +0 -0
- package/modules/admin/dist/static/js/572.13392e1d.js +1 -0
- package/modules/admin/dist/static/js/572.13392e1d.js.gz +0 -0
- package/modules/admin/dist/static/js/603.7b34a650.js +1 -0
- package/modules/admin/dist/static/js/603.7b34a650.js.gz +0 -0
- package/modules/admin/dist/static/js/651.564b24b6.js +1 -0
- package/modules/admin/dist/static/js/651.564b24b6.js.gz +0 -0
- package/modules/admin/dist/static/js/653.02ec1098.js +1 -0
- package/modules/admin/dist/static/js/653.02ec1098.js.gz +0 -0
- package/modules/admin/dist/static/js/750.50c950da.js +1 -0
- package/modules/admin/dist/static/js/750.50c950da.js.gz +0 -0
- package/modules/admin/dist/static/js/950.d4ff0a18.js +1 -0
- package/modules/admin/dist/static/js/950.d4ff0a18.js.gz +0 -0
- package/modules/admin/dist/static/js/app.ea86ae11.js +2 -0
- package/modules/admin/dist/static/js/{chunk-libs.06c38828.js.LICENSE.txt → app.ea86ae11.js.LICENSE.txt} +15 -18
- package/modules/admin/dist/static/js/app.ea86ae11.js.LICENSE.txt.gz +0 -0
- package/modules/admin/dist/static/js/app.ea86ae11.js.gz +0 -0
- package/modules/admin/index.html +16 -16
- package/modules/admin/package.json +101 -101
- package/modules/admin/src/App.vue +11 -11
- package/modules/admin/src/api/login.js +26 -26
- package/modules/admin/src/api/table.js +9 -9
- package/modules/admin/src/components/Breadcrumb/index.vue +79 -79
- package/modules/admin/src/components/Hamburger/index.vue +62 -62
- package/modules/admin/src/components/JSONEditor.vue +180 -180
- package/modules/admin/src/components/SvgIcon/index.vue +43 -43
- package/modules/admin/src/icons/index.js +9 -9
- package/modules/admin/src/icons/svgo.yml +22 -22
- package/modules/admin/src/main.js +42 -42
- package/modules/admin/src/permission.js +49 -49
- package/modules/admin/src/router/index.js +200 -200
- package/modules/admin/src/store/getters.js +11 -11
- package/modules/admin/src/store/index.js +17 -17
- package/modules/admin/src/store/modules/app.js +43 -43
- package/modules/admin/src/store/modules/user.js +104 -104
- package/modules/admin/src/styles/element-ui.scss +29 -29
- package/modules/admin/src/styles/index.scss +78 -78
- package/modules/admin/src/styles/mixin.scss +27 -27
- package/modules/admin/src/styles/sidebar.scss +133 -133
- package/modules/admin/src/styles/transition.scss +46 -46
- package/modules/admin/src/styles/variables.scss +4 -4
- package/modules/admin/src/utils/auth.js +15 -15
- package/modules/admin/src/utils/index.js +74 -74
- package/modules/admin/src/utils/request.js +48 -48
- package/modules/admin/src/utils/validate.js +31 -31
- package/modules/admin/src/views/404.vue +235 -235
- package/modules/admin/src/views/Dev.vue +33 -33
- package/modules/admin/src/views/EditDocument.vue +121 -116
- package/modules/admin/src/views/Endpoints.vue +279 -279
- package/modules/admin/src/views/Home.vue +84 -84
- package/modules/admin/src/views/Install.vue +79 -79
- package/modules/admin/src/views/ListDocuments.vue +72 -72
- package/modules/admin/src/views/ListDocuments2.vue +467 -462
- package/modules/admin/src/views/ManageListeners.vue +67 -67
- package/modules/admin/src/views/ManageMiddleware.vue +43 -43
- package/modules/admin/src/views/ManagePermissions.vue +75 -75
- package/modules/admin/src/views/ViewRequest.vue +77 -77
- package/modules/admin/src/views/form/index.vue +91 -91
- package/modules/admin/src/views/layout/Layout.vue +69 -69
- package/modules/admin/src/views/layout/components/AppMain.vue +41 -41
- package/modules/admin/src/views/layout/components/Navbar.vue +97 -97
- package/modules/admin/src/views/layout/components/Sidebar/Item.vue +29 -29
- package/modules/admin/src/views/layout/components/Sidebar/Link.vue +39 -39
- package/modules/admin/src/views/layout/components/Sidebar/SidebarItem.vue +103 -103
- package/modules/admin/src/views/layout/components/Sidebar/index.vue +62 -62
- package/modules/admin/src/views/layout/components/index.js +3 -3
- package/modules/admin/src/views/layout/mixin/ResizeHandler.js +41 -41
- package/modules/admin/src/views/login/index.vue +195 -195
- package/modules/admin/src/views/tree/index.vue +77 -77
- package/modules/collections/collections.js +39 -39
- package/modules/core/core.js +197 -197
- package/modules/logging/logging.js +88 -88
- package/modules/permissions/permissions.js +106 -106
- package/package.json +59 -59
- package/scripts/expressa +12 -12
- package/scripts/run_cypress_tests.sh +36 -6
- package/scripts/run_db_tests.sh +8 -8
- package/test/0-install.js +96 -96
- package/test/collections.js +431 -431
- package/test/collections.querying.js +288 -288
- package/test/db.js +258 -258
- package/test/db.strings.js +73 -73
- package/test/db.updating.js +144 -144
- package/test/logging.js +29 -29
- package/test/test.js +93 -93
- package/test/testserver.js +17 -17
- package/test/testutils.js +136 -136
- package/test/users.js +425 -425
- package/util.js +522 -522
- package/modules/admin/dist/index.html.gz +0 -0
- package/modules/admin/dist/static/css/app.9dd57eea.css +0 -1
- package/modules/admin/dist/static/css/app.9dd57eea.css.gz +0 -0
- package/modules/admin/dist/static/css/chunk-5061.ce0e4bdb.css +0 -1
- package/modules/admin/dist/static/css/chunk-5061.ce0e4bdb.css.gz +0 -0
- package/modules/admin/dist/static/css/chunk-5b97.7f9cb53e.css +0 -1
- package/modules/admin/dist/static/css/chunk-5b97.7f9cb53e.css.gz +0 -0
- package/modules/admin/dist/static/css/chunk-621e.b826be8d.css +0 -1
- package/modules/admin/dist/static/css/chunk-621e.b826be8d.css.gz +0 -0
- package/modules/admin/dist/static/css/chunk-656b.9c828b00.css +0 -1
- package/modules/admin/dist/static/css/chunk-656b.9c828b00.css.gz +0 -0
- package/modules/admin/dist/static/css/chunk-7291.13514e45.css +0 -0
- package/modules/admin/dist/static/css/chunk-7291.13514e45.css.gz +0 -0
- package/modules/admin/dist/static/css/chunk-7f29.9c828b00.css +0 -1
- package/modules/admin/dist/static/css/chunk-7f29.9c828b00.css.gz +0 -0
- package/modules/admin/dist/static/css/chunk-9a95.c647a2ef.css +0 -1
- package/modules/admin/dist/static/css/chunk-9a95.c647a2ef.css.gz +0 -0
- package/modules/admin/dist/static/css/chunk-b423.5f239333.css +0 -0
- package/modules/admin/dist/static/css/chunk-b423.5f239333.css.gz +0 -0
- package/modules/admin/dist/static/css/chunk-bd5f.aefa49d7.css +0 -1
- package/modules/admin/dist/static/css/chunk-bd5f.aefa49d7.css.gz +0 -0
- package/modules/admin/dist/static/css/chunk-e374.025d58bb.css +0 -1
- package/modules/admin/dist/static/css/chunk-e374.025d58bb.css.gz +0 -0
- package/modules/admin/dist/static/css/chunk-elementUI.3d004d9f.css +0 -1
- package/modules/admin/dist/static/css/chunk-elementUI.3d004d9f.css.gz +0 -0
- package/modules/admin/dist/static/css/chunk-f075.3a895a1e.css +0 -1
- package/modules/admin/dist/static/css/chunk-f075.3a895a1e.css.gz +0 -0
- package/modules/admin/dist/static/css/chunk-libs.327ad89e.css +0 -14
- package/modules/admin/dist/static/css/chunk-libs.327ad89e.css.gz +0 -0
- package/modules/admin/dist/static/fonts/element-icons.6f0a763.ttf +0 -0
- package/modules/admin/dist/static/js/app.e9461593.js +0 -1
- package/modules/admin/dist/static/js/app.e9461593.js.gz +0 -0
- package/modules/admin/dist/static/js/chunk-09d8.561880e0.js +0 -2
- package/modules/admin/dist/static/js/chunk-09d8.561880e0.js.LICENSE.txt +0 -13
- package/modules/admin/dist/static/js/chunk-09d8.561880e0.js.LICENSE.txt.gz +0 -0
- package/modules/admin/dist/static/js/chunk-09d8.561880e0.js.gz +0 -0
- package/modules/admin/dist/static/js/chunk-5061.83e6ccfa.js +0 -1
- package/modules/admin/dist/static/js/chunk-5061.83e6ccfa.js.gz +0 -0
- package/modules/admin/dist/static/js/chunk-5b97.ac360086.js +0 -1
- package/modules/admin/dist/static/js/chunk-5b97.ac360086.js.gz +0 -0
- package/modules/admin/dist/static/js/chunk-5e17.a2b69611.js +0 -1
- package/modules/admin/dist/static/js/chunk-5e17.a2b69611.js.gz +0 -0
- package/modules/admin/dist/static/js/chunk-621e.ccf42393.js +0 -1
- package/modules/admin/dist/static/js/chunk-621e.ccf42393.js.gz +0 -0
- package/modules/admin/dist/static/js/chunk-656b.9d5b03c0.js +0 -1
- package/modules/admin/dist/static/js/chunk-656b.9d5b03c0.js.gz +0 -0
- package/modules/admin/dist/static/js/chunk-7291.590524e9.js +0 -1
- package/modules/admin/dist/static/js/chunk-7291.590524e9.js.gz +0 -0
- package/modules/admin/dist/static/js/chunk-7f29.6ffc142e.js +0 -1
- package/modules/admin/dist/static/js/chunk-7f29.6ffc142e.js.gz +0 -0
- package/modules/admin/dist/static/js/chunk-9a95.bdd76583.js +0 -1
- package/modules/admin/dist/static/js/chunk-9a95.bdd76583.js.gz +0 -0
- package/modules/admin/dist/static/js/chunk-b423.387c504c.js +0 -1
- package/modules/admin/dist/static/js/chunk-b423.387c504c.js.gz +0 -0
- package/modules/admin/dist/static/js/chunk-bd5f.e9509f10.js +0 -1
- package/modules/admin/dist/static/js/chunk-bd5f.e9509f10.js.gz +0 -0
- package/modules/admin/dist/static/js/chunk-e374.7c87a0dd.js +0 -1
- package/modules/admin/dist/static/js/chunk-e374.7c87a0dd.js.gz +0 -0
- package/modules/admin/dist/static/js/chunk-e7c3.e5df8ceb.js +0 -1
- package/modules/admin/dist/static/js/chunk-e7c3.e5df8ceb.js.gz +0 -0
- package/modules/admin/dist/static/js/chunk-elementUI.b0c99c08.js +0 -1
- package/modules/admin/dist/static/js/chunk-elementUI.b0c99c08.js.gz +0 -0
- package/modules/admin/dist/static/js/chunk-f075.315ceec1.js +0 -1
- package/modules/admin/dist/static/js/chunk-f075.315ceec1.js.gz +0 -0
- package/modules/admin/dist/static/js/chunk-libs.06c38828.js +0 -2
- package/modules/admin/dist/static/js/chunk-libs.06c38828.js.LICENSE.txt.gz +0 -0
- package/modules/admin/dist/static/js/chunk-libs.06c38828.js.gz +0 -0
- /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
|