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.
@@ -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 || util.normalizeOrderBy({ 'meta.created': 1 }) // paging requires orderby to ensure consistent results with offset and limit
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 mongoToPostgres = require('mongo-query-to-postgres-jsonb')
2
- const util = require('../util')
3
-
4
- module.exports = function (settings, collectionId, collection) {
5
- const pool = util.getPgPool(settings.postgresql_uri)
6
-
7
- return {
8
- init: async function () {
9
- if (collection.plainStringIds) {
10
- await pool.query('CREATE TABLE IF NOT EXISTS ' + collectionId + ' (id text primary key, data jsonb)')
11
- } else {
12
- await pool.query('CREATE TABLE IF NOT EXISTS ' + collectionId + ' (id uuid primary key, data jsonb)')
13
- }
14
- },
15
- all: async function () {
16
- return this.find({})
17
- },
18
- find: async function (rawQuery, offset, limit, orderby, fields) {
19
- const arrayFields = util.getArrayPaths('', collection.schema)
20
- const pgQuery = mongoToPostgres('data', rawQuery || {}, arrayFields)
21
- const select = fields ? mongoToPostgres.convertSelect('data', fields, arrayFields) : '*'
22
- let query = 'SELECT ' + select + ' FROM ' + collectionId + (pgQuery ? ' WHERE ' + pgQuery : '')
23
- if (typeof orderby !== 'undefined') {
24
- query += ' ORDER BY '
25
- query += orderby.map((ordering) => {
26
- return mongoToPostgres.convertDotNotation('data', ordering[0]) + (ordering[1] > 0 ? ' ASC' : ' DESC')
27
- }).join(', ')
28
- }
29
- if (typeof offset !== 'undefined') {
30
- query += ' OFFSET ' + offset
31
- }
32
- if (typeof limit !== 'undefined') {
33
- query += ' LIMIT ' + limit
34
- }
35
- const result = await pool.query(query)
36
- return result.rows.map((row) => row.data)
37
- },
38
- count: async function(rawQuery, offset, limit) {
39
- const arrayFields = util.getArrayPaths('', collection.schema)
40
- const pgQuery = mongoToPostgres('data', rawQuery || {}, arrayFields)
41
- let query = 'SELECT COUNT(*) FROM ' + collectionId + (pgQuery ? ' WHERE ' + pgQuery : '')
42
- if (typeof offset !== 'undefined') {
43
- query += ' OFFSET ' + offset
44
- }
45
- if (typeof limit !== 'undefined') {
46
- query += ' LIMIT ' + limit
47
- }
48
- const result = await pool.query(query)
49
- return parseInt(result.rows[0].count)
50
- },
51
- get: async function (id, fields) {
52
- const arrayFields = util.getArrayPaths('', collection.schema)
53
- const select = fields ? mongoToPostgres.convertSelect('data', fields, arrayFields) : '*'
54
- const result = await pool.query(`SELECT ${select} FROM ${collectionId} WHERE id = $1`, [id])
55
- if (result.rowCount === 0) {
56
- throw new util.ApiError(404, 'document not found')
57
- }
58
- return result.rows[0].data
59
- },
60
- create: async function (data) {
61
- util.addIdIfMissing(data)
62
- try {
63
- await pool.query('INSERT INTO ' + collectionId + ' (id, data) VALUES ($1, $2)', [data._id, data])
64
- } catch (e) {
65
- if (e.message.includes('duplicate key')) {
66
- throw new util.ApiError(409, 'document already exists')
67
- }
68
- throw e
69
- }
70
- return data._id
71
- },
72
- update: async function (id, data) {
73
- if (typeof data._id === 'undefined') {
74
- data._id = id
75
- }
76
- const result = await pool.query('UPDATE ' + collectionId + ' SET data=$2,id=$3 WHERE id=$1', [id, data, data._id])
77
- if (result.rowCount === 0) {
78
- throw new util.ApiError(404, 'document not found')
79
- }
80
- return data
81
- },
82
- // eslint-disable-next-line no-unused-vars
83
- updateWithQuery: async function (query, update, options) {
84
- const arrayFields = util.getArrayPaths('', collection.schema)
85
- const pgQuery = mongoToPostgres('data', query || {}, arrayFields)
86
- const updateSql = mongoToPostgres.convertUpdate('data', update,false)
87
- const result = await pool.query('UPDATE ' + collectionId + ' SET data=' + updateSql + ' WHERE ' + pgQuery)
88
- return {
89
- matchedCount: result.rowCount
90
- }
91
- },
92
- delete: async function (id) {
93
- const result = await pool.query('DELETE FROM ' + collectionId + ' WHERE id=$1', [id])
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
+ }
@@ -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.addSchemaValidator(collection._id, schema)
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.addSchemaValidator(data._id, schema)
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": "5.2.1",
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.2",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expressa",
3
- "version": "2.0.7",
3
+ "version": "2.0.9",
4
4
  "description": "API framework using JSON schemas",
5
5
  "main": "index.js",
6
6
  "repository": "https://github.com/thomas4019/expressa",
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.addSchemaValidator = function(collection, schema) {
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') {