expressa 2.0.7 → 2.0.9
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/controllers/collections.js +1 -1
- package/db/postgres.js +93 -100
- package/listeners_validation.js +2 -2
- package/modules/admin/package.json +2 -2
- package/package.json +1 -1
- package/util.js +38 -2
|
@@ -101,7 +101,7 @@ exports.get = async function (req) {
|
|
|
101
101
|
}
|
|
102
102
|
req.query.pageitems = parseInt(req.query.pageitems) || parseInt(req.query.limit) || 10
|
|
103
103
|
req.query.offset = util.getDatabaseOffset(req.query.page, req.query.pageitems)
|
|
104
|
-
req.query.orderby = req.query.orderby
|
|
104
|
+
req.query.orderby = util.orderByForPagedRequests(req.query.orderby)
|
|
105
105
|
delete req.query.limit
|
|
106
106
|
delete req.query.skip
|
|
107
107
|
let pageData, totalItems
|
package/db/postgres.js
CHANGED
|
@@ -1,100 +1,93 @@
|
|
|
1
|
-
const
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const result = await pool.query(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
if (result.rowCount === 0) {
|
|
95
|
-
throw new util.ApiError(404, 'document not found')
|
|
96
|
-
}
|
|
97
|
-
return 'OK'
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
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/listeners_validation.js
CHANGED
|
@@ -18,13 +18,13 @@ module.exports = async function (api) {
|
|
|
18
18
|
const collections = await api.db.collection.all()
|
|
19
19
|
collections.forEach((collection) => {
|
|
20
20
|
const schema = addImplicitFields(collection.schema)
|
|
21
|
-
util.
|
|
21
|
+
util.addSchema(collection._id, schema)
|
|
22
22
|
})
|
|
23
23
|
|
|
24
24
|
// Load new and update validators as necessary
|
|
25
25
|
api.addCollectionListener('changed', 'collection', function updateSchemaValidators (req, collection, data) {
|
|
26
26
|
const schema = addImplicitFields(data.schema)
|
|
27
|
-
util.
|
|
27
|
+
util.addSchema(data._id, schema)
|
|
28
28
|
})
|
|
29
29
|
|
|
30
30
|
api.addCollectionListener('get', ['collection', 'schemas'], function ensureIdAdded (req, collection, data) {
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"friendly-errors-webpack-plugin": "1.7.0",
|
|
60
60
|
"html-webpack-plugin": "^4.0.0",
|
|
61
61
|
"mini-css-extract-plugin": "0.4.1",
|
|
62
|
-
"node-notifier": "
|
|
62
|
+
"node-notifier": "8.0.1",
|
|
63
63
|
"node-sass": "^6.0.1",
|
|
64
64
|
"optimize-css-assets-webpack-plugin": "5.0.0",
|
|
65
65
|
"ora": "3.0.0",
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
"sass-loader": "7.0.3",
|
|
73
73
|
"script-ext-html-webpack-plugin": "^2.1.5",
|
|
74
74
|
"semver": "5.5.0",
|
|
75
|
-
"shelljs": "0.8.
|
|
75
|
+
"shelljs": "0.8.5",
|
|
76
76
|
"svg-sprite-loader": "3.8.0",
|
|
77
77
|
"svgo": "^1.3.2",
|
|
78
78
|
"terser-webpack-plugin": "^2.2.3",
|
package/package.json
CHANGED
package/util.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
const randomstring = require('randomstring')
|
|
2
1
|
const {v4} = require('uuid')
|
|
3
2
|
const debug = require('debug')('expressa')
|
|
4
3
|
const crypto = require('crypto')
|
|
@@ -6,6 +5,7 @@ const pg = require('pg')
|
|
|
6
5
|
const pgPools = {}
|
|
7
6
|
const dot = require('dot-object')
|
|
8
7
|
const mongoQuery = require('mongo-query')
|
|
8
|
+
const mongoToPostgres = require('mongo-query-to-postgres-jsonb')
|
|
9
9
|
const sift = require('sift')
|
|
10
10
|
const auth = require('./auth/index')
|
|
11
11
|
const Ajv = require('ajv')
|
|
@@ -33,9 +33,11 @@ ajv.addKeyword({
|
|
|
33
33
|
const formatKey = ajv.getKeyword('format')
|
|
34
34
|
formatKey.type = formatKey.type.concat(['array', 'boolean', 'object'])
|
|
35
35
|
|
|
36
|
+
const schemas = {}
|
|
36
37
|
const schemaValidators = {}
|
|
37
38
|
|
|
38
|
-
exports.
|
|
39
|
+
exports.addSchema = function(collection, schema) {
|
|
40
|
+
schemas[collection] = schema
|
|
39
41
|
schemaValidators[collection] = ajv.compile(schema)
|
|
40
42
|
}
|
|
41
43
|
|
|
@@ -141,6 +143,27 @@ function _exclude(obj, source) {
|
|
|
141
143
|
return data
|
|
142
144
|
}
|
|
143
145
|
|
|
146
|
+
exports.mongoToPostgresSelect = function(collection, fields) {
|
|
147
|
+
const arrayFields = exports.getArrayPaths('', schemas[collection])
|
|
148
|
+
return fields ? mongoToPostgres.convertSelect('data', fields, arrayFields) : '*'
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
exports.mongoToPostgresUpdate = function(collection, query) {
|
|
152
|
+
return mongoToPostgres.convertUpdate('data', query,false)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
exports.mongoToPostgresWhere = function(collection, query) {
|
|
156
|
+
const arrayFields = exports.getArrayPaths('', schemas[collection])
|
|
157
|
+
return mongoToPostgres('data', query || {}, arrayFields)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
exports.mongoToPostgresOrderBy = function(collection, orderby) {
|
|
161
|
+
const normalizedOrderBy = exports.normalizeOrderBy(orderby)
|
|
162
|
+
return normalizedOrderBy.map((ordering) => {
|
|
163
|
+
return mongoToPostgres.convertDotNotation('data', ordering[0]) + (ordering[1] > 0 ? ' ASC' : ' DESC')
|
|
164
|
+
}).join(', ')
|
|
165
|
+
}
|
|
166
|
+
|
|
144
167
|
exports.mongoProject = function(record, projection) {
|
|
145
168
|
if (!projection || Object.keys(projection).length < 1) {
|
|
146
169
|
return record
|
|
@@ -162,6 +185,18 @@ exports.mongoUpdate = function(doc, update) {
|
|
|
162
185
|
return mongoQuery(doc, {}, update)
|
|
163
186
|
}
|
|
164
187
|
|
|
188
|
+
// paging requires orderby to include a field that is known to be unique and constant
|
|
189
|
+
// to ensure consistent results from database with offset and limit. Chosen field should
|
|
190
|
+
// also represent a preferred ordering when doing final sort
|
|
191
|
+
exports.orderByForPagedRequests = function(orderby, finalSortField = 'meta.created') {
|
|
192
|
+
orderby = exports.normalizeOrderBy(orderby || {})
|
|
193
|
+
const allFields = orderby.map(([field]) => field)
|
|
194
|
+
if (!allFields.includes(finalSortField)) {
|
|
195
|
+
orderby.push([finalSortField, 1])
|
|
196
|
+
}
|
|
197
|
+
return orderby
|
|
198
|
+
}
|
|
199
|
+
|
|
165
200
|
exports.normalizeOrderBy = function(orderby) {
|
|
166
201
|
if (Array.isArray(orderby)) {
|
|
167
202
|
orderby = orderby.map(function (ordering) {
|
|
@@ -172,6 +207,7 @@ exports.normalizeOrderBy = function(orderby) {
|
|
|
172
207
|
return [ordering[0], 1] // add 1 (default to ascending sort)
|
|
173
208
|
}
|
|
174
209
|
}
|
|
210
|
+
// if reaches here, is already normalized
|
|
175
211
|
return ordering
|
|
176
212
|
})
|
|
177
213
|
} else if (typeof orderby === 'object') {
|