doix-db 1.0.18 → 1.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/README.md CHANGED
@@ -2,4 +2,11 @@
2
2
  ![Jest coverage](./badges/coverage-jest%20coverage.svg)
3
3
 
4
4
  # node-doix-db
5
- Shared database related code for doix
5
+ `doix-db` is a plug in for [doix](https://github.com/do-/node-doix) framework implementing a common interface to relational databases. It features:
6
+ * [DbClient](DbClient) — the database API available to `doix` [Job](https://github.com/do-/node-doix/wiki/Job)s;
7
+ * [DbModel](DbModel) — the set of classes representing the database structure;
8
+ * [DbQuery](DbQuery) — a `DbModel` based `SELECT` builder;
9
+ * [DbMigrationPlan](DbMigrationPlan) — a diff/patch tool to compare existing database structure with the required `DbModel`;
10
+ * [DbLang](DbLang) — a set of SQL generating functions for miscellaneous application tasks.
11
+
12
+ * More information is available at https://github.com/do-/node-doix-db/wiki
package/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  module.exports = {
2
2
 
3
- DbEventLogger : require ('./lib/DbEventLogger.js'),
4
- DbClient: require ('./lib/DbClient.js'),
3
+ DbCallTracker : require ('./lib/DbCallTracker.js'),
4
+ DbClient: require ('./lib/DbClient.js'),
5
5
  DbPool: require ('./lib/DbPool.js'),
6
6
  DbLang: require ('./lib/DbLang.js'),
7
7
  DbColumn: require ('./lib/model/DbColumn.js'),
package/lib/DbCall.js ADDED
@@ -0,0 +1,255 @@
1
+ const EV_FINISH = 'finish'
2
+ const EV_ERROR = 'error'
3
+ const EV_END_STREAM = [
4
+ 'end',
5
+ 'close',
6
+ ]
7
+
8
+ const OPT_REQUIRING_MAX_ROW = [
9
+ 'rowMode',
10
+ 'checkOverflow',
11
+ 'minRows',
12
+ ]
13
+
14
+ const NULL = Symbol.for ('NULL'), mayBeNull = v => v === NULL ? null : v
15
+
16
+ const MD_ARRAY = 'array'
17
+ const MD_OBJECT = 'object'
18
+ const MD_SCALAR = 'scalar'
19
+
20
+ const EventEmitter = require ('events')
21
+ const {Transform} = require ('stream')
22
+
23
+ class DbCall extends EventEmitter {
24
+
25
+ constructor (db, sql, params = [], options = {}) {
26
+
27
+ if (!db) throw Error ('DbCall: db must be defined')
28
+
29
+ super ()
30
+
31
+ this.ord = ++ db.count
32
+
33
+ if (typeof sql !== 'string') throw Error ('DbCall: sql must be a string, found: ' + sql)
34
+
35
+ if (!('maxRows' in options)) options.maxRows = 0
36
+
37
+ if (typeof options.maxRows !== 'number') throw Error ('DbCall: maxRows must be a number, found: ' + options.maxRows)
38
+
39
+ if (options.maxRows < 0) throw Error ('DbCall: maxRows cannot be negative, found: ' + options.maxRows)
40
+
41
+ if (options.maxRows > 0) {
42
+
43
+ if (!('rowMode' in options)) options.rowMode = MD_OBJECT
44
+
45
+ switch (options.rowMode) {
46
+
47
+ case MD_ARRAY:
48
+ case MD_OBJECT:
49
+ case MD_SCALAR:
50
+ break
51
+
52
+ default:
53
+ throw Error ('DbCall: rowMode must be one of (scalar|array|object) found' + options.rowMode)
54
+
55
+ }
56
+
57
+ if (!('checkOverflow' in options)) options.checkOverflow = false
58
+
59
+ if (typeof options.checkOverflow !== 'boolean') throw Error ('DbCall: checkOverflow must be a boolean, found: ' + options.maxRows)
60
+
61
+ if (options.maxRows === Infinity) {
62
+
63
+ if ('minRows' in options) throw Error ('DbCall: minRows requires a finite maxRows')
64
+
65
+ }
66
+ else {
67
+
68
+ if (!('minRows' in options)) options.minRows = 0
69
+
70
+ if (typeof options.minRows !== 'number') throw Error ('DbCall: minRows must be a number, found: ' + options.minRows)
71
+
72
+ if (options.minRows < 0) throw Error ('DbCall: minRows cannot be negative, found: ' + options.minRows)
73
+
74
+ if (options.minRows > options.maxRows) throw Error ('DbCall: minRows cannot exceed maxRows=' + options.maxRows)
75
+
76
+ }
77
+
78
+ }
79
+ else {
80
+
81
+ for (const k of OPT_REQUIRING_MAX_ROW) if (k in options) throw Error (`DbCall: ${k} cannot be set with maxRows=${options.maxRows}`)
82
+
83
+ }
84
+
85
+ if ('notFound' in options && !(options.minRows > 0)) throw Error ('DbCall: notFound requires a positive minRows')
86
+
87
+ this.db = db
88
+ this.sql = sql
89
+ this.params = params
90
+ this.options = options
91
+
92
+ }
93
+
94
+ get objectMode () {
95
+
96
+ return this.options.rowMode === MD_OBJECT
97
+
98
+ }
99
+
100
+ get result () {
101
+
102
+ const {rows, options} = this, {minRows, maxRows} = options
103
+
104
+ if (maxRows === 0) return
105
+
106
+ if (minRows > rows.length) {
107
+
108
+ if ('notFound' in options) return options.notFound
109
+
110
+ throw Error (`Missing data: ${rows.length} found, ${minRows} required`)
111
+
112
+ }
113
+
114
+ return maxRows === 1 ? rows [0] : rows
115
+
116
+ }
117
+
118
+ finish () {
119
+
120
+ this.emit (EV_FINISH)
121
+
122
+ this.removeAllListeners (EV_FINISH)
123
+
124
+ }
125
+
126
+ flattenArray () {
127
+
128
+ const {rows} = this
129
+
130
+ for (let i = 0; i < rows.length; i ++) rows [i] = rows [i] [0]
131
+
132
+ }
133
+
134
+ flattenStream () {
135
+
136
+ const {rows} = this
137
+
138
+ const xform = new Transform ({objectMode: true,
139
+ transform (r, __, cb) {
140
+ const v = r [0]
141
+ cb (null, v == null ? NULL : v)
142
+ }
143
+ })
144
+
145
+ rows.once ('error', e => xform.destroy (e))
146
+
147
+ this.rows = this.rows.pipe (xform)
148
+
149
+ }
150
+
151
+ observeStream () {
152
+
153
+ const {rows} = this
154
+
155
+ rows.on (EV_ERROR, e => this.emit (EV_ERROR, e))
156
+
157
+ const finish = () => this.finish ()
158
+
159
+ for (const event of EV_END_STREAM) rows.once (event, finish)
160
+
161
+ }
162
+
163
+ processArray () {
164
+
165
+ if (this.options.rowMode === MD_SCALAR) this.flattenArray ()
166
+
167
+ this.finish ()
168
+
169
+ return this.result
170
+
171
+ }
172
+
173
+ processStream () {
174
+
175
+ const {rowMode} = this.options
176
+
177
+ if (rowMode === MD_SCALAR) this.flattenStream ()
178
+
179
+ this.observeStream ()
180
+
181
+ }
182
+
183
+ async fetchStream () {
184
+
185
+ const a = [], {rows, options: {maxRows, checkOverflow}} = this; await new Promise ((ok, fail) => {
186
+
187
+ rows.once ('error', fail)
188
+
189
+ let stop = false; rows.on ('end', () => ok (stop = true))
190
+
191
+ rows.on ('data',
192
+
193
+ checkOverflow ? r => {
194
+ if (a.length === maxRows) return rows.destroy (Error ('Result set overflow, maxRows = ' + maxRows))
195
+ a.push (mayBeNull (r))
196
+ }
197
+
198
+ : r => {
199
+ if (stop) return
200
+ a.push (mayBeNull (r))
201
+ if (a.length === maxRows) rows.emit ('end')
202
+ }
203
+
204
+ )
205
+
206
+ }).finally (() => rows.destroy ())
207
+
208
+ this.rows = a
209
+
210
+ }
211
+
212
+ async exec () {
213
+
214
+ const {db} = this
215
+
216
+ db.lang.normalizeSQL (this)
217
+
218
+ this.once ('error', () => this.finish ())
219
+
220
+ try {
221
+
222
+ const {maxRows} = this.options
223
+
224
+ this.emit ('start')
225
+
226
+ await db.exec (this)
227
+
228
+ this.emit ('result')
229
+
230
+ if (maxRows === 0) return
231
+
232
+ if (Array.isArray (this.rows)) return this.processArray ()
233
+
234
+ this.processStream ()
235
+
236
+ if (maxRows === Infinity) return this.result
237
+
238
+ await this.fetchStream ()
239
+
240
+ return this.result
241
+
242
+ }
243
+ catch (error) {
244
+
245
+ this.emit ('error', this.error = error)
246
+
247
+ throw error
248
+
249
+ }
250
+
251
+ }
252
+
253
+ }
254
+
255
+ module.exports = DbCall
@@ -0,0 +1,70 @@
1
+ const {LifeCycleTracker} = require ('doix')
2
+ const stringEscape = require ('string-escape-map')
3
+
4
+ const ESC_PARAMS = new stringEscape ([
5
+ ['\t', '\\t'],
6
+ ['\n', '\\n'],
7
+ ['\r', '\\r'],
8
+ [ "'", "''"],
9
+ ])
10
+
11
+ const stringifyParams = params => {
12
+
13
+ if (!Array.isArray (params)) return JSON.stringify (params)
14
+
15
+ if (params.length === 0) return ''
16
+
17
+ let s = '['; for (const p of params) {
18
+
19
+ if (s.length !== 1) s += ', '
20
+
21
+ if (typeof p === 'string') {
22
+
23
+ s += "'"
24
+ s += ESC_PARAMS.escape (p)
25
+ s += "'"
26
+
27
+ }
28
+ else {
29
+
30
+ s += p
31
+
32
+ }
33
+
34
+ }
35
+
36
+ return s + ']'
37
+
38
+ }
39
+
40
+ class DbCallTracker extends LifeCycleTracker {
41
+
42
+ constructor (call) {
43
+
44
+ const {db} = call
45
+
46
+ super (call, db.pool.logger)
47
+
48
+ this.call = call
49
+
50
+ this.prefix = db.job.tracker.prefix + '/' + db.uuid + '/' + call.ord
51
+
52
+ }
53
+
54
+ startMessage () {
55
+
56
+ const {sql, params} = this.call
57
+
58
+ const s = super.startMessage () + ' ' + sql
59
+
60
+ if (params.length === 0) return s
61
+
62
+ return s + ' ' + stringifyParams (params)
63
+
64
+ }
65
+
66
+ }
67
+
68
+ DbCallTracker.stringifyParams = stringifyParams
69
+
70
+ module.exports = DbCallTracker
package/lib/DbClient.js CHANGED
@@ -1,16 +1,34 @@
1
1
  const EventEmitter = require ('events')
2
2
  const {randomUUID} = require ('crypto')
3
3
 
4
+ const DbCall = require ('./DbCall.js')
4
5
  const DbQuery = require ('./query/DbQuery.js')
5
6
  const DbMigrationPlan = require ('./migration/DbMigrationPlan.js')
6
7
 
7
- const NULL = Symbol.for ('NULL')
8
+ const PR_QUERY = Symbol.for ('query')
9
+ const PR_CALL = Symbol.for ('call')
10
+ const PR_COLUMNS = Symbol.for ('columns')
11
+ const PR_COUNT = Symbol.for ('count')
12
+
13
+ const set = (o, k, v) => Object.defineProperty (o, k, {
14
+ configurable: false,
15
+ enumerable: false,
16
+ get: () => v
17
+ })
18
+
19
+ const setAll = (rows, call, q) => {
20
+ set (rows, PR_CALL, call)
21
+ set (rows, PR_COLUMNS, call.columns)
22
+ if (q instanceof DbQuery) set (rows, PR_QUERY, q)
23
+ }
8
24
 
9
25
  class DbClient extends EventEmitter {
10
26
 
11
27
  constructor () {
12
28
 
13
29
  super ()
30
+
31
+ this.count = 0
14
32
 
15
33
  this.uuid = randomUUID ()
16
34
 
@@ -22,106 +40,122 @@ class DbClient extends EventEmitter {
22
40
 
23
41
  }
24
42
 
25
- async getArrayBySql (sql, params = [], options = {}) {
43
+ async getArrayOnly (q, params = [], o = {}) {
26
44
 
27
- const maxRows = options.maxRows || 1000
28
- const isPartial = options.isPartial === true
29
-
30
- const s = await this.getStream (sql, params, options)
31
- const rows = []
45
+ const options = {...o}
32
46
 
33
- Object.defineProperty (rows, Symbol.for ('columns'), {
34
- configurable: false,
35
- enumerable: false,
36
- get: () => s [Symbol.for ('columns')]
37
- })
38
-
39
- for await (const r of s) {
47
+ if (!('maxRows' in options)) {
48
+ options.maxRows = 1000
49
+ options.checkOverflow = true
50
+ }
40
51
 
41
- if (rows.length === maxRows && !isPartial) {
52
+ const call = this.call (q, params, options)
42
53
 
43
- const err = Error (maxRows + ' rows limit exceeded. Please fix the request or consider using getStream instead of getArray')
54
+ await call.exec ()
44
55
 
45
- s.destroy (err)
56
+ const {rows} = call
46
57
 
47
- throw err
58
+ setAll (rows, call, q)
48
59
 
49
- }
60
+ return rows
50
61
 
51
- rows.push (r === NULL ? null : r)
62
+ }
52
63
 
53
- if (rows.length === maxRows && isPartial) {
54
-
55
- s.destroy ()
64
+ async getArray (q, p, o) {
56
65
 
57
- break
58
-
59
- }
66
+ const arrayOnly = this.getArrayOnly (q, p, o)
60
67
 
61
- }
68
+ if (!(q instanceof DbQuery) || !('offset' in q.options)) return arrayOnly
62
69
 
70
+ const [rows, cnt] = await Promise.all ([
71
+ arrayOnly,
72
+ this.getScalar (q.toQueryCount ())
73
+ ])
74
+
75
+ set (rows, PR_COUNT, parseInt (cnt))
76
+
63
77
  return rows
64
78
 
65
79
  }
80
+
81
+ async getObject (sql, params = [], options = {}) {
66
82
 
67
- async getArray (q, p, o) {
83
+ const {model} = this; if (model) {
68
84
 
69
- if (!(q instanceof DbQuery)) return this.getArrayBySql (q, p, o)
70
-
71
- const addCount = 'offset' in q.options, todo = addCount ? [null, this.getScalar (q.toQueryCount ())] : []
72
-
73
- const params = this.lang.toParamsSql (q), sql = params.pop (); todo [0] = this.getArrayBySql (sql, params, o)
85
+ const relation = model.find (sql); if (relation) {
74
86
 
75
- const done = await Promise.all (todo), rows = done [0]
87
+ const {name, pk} = relation, filters = []; for (let i = 0; i < pk.length; i ++) filters.push ([pk [i], '=', params [i]])
76
88
 
77
- Object.defineProperty (rows, Symbol.for ('query'), {
78
- configurable: false,
79
- enumerable: false,
80
- get: () => q
81
- })
89
+ sql = model.createQuery ([[name, {filters}]])
82
90
 
83
- if (addCount) {
91
+ }
84
92
 
85
- const count = parseInt (done [1]), get = () => count
93
+ }
86
94
 
87
- Object.defineProperty (rows, Symbol.for ('count'), {
88
- configurable: false,
89
- enumerable: false,
90
- get
91
- })
95
+ return this.call (sql, params, {
96
+ ...options,
97
+ minRows: 1,
98
+ maxRows: 1,
99
+ }).exec ()
92
100
 
93
- }
101
+ }
102
+
103
+ async getScalar (sql, params = [], options = {}) {
104
+
105
+ return this.call (sql, params, {
106
+ notFound: undefined,
107
+ ...options,
108
+ rowMode: 'scalar',
109
+ minRows: 1,
110
+ maxRows: 1,
111
+ }).exec ()
94
112
 
95
- return rows
96
-
97
- }
113
+ }
98
114
 
99
- async getObject (sqlOrName, p = [], options = {}) {
100
-
101
- const params = this.lang.genSelectObjectParamsSql (sqlOrName, p), sql = params.pop ()
115
+ async do (sql, params = [], options = {}) {
102
116
 
103
- const a = await this.getArray (sql, params, {
117
+ const call = this.call (sql, params, {
104
118
  ...options,
105
- maxRows: 1,
106
- isPartial: true,
119
+ maxRows: 0,
107
120
  })
108
121
 
109
- if (a.length === 1) return a [0]
110
-
111
- const {notFound} = options; if (notFound instanceof Error) throw notFound
112
-
113
- return 'notFound' in options ? notFound : {}
122
+ await call.exec ()
123
+
124
+ return call
114
125
 
115
126
  }
116
127
 
117
- async getScalar (sql, params = [], options = {}) {
128
+ async getStream (q, p = [], options = {}) {
118
129
 
119
- return this.getObject (sql, params, {
120
- notFound: undefined,
130
+ const call = this.call (q, p, {
121
131
  ...options,
122
- rowMode: 'scalar',
132
+ maxRows: Infinity,
123
133
  })
124
134
 
135
+ const rows = await call.exec ()
136
+
137
+ setAll (rows, call, q)
138
+
139
+ return rows
140
+
141
+ }
142
+
143
+ call (q, params, options) {
144
+
145
+ if (q instanceof DbQuery) {
146
+
147
+ params = this.lang.toParamsSql (q)
148
+
149
+ q = params.pop ()
150
+
151
+ }
152
+
153
+ const call = new DbCall (this, q, params, options)
154
+
155
+ call.tracker = new this.pool.trackerClass (call)
156
+
157
+ return call
158
+
125
159
  }
126
160
 
127
161
  }
package/lib/DbLang.js CHANGED
@@ -263,37 +263,6 @@ class DbLang {
263
263
 
264
264
  }
265
265
 
266
- genSelectObjectParamsSql (sqlOrName, params) {
267
-
268
- if (!Array.isArray (params)) params = [params]
269
-
270
- const {model} = this
271
-
272
- const relation = model ? model.find (sqlOrName) : undefined; if (relation) {
273
-
274
- const {qName, pk, columns} = relation
275
-
276
- let filter = ''; for (const name of pk) {
277
-
278
- if (filter.length !== 0) filter += ' AND '
279
-
280
- filter += columns [name].qName + '=?'
281
-
282
- }
283
-
284
- params.push (`SELECT * FROM ${qName} WHERE ${filter}`)
285
-
286
- }
287
- else {
288
-
289
- params.push (sqlOrName)
290
-
291
- }
292
-
293
- return params
294
-
295
- }
296
-
297
266
  genInsertParamsSql (name, data) {
298
267
 
299
268
  const {model} = this; if (!model) throw Error ('Model not set')
@@ -510,6 +479,30 @@ class DbLang {
510
479
 
511
480
  }
512
481
 
482
+ normalizeSQL (call) {
483
+
484
+ call.sql = call.sql.trim ()
485
+
486
+ const {sql} = call, {length} = sql; if (length === 0) return
487
+
488
+ let r = '', from = 0
489
+
490
+ while (from < length) {
491
+
492
+ let to = from + 1; while (to < length && sql.charCodeAt (to) > 32) to ++
493
+
494
+ if (r.length !== 0) r += ' '
495
+
496
+ r += sql.slice (from, to)
497
+
498
+ from = to; while (from < length && sql.charCodeAt (from) <= 32) from ++
499
+
500
+ }
501
+
502
+ call.sql = r
503
+
504
+ }
505
+
513
506
  }
514
507
 
515
508
  module.exports = DbLang
package/lib/DbPool.js CHANGED
@@ -1,5 +1,5 @@
1
1
  const {ResourcePool} = require ('doix')
2
- const DbEventLogger = require ('./DbEventLogger.js')
2
+ const DbCallTracker = require ('./DbCallTracker.js')
3
3
 
4
4
  class DbPool extends ResourcePool {
5
5
 
@@ -12,7 +12,7 @@ class DbPool extends ResourcePool {
12
12
 
13
13
  this.logger = o.logger
14
14
 
15
- this.eventLoggerClass = o.eventLoggerClass || DbEventLogger
15
+ this.trackerClass = o.trackerClass || DbCallTracker
16
16
 
17
17
  }
18
18
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "doix-db",
3
- "version": "1.0.18",
3
+ "version": "1.0.20",
4
4
  "description": "Shared database related code for doix",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -40,7 +40,7 @@
40
40
  },
41
41
  "homepage": "https://github.com/do-/node-doix-db#readme",
42
42
  "peerDependencies": {
43
- "doix": "^1.0.3"
43
+ "doix": "^1.0.5"
44
44
  },
45
45
  "devDependencies": {
46
46
  "jest": "^29.6.1"
@@ -1,101 +0,0 @@
1
- const {EventLogger} = require ('doix')
2
- const stringEscape = require ('string-escape-map')
3
-
4
- const ESC_PARAMS = new stringEscape ([
5
- ['\t', '\\t'],
6
- ['\n', '\\n'],
7
- ['\r', '\\r'],
8
- [ "'", "''"],
9
- ])
10
-
11
- const normalizeSpace = s => {
12
-
13
- s = s.trim ();
14
-
15
- const {length} = s; if (length === 0) return s
16
-
17
- let r = '', from = 0
18
-
19
- while (from < length) {
20
-
21
- let to = from + 1; while (to < length && s.charCodeAt (to) > 32) to ++
22
-
23
- if (r.length !== 0) r += ' '
24
-
25
- r += s.slice (from, to)
26
-
27
- from = to; while (from < length && s.charCodeAt (from) <= 32) from ++
28
-
29
- }
30
-
31
- return r
32
-
33
- }
34
-
35
- const stringifyParams = params => {
36
-
37
- if (!Array.isArray (params)) return JSON.stringify (params)
38
-
39
- if (params.length === 0) return ''
40
-
41
- let s = '['; for (const p of params) {
42
-
43
- if (s.length !== 1) s += ', '
44
-
45
- if (typeof p === 'string') {
46
-
47
- s += "'"
48
- s += ESC_PARAMS.escape (p)
49
- s += "'"
50
-
51
- }
52
- else {
53
-
54
- s += p
55
-
56
- }
57
-
58
- }
59
-
60
- return s + ']'
61
-
62
- }
63
-
64
- class DbEventLogger extends EventLogger {
65
-
66
- constructor (client) {
67
-
68
- super (client)
69
-
70
- this.client = client
71
-
72
- this.logger = client.logger
73
-
74
- }
75
-
76
- get prefix () {
77
-
78
- let {client} = this, {job} = client
79
-
80
- let p = job.uuid + '/' + client.uuid
81
-
82
- while (job = job.parent) p = job.uuid + '/' + p
83
-
84
- return p
85
-
86
- }
87
-
88
- startMessage ({sql, params}) {
89
-
90
- const s = '> ' + normalizeSpace (sql), p = stringifyParams (params)
91
-
92
- return this.message (p.length === 0 ? s : s + ' ' + p)
93
-
94
- }
95
-
96
- }
97
-
98
- DbEventLogger.normalizeSpace = normalizeSpace
99
- DbEventLogger.stringifyParams = stringifyParams
100
-
101
- module.exports = DbEventLogger