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/db/postgres.js
CHANGED
|
@@ -1,93 +1,93 @@
|
|
|
1
|
-
const util = require('../util')
|
|
2
|
-
|
|
3
|
-
module.exports = function (settings, collectionId, collection) {
|
|
4
|
-
const pool = util.getPgPool(settings.postgresql_uri)
|
|
5
|
-
|
|
6
|
-
return {
|
|
7
|
-
init: async function () {
|
|
8
|
-
if (collection.plainStringIds) {
|
|
9
|
-
await pool.query('CREATE TABLE IF NOT EXISTS ' + collectionId + ' (id text primary key, data jsonb)')
|
|
10
|
-
} else {
|
|
11
|
-
await pool.query('CREATE TABLE IF NOT EXISTS ' + collectionId + ' (id uuid primary key, data jsonb)')
|
|
12
|
-
}
|
|
13
|
-
},
|
|
14
|
-
all: async function () {
|
|
15
|
-
return this.find({})
|
|
16
|
-
},
|
|
17
|
-
find: async function (rawQuery, offset, limit, orderby, fields) {
|
|
18
|
-
const pgSelect = util.mongoToPostgresSelect(collectionId, fields)
|
|
19
|
-
const pgWhere = util.mongoToPostgresWhere(collectionId, rawQuery)
|
|
20
|
-
let query = 'SELECT ' + pgSelect + ' FROM ' + collectionId + (pgWhere ? ' WHERE ' + pgWhere : '')
|
|
21
|
-
if (typeof orderby !== 'undefined') {
|
|
22
|
-
query += ' ORDER BY '
|
|
23
|
-
query += util.mongoToPostgresOrderBy(collectionId, orderby)
|
|
24
|
-
}
|
|
25
|
-
if (typeof offset !== 'undefined') {
|
|
26
|
-
query += ' OFFSET ' + offset
|
|
27
|
-
}
|
|
28
|
-
if (typeof limit !== 'undefined') {
|
|
29
|
-
query += ' LIMIT ' + limit
|
|
30
|
-
}
|
|
31
|
-
const result = await pool.query(query)
|
|
32
|
-
return result.rows.map((row) => row.data)
|
|
33
|
-
},
|
|
34
|
-
count: async function(rawQuery, offset, limit) {
|
|
35
|
-
const pgWhere = util.mongoToPostgresWhere(collectionId, rawQuery)
|
|
36
|
-
let query = 'SELECT COUNT(*) FROM ' + collectionId + (pgWhere ? ' WHERE ' + pgWhere : '')
|
|
37
|
-
if (typeof offset !== 'undefined') {
|
|
38
|
-
query += ' OFFSET ' + offset
|
|
39
|
-
}
|
|
40
|
-
if (typeof limit !== 'undefined') {
|
|
41
|
-
query += ' LIMIT ' + limit
|
|
42
|
-
}
|
|
43
|
-
const result = await pool.query(query)
|
|
44
|
-
return parseInt(result.rows[0].count)
|
|
45
|
-
},
|
|
46
|
-
get: async function (id, fields) {
|
|
47
|
-
const pgSelect = util.mongoToPostgresSelect(collectionId, fields)
|
|
48
|
-
const result = await pool.query(`SELECT ${pgSelect} FROM ${collectionId} WHERE id = $1`, [id])
|
|
49
|
-
if (result.rowCount === 0) {
|
|
50
|
-
throw new util.ApiError(404, 'document not found')
|
|
51
|
-
}
|
|
52
|
-
return result.rows[0].data
|
|
53
|
-
},
|
|
54
|
-
create: async function (data) {
|
|
55
|
-
util.addIdIfMissing(data)
|
|
56
|
-
try {
|
|
57
|
-
await pool.query('INSERT INTO ' + collectionId + ' (id, data) VALUES ($1, $2)', [data._id, data])
|
|
58
|
-
} catch (e) {
|
|
59
|
-
if (e.message.includes('duplicate key')) {
|
|
60
|
-
throw new util.ApiError(409, 'document already exists')
|
|
61
|
-
}
|
|
62
|
-
throw e
|
|
63
|
-
}
|
|
64
|
-
return data._id
|
|
65
|
-
},
|
|
66
|
-
update: async function (id, data) {
|
|
67
|
-
if (typeof data._id === 'undefined') {
|
|
68
|
-
data._id = id
|
|
69
|
-
}
|
|
70
|
-
const result = await pool.query('UPDATE ' + collectionId + ' SET data=$2,id=$3 WHERE id=$1', [id, data, data._id])
|
|
71
|
-
if (result.rowCount === 0) {
|
|
72
|
-
throw new util.ApiError(404, 'document not found')
|
|
73
|
-
}
|
|
74
|
-
return data
|
|
75
|
-
},
|
|
76
|
-
// eslint-disable-next-line no-unused-vars
|
|
77
|
-
updateWithQuery: async function (query, update, options) {
|
|
78
|
-
const pgWhere = util.mongoToPostgresWhere(collectionId, query)
|
|
79
|
-
const updateSql = util.mongoToPostgresUpdate(collectionId, update)
|
|
80
|
-
const result = await pool.query('UPDATE ' + collectionId + ' SET data=' + updateSql + ' WHERE ' + pgWhere)
|
|
81
|
-
return {
|
|
82
|
-
matchedCount: result.rowCount
|
|
83
|
-
}
|
|
84
|
-
},
|
|
85
|
-
delete: async function (id) {
|
|
86
|
-
const result = await pool.query('DELETE FROM ' + collectionId + ' WHERE id=$1', [id])
|
|
87
|
-
if (result.rowCount === 0) {
|
|
88
|
-
throw new util.ApiError(404, 'document not found')
|
|
89
|
-
}
|
|
90
|
-
return 'OK'
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
1
|
+
const util = require('../util')
|
|
2
|
+
|
|
3
|
+
module.exports = function (settings, collectionId, collection) {
|
|
4
|
+
const pool = util.getPgPool(settings.postgresql_uri)
|
|
5
|
+
|
|
6
|
+
return {
|
|
7
|
+
init: async function () {
|
|
8
|
+
if (collection.plainStringIds) {
|
|
9
|
+
await pool.query('CREATE TABLE IF NOT EXISTS ' + collectionId + ' (id text primary key, data jsonb)')
|
|
10
|
+
} else {
|
|
11
|
+
await pool.query('CREATE TABLE IF NOT EXISTS ' + collectionId + ' (id uuid primary key, data jsonb)')
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
all: async function () {
|
|
15
|
+
return this.find({})
|
|
16
|
+
},
|
|
17
|
+
find: async function (rawQuery, offset, limit, orderby, fields) {
|
|
18
|
+
const pgSelect = util.mongoToPostgresSelect(collectionId, fields)
|
|
19
|
+
const pgWhere = util.mongoToPostgresWhere(collectionId, rawQuery)
|
|
20
|
+
let query = 'SELECT ' + pgSelect + ' FROM ' + collectionId + (pgWhere ? ' WHERE ' + pgWhere : '')
|
|
21
|
+
if (typeof orderby !== 'undefined') {
|
|
22
|
+
query += ' ORDER BY '
|
|
23
|
+
query += util.mongoToPostgresOrderBy(collectionId, orderby)
|
|
24
|
+
}
|
|
25
|
+
if (typeof offset !== 'undefined') {
|
|
26
|
+
query += ' OFFSET ' + offset
|
|
27
|
+
}
|
|
28
|
+
if (typeof limit !== 'undefined') {
|
|
29
|
+
query += ' LIMIT ' + limit
|
|
30
|
+
}
|
|
31
|
+
const result = await pool.query(query)
|
|
32
|
+
return result.rows.map((row) => row.data)
|
|
33
|
+
},
|
|
34
|
+
count: async function(rawQuery, offset, limit) {
|
|
35
|
+
const pgWhere = util.mongoToPostgresWhere(collectionId, rawQuery)
|
|
36
|
+
let query = 'SELECT COUNT(*) FROM ' + collectionId + (pgWhere ? ' WHERE ' + pgWhere : '')
|
|
37
|
+
if (typeof offset !== 'undefined') {
|
|
38
|
+
query += ' OFFSET ' + offset
|
|
39
|
+
}
|
|
40
|
+
if (typeof limit !== 'undefined') {
|
|
41
|
+
query += ' LIMIT ' + limit
|
|
42
|
+
}
|
|
43
|
+
const result = await pool.query(query)
|
|
44
|
+
return parseInt(result.rows[0].count)
|
|
45
|
+
},
|
|
46
|
+
get: async function (id, fields) {
|
|
47
|
+
const pgSelect = util.mongoToPostgresSelect(collectionId, fields)
|
|
48
|
+
const result = await pool.query(`SELECT ${pgSelect} FROM ${collectionId} WHERE id = $1`, [id])
|
|
49
|
+
if (result.rowCount === 0) {
|
|
50
|
+
throw new util.ApiError(404, 'document not found')
|
|
51
|
+
}
|
|
52
|
+
return result.rows[0].data
|
|
53
|
+
},
|
|
54
|
+
create: async function (data) {
|
|
55
|
+
util.addIdIfMissing(data)
|
|
56
|
+
try {
|
|
57
|
+
await pool.query('INSERT INTO ' + collectionId + ' (id, data) VALUES ($1, $2)', [data._id, data])
|
|
58
|
+
} catch (e) {
|
|
59
|
+
if (e.message.includes('duplicate key')) {
|
|
60
|
+
throw new util.ApiError(409, 'document already exists')
|
|
61
|
+
}
|
|
62
|
+
throw e
|
|
63
|
+
}
|
|
64
|
+
return data._id
|
|
65
|
+
},
|
|
66
|
+
update: async function (id, data) {
|
|
67
|
+
if (typeof data._id === 'undefined') {
|
|
68
|
+
data._id = id
|
|
69
|
+
}
|
|
70
|
+
const result = await pool.query('UPDATE ' + collectionId + ' SET data=$2,id=$3 WHERE id=$1', [id, data, data._id])
|
|
71
|
+
if (result.rowCount === 0) {
|
|
72
|
+
throw new util.ApiError(404, 'document not found')
|
|
73
|
+
}
|
|
74
|
+
return data
|
|
75
|
+
},
|
|
76
|
+
// eslint-disable-next-line no-unused-vars
|
|
77
|
+
updateWithQuery: async function (query, update, options) {
|
|
78
|
+
const pgWhere = util.mongoToPostgresWhere(collectionId, query)
|
|
79
|
+
const updateSql = util.mongoToPostgresUpdate(collectionId, update)
|
|
80
|
+
const result = await pool.query('UPDATE ' + collectionId + ' SET data=' + updateSql + ' WHERE ' + pgWhere)
|
|
81
|
+
return {
|
|
82
|
+
matchedCount: result.rowCount
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
delete: async function (id) {
|
|
86
|
+
const result = await pool.query('DELETE FROM ' + collectionId + ' WHERE id=$1', [id])
|
|
87
|
+
if (result.rowCount === 0) {
|
|
88
|
+
throw new util.ApiError(404, 'document not found')
|
|
89
|
+
}
|
|
90
|
+
return 'OK'
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
package/doc/authentication.md
CHANGED
|
@@ -1,38 +1,38 @@
|
|
|
1
|
-
## Authentication
|
|
2
|
-
Uses [JSON Web Tokens](https://jwt.io/)
|
|
3
|
-
|
|
4
|
-
| method | endpoint | description |
|
|
5
|
-
|--------|-------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
6
|
-
| POST | /users/login | expects JSON in the message body. e.g. `{"email": "email@example.com", password: "<the password>"} |
|
|
7
|
-
|
|
8
|
-
Returns the following
|
|
9
|
-
```javascript
|
|
10
|
-
{
|
|
11
|
-
"uid": "a52ee085-ff47-4ee1-bac3-a8a319f674ed",
|
|
12
|
-
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJfaWQiOiI1NmNkNWM5NmY4MjA4N2I1MDQ0OTM3YjEiLCJ1bml2ZXJzaXR5IjoiQllVIiwiZnVsbE5hbWUiOiJUaG9tYXMgSGFuc2VuIiwicGFzc3dvcmQiOiIkMmEkMTAkb0prdlBnTTlkR2FJRTIzaWFabGEvT0tjZC9PL3phSGFJOHFRUDBuZ2pPUVV1Ums3Vng2QkciLCJlbWFpbCI6InRoNDAxOUBnbWFpbC5jb20iLCJfX3YiOjAsImxpc3RpbmdzIjpbXSwiaWF0IjoxNDU2NDMwMjE5LCJleHAiOjE0NTY1MTY2MTl9._ijngdgwLU9AJnAjbySUgEFsR8hJCSw8PhH1AnyBHuM"
|
|
13
|
-
}
|
|
14
|
-
```
|
|
15
|
-
Or it will respond with a status code of 401 and a message explaining why they can't login.
|
|
16
|
-
|
|
17
|
-
```javascript
|
|
18
|
-
{
|
|
19
|
-
"success": false,
|
|
20
|
-
"message": "Authentication failed. Wrong password."
|
|
21
|
-
}
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
The returned token must then be passed in as a header on future requests using the header x-access-token
|
|
25
|
-
|
|
26
|
-
* `GET /users/me` - returns the logged in user's object
|
|
27
|
-
|
|
28
|
-
## Making authenticated requests
|
|
29
|
-
|
|
30
|
-
After obtaining a token as described above you need to pass it with your request in one of these ways
|
|
31
|
-
|
|
32
|
-
1. Using the `x-access-token`
|
|
33
|
-
|
|
34
|
-
`curl --header "x-access-token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJfaWQiOiI1NmNkNWM5NmY4MjA4N2I1MDQ0OTM3YjEiLCJ1bml2ZXJzaXR5IjoiQllVIiwiZnVsbE5hbWUiOiJUaG9tYXMgSGFuc2VuIiwicGFzc3dvcmQiOiIkMmEkMTAkb0prdlBnTTlkR2FJRTIzaWFabGEvT0tjZC9PL3phSGFJOHFRUDBuZ2pPUVV1Ums3Vng2QkciLCJlbWFpbCI6InRoNDAxOUBnbWFpbC5jb20iLCJfX3YiOjAsImxpc3RpbmdzIjpbXSwiaWF0IjoxNDU2NDMwMjE5LCJleHAiOjE0NTY1MTY2MTl9._ijngdgwLU9AJnAjbySUgEFsR8hJCSw8PhH1AnyBHuM" api.example.com/users/me`
|
|
35
|
-
|
|
36
|
-
2. Pass the token as a query parameter.
|
|
37
|
-
|
|
38
|
-
`curl api.example.com/users/me?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJfaWQiOiI1NmNkNWM5NmY4MjA4N2I1MDQ0OTM3YjEiLCJ1bml2ZXJzaXR5IjoiQllVIiwiZnVsbE5hbWUiOiJUaG9tYXMgSGFuc2VuIiwicGFzc3dvcmQiOiIkMmEkMTAkb0prdlBnTTlkR2FJRTIzaWFabGEvT0tjZC9PL3phSGFJOHFRUDBuZ2pPUVV1Ums3Vng2QkciLCJlbWFpbCI6InRoNDAxOUBnbWFpbC5jb20iLCJfX3YiOjAsImxpc3RpbmdzIjpbXSwiaWF0IjoxNDU2NDMwMjE5LCJleHAiOjE0NTY1MTY2MTl9._ijngdgwLU9AJnAjbySUgEFsR8hJCSw8PhH1AnyBHuM`
|
|
1
|
+
## Authentication
|
|
2
|
+
Uses [JSON Web Tokens](https://jwt.io/)
|
|
3
|
+
|
|
4
|
+
| method | endpoint | description |
|
|
5
|
+
|--------|-------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
6
|
+
| POST | /users/login | expects JSON in the message body. e.g. `{"email": "email@example.com", password: "<the password>"} |
|
|
7
|
+
|
|
8
|
+
Returns the following
|
|
9
|
+
```javascript
|
|
10
|
+
{
|
|
11
|
+
"uid": "a52ee085-ff47-4ee1-bac3-a8a319f674ed",
|
|
12
|
+
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJfaWQiOiI1NmNkNWM5NmY4MjA4N2I1MDQ0OTM3YjEiLCJ1bml2ZXJzaXR5IjoiQllVIiwiZnVsbE5hbWUiOiJUaG9tYXMgSGFuc2VuIiwicGFzc3dvcmQiOiIkMmEkMTAkb0prdlBnTTlkR2FJRTIzaWFabGEvT0tjZC9PL3phSGFJOHFRUDBuZ2pPUVV1Ums3Vng2QkciLCJlbWFpbCI6InRoNDAxOUBnbWFpbC5jb20iLCJfX3YiOjAsImxpc3RpbmdzIjpbXSwiaWF0IjoxNDU2NDMwMjE5LCJleHAiOjE0NTY1MTY2MTl9._ijngdgwLU9AJnAjbySUgEFsR8hJCSw8PhH1AnyBHuM"
|
|
13
|
+
}
|
|
14
|
+
```
|
|
15
|
+
Or it will respond with a status code of 401 and a message explaining why they can't login.
|
|
16
|
+
|
|
17
|
+
```javascript
|
|
18
|
+
{
|
|
19
|
+
"success": false,
|
|
20
|
+
"message": "Authentication failed. Wrong password."
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The returned token must then be passed in as a header on future requests using the header x-access-token
|
|
25
|
+
|
|
26
|
+
* `GET /users/me` - returns the logged in user's object
|
|
27
|
+
|
|
28
|
+
## Making authenticated requests
|
|
29
|
+
|
|
30
|
+
After obtaining a token as described above you need to pass it with your request in one of these ways
|
|
31
|
+
|
|
32
|
+
1. Using the `x-access-token`
|
|
33
|
+
|
|
34
|
+
`curl --header "x-access-token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJfaWQiOiI1NmNkNWM5NmY4MjA4N2I1MDQ0OTM3YjEiLCJ1bml2ZXJzaXR5IjoiQllVIiwiZnVsbE5hbWUiOiJUaG9tYXMgSGFuc2VuIiwicGFzc3dvcmQiOiIkMmEkMTAkb0prdlBnTTlkR2FJRTIzaWFabGEvT0tjZC9PL3phSGFJOHFRUDBuZ2pPUVV1Ums3Vng2QkciLCJlbWFpbCI6InRoNDAxOUBnbWFpbC5jb20iLCJfX3YiOjAsImxpc3RpbmdzIjpbXSwiaWF0IjoxNDU2NDMwMjE5LCJleHAiOjE0NTY1MTY2MTl9._ijngdgwLU9AJnAjbySUgEFsR8hJCSw8PhH1AnyBHuM" api.example.com/users/me`
|
|
35
|
+
|
|
36
|
+
2. Pass the token as a query parameter.
|
|
37
|
+
|
|
38
|
+
`curl api.example.com/users/me?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJfaWQiOiI1NmNkNWM5NmY4MjA4N2I1MDQ0OTM3YjEiLCJ1bml2ZXJzaXR5IjoiQllVIiwiZnVsbE5hbWUiOiJUaG9tYXMgSGFuc2VuIiwicGFzc3dvcmQiOiIkMmEkMTAkb0prdlBnTTlkR2FJRTIzaWFabGEvT0tjZC9PL3phSGFJOHFRUDBuZ2pPUVV1Ums3Vng2QkciLCJlbWFpbCI6InRoNDAxOUBnbWFpbC5jb20iLCJfX3YiOjAsImxpc3RpbmdzIjpbXSwiaWF0IjoxNDU2NDMwMjE5LCJleHAiOjE0NTY1MTY2MTl9._ijngdgwLU9AJnAjbySUgEFsR8hJCSw8PhH1AnyBHuM`
|
package/doc/automatic-fields.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
## Automatic Fields
|
|
2
|
-
|
|
3
|
-
Each object will contain this meta property which has autogenerated fields shared across all collection types:
|
|
4
|
-
|
|
5
|
-
```javascript
|
|
6
|
-
"meta": {
|
|
7
|
-
"created": "2016-05-16T23:56:11.615Z",
|
|
8
|
-
"updated": "2016-05-16T23:56:28.262Z",
|
|
9
|
-
"owner": "56cb5df7f56ef0b92f7b984b"
|
|
10
|
-
},
|
|
11
|
-
```
|
|
12
|
-
|
|
1
|
+
## Automatic Fields
|
|
2
|
+
|
|
3
|
+
Each object will contain this meta property which has autogenerated fields shared across all collection types:
|
|
4
|
+
|
|
5
|
+
```javascript
|
|
6
|
+
"meta": {
|
|
7
|
+
"created": "2016-05-16T23:56:11.615Z",
|
|
8
|
+
"updated": "2016-05-16T23:56:28.262Z",
|
|
9
|
+
"owner": "56cb5df7f56ef0b92f7b984b"
|
|
10
|
+
},
|
|
11
|
+
```
|
|
12
|
+
|
package/doc/blogexample.md
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
## Blog Example
|
|
2
|
-
|
|
3
|
-
### Creating the blog "post" collection and its schema.
|
|
4
|
-
|
|
5
|
-
Here's what the UI looks like for adding this new custom collection type.
|
|
6
|
-
|
|
7
|
-

|
|
8
|
-
|
|
9
|
-
--------------------------------
|
|
10
|
-
|
|
11
|
-
### Creating a post
|
|
12
|
-
|
|
13
|
-
After creating the blog post collection, here's what adding a document of that type looks like using the built in editor.
|
|
14
|
-
|
|
15
|
-

|
|
16
|
-
|
|
1
|
+
## Blog Example
|
|
2
|
+
|
|
3
|
+
### Creating the blog "post" collection and its schema.
|
|
4
|
+
|
|
5
|
+
Here's what the UI looks like for adding this new custom collection type.
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
--------------------------------
|
|
10
|
+
|
|
11
|
+
### Creating a post
|
|
12
|
+
|
|
13
|
+
After creating the blog post collection, here's what adding a document of that type looks like using the built in editor.
|
|
14
|
+
|
|
15
|
+

|
|
16
|
+
|
package/doc/custom-endpoints.md
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
## Custom Endpoints
|
|
2
|
-
|
|
3
|
-
Since Expressa exports a middleware you can include it in express apps that have other endpoints. These other endpoints
|
|
4
|
-
do not have access to the current user and other expressa objects attached to the request object. If you need these see
|
|
5
|
-
the section below.
|
|
6
|
-
|
|
7
|
-
## Add endpoints within expressa
|
|
8
|
-
|
|
9
|
-
The api middleware exposes a "custom" which is an express router you can attach additional endpoints to. These endpoints
|
|
10
|
-
are called after internal expressa middleware, but before any expressa endpoints.
|
|
11
|
-
|
|
12
|
-
Note: you can use req.hasPermission('users: edit') to check if the user has a specific permission.
|
|
13
|
-
|
|
14
|
-
Example:
|
|
15
|
-
```javascript
|
|
16
|
-
const api = expressa.api();
|
|
17
|
-
api.custom.get('/test', function (req, res) {
|
|
18
|
-
res.send(req.user)
|
|
19
|
-
})
|
|
20
|
-
```
|
|
1
|
+
## Custom Endpoints
|
|
2
|
+
|
|
3
|
+
Since Expressa exports a middleware you can include it in express apps that have other endpoints. These other endpoints
|
|
4
|
+
do not have access to the current user and other expressa objects attached to the request object. If you need these see
|
|
5
|
+
the section below.
|
|
6
|
+
|
|
7
|
+
## Add endpoints within expressa
|
|
8
|
+
|
|
9
|
+
The api middleware exposes a "custom" which is an express router you can attach additional endpoints to. These endpoints
|
|
10
|
+
are called after internal expressa middleware, but before any expressa endpoints.
|
|
11
|
+
|
|
12
|
+
Note: you can use req.hasPermission('users: edit') to check if the user has a specific permission.
|
|
13
|
+
|
|
14
|
+
Example:
|
|
15
|
+
```javascript
|
|
16
|
+
const api = expressa.api();
|
|
17
|
+
api.custom.get('/test', function (req, res) {
|
|
18
|
+
res.send(req.user)
|
|
19
|
+
})
|
|
20
|
+
```
|
package/doc/database.md
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
### Generic JSON Database API
|
|
2
|
-
|
|
3
|
-
You can access a collection's database using `expressa.db[collectionName].action where action is one of the following:
|
|
4
|
-
|
|
5
|
-
* all - returns all documents
|
|
6
|
-
* find - returns array of documents matching the mongo query
|
|
7
|
-
* get - retrieve a document by id
|
|
8
|
-
* create - create a new document
|
|
9
|
-
* update - modify an existing document
|
|
10
|
-
* delete - delete a document by id
|
|
11
|
-
* init - called once during startup, useful to create/ensure collection exists
|
|
12
|
-
|
|
13
|
-
### Implemented JSON databases
|
|
14
|
-
* MongoDB
|
|
15
|
-
* PostgreSQL (using [jsonb](http://www.postgresql.org/docs/9.4/static/datatype-json.html) and [mongo-query-to-postgres-jsonb](https://github.com/thomas4019/mongo-query-to-postgres-jsonb))
|
|
16
|
-
* Text files (using [json-file-store](https://github.com/flosse/json-file-store) and [mongo-query](https://github.com/Automattic/mongo-query))
|
|
17
|
-
* In-memory (useful for small/medium ephemeral datasets like logs)
|
|
18
|
-
|
|
19
|
-
Other JSON capable databases can be added easily (pull requests welcome!) by writing a wrapper that supports each of the above methods.
|
|
20
|
-
|
|
21
|
-
### Smart database objects
|
|
22
|
-
|
|
23
|
-
[expressa-folder](https://npmjs.org/package/expressa-folder) will automatically add functions to objects returned by `all`,`find` and `get`
|
|
1
|
+
### Generic JSON Database API
|
|
2
|
+
|
|
3
|
+
You can access a collection's database using `expressa.db[collectionName].action where action is one of the following:
|
|
4
|
+
|
|
5
|
+
* all - returns all documents
|
|
6
|
+
* find - returns array of documents matching the mongo query
|
|
7
|
+
* get - retrieve a document by id
|
|
8
|
+
* create - create a new document
|
|
9
|
+
* update - modify an existing document
|
|
10
|
+
* delete - delete a document by id
|
|
11
|
+
* init - called once during startup, useful to create/ensure collection exists
|
|
12
|
+
|
|
13
|
+
### Implemented JSON databases
|
|
14
|
+
* MongoDB
|
|
15
|
+
* PostgreSQL (using [jsonb](http://www.postgresql.org/docs/9.4/static/datatype-json.html) and [mongo-query-to-postgres-jsonb](https://github.com/thomas4019/mongo-query-to-postgres-jsonb))
|
|
16
|
+
* Text files (using [json-file-store](https://github.com/flosse/json-file-store) and [mongo-query](https://github.com/Automattic/mongo-query))
|
|
17
|
+
* In-memory (useful for small/medium ephemeral datasets like logs)
|
|
18
|
+
|
|
19
|
+
Other JSON capable databases can be added easily (pull requests welcome!) by writing a wrapper that supports each of the above methods.
|
|
20
|
+
|
|
21
|
+
### Smart database objects
|
|
22
|
+
|
|
23
|
+
[expressa-folder](https://npmjs.org/package/expressa-folder) will automatically add functions to objects returned by `all`,`find` and `get`
|
package/doc/development.md
CHANGED
|
@@ -1,34 +1,34 @@
|
|
|
1
|
-
## Setup for developing expressa
|
|
2
|
-
1. git clone git@github.com:thomas4019/expressa.git
|
|
3
|
-
1. cd expressa
|
|
4
|
-
1. npm install
|
|
5
|
-
1. npm link
|
|
6
|
-
1. cd modules/admin
|
|
7
|
-
1. npm install
|
|
8
|
-
1. npm run build
|
|
9
|
-
|
|
10
|
-
In another folder, create a project using app.js from the readme.
|
|
11
|
-
1. npm link expressa
|
|
12
|
-
1. node app.js
|
|
13
|
-
|
|
14
|
-
## Developing admin UI locally
|
|
15
|
-
* npm run testserver
|
|
16
|
-
* (cd modules/admin && npm run dev)
|
|
17
|
-
* npm run cypress (to manually run specific tests)
|
|
18
|
-
|
|
19
|
-
## Before committing/releasing
|
|
20
|
-
* npm run test
|
|
21
|
-
* npm run testdbs (if changing Postgres or Mongo code)
|
|
22
|
-
* npm run testcypress (if changing admin UI or related)
|
|
23
|
-
* npm run fixlint
|
|
24
|
-
* (cd modules/admin && npm run lint) (if changing admin UI)
|
|
25
|
-
|
|
26
|
-
## To release
|
|
27
|
-
* Update version in package.json
|
|
28
|
-
* (cd modules/admin && npm run build)
|
|
29
|
-
* ls -la modules/admin/dist/static/js
|
|
30
|
-
* npm publish
|
|
31
|
-
* Create release on GitHub
|
|
32
|
-
|
|
33
|
-
## More info
|
|
1
|
+
## Setup for developing expressa
|
|
2
|
+
1. git clone git@github.com:thomas4019/expressa.git
|
|
3
|
+
1. cd expressa
|
|
4
|
+
1. npm install
|
|
5
|
+
1. npm link
|
|
6
|
+
1. cd modules/admin
|
|
7
|
+
1. npm install
|
|
8
|
+
1. npm run build
|
|
9
|
+
|
|
10
|
+
In another folder, create a project using app.js from the readme.
|
|
11
|
+
1. npm link expressa
|
|
12
|
+
1. node app.js
|
|
13
|
+
|
|
14
|
+
## Developing admin UI locally
|
|
15
|
+
* npm run testserver
|
|
16
|
+
* (cd modules/admin && npm run dev)
|
|
17
|
+
* npm run cypress (to manually run specific tests)
|
|
18
|
+
|
|
19
|
+
## Before committing/releasing
|
|
20
|
+
* npm run test
|
|
21
|
+
* npm run testdbs (if changing Postgres or Mongo code)
|
|
22
|
+
* npm run testcypress (if changing admin UI or related)
|
|
23
|
+
* npm run fixlint
|
|
24
|
+
* (cd modules/admin && npm run lint) (if changing admin UI)
|
|
25
|
+
|
|
26
|
+
## To release
|
|
27
|
+
* Update version in package.json
|
|
28
|
+
* (cd modules/admin && npm run build)
|
|
29
|
+
* ls -la modules/admin/dist/static/js
|
|
30
|
+
* npm publish
|
|
31
|
+
* Create release on GitHub
|
|
32
|
+
|
|
33
|
+
## More info
|
|
34
34
|
[Modules](doc/modules.md)
|