dobo 2.5.0 → 2.6.0
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/index.js +1 -1
- package/lib/collect-models.js +3 -1
- package/lib/factory/driver.js +18 -6
- package/lib/factory/model/_util.js +74 -51
- package/package.json +1 -1
- package/wiki/CHANGES.md +8 -0
package/index.js
CHANGED
|
@@ -481,7 +481,7 @@ async function factory (pkgName) {
|
|
|
481
481
|
if (!this.constructor.histogramTypes.includes(type)) throw this.error('unsupportedHistogramType%s', type)
|
|
482
482
|
}
|
|
483
483
|
|
|
484
|
-
|
|
484
|
+
runModelHook = async (model, hookName, ...args) => {
|
|
485
485
|
const { orderBy } = this.app.lib._
|
|
486
486
|
const hooks = orderBy(model.hooks.filter(hook => hook.name === hookName), ['level'])
|
|
487
487
|
for (const hook of hooks) {
|
package/lib/collect-models.js
CHANGED
|
@@ -203,7 +203,9 @@ export async function sanitizeAll (model) {
|
|
|
203
203
|
}
|
|
204
204
|
}
|
|
205
205
|
await runHook(`dobo.${camelCase(model.name)}:afterSanitizeModel`, model)
|
|
206
|
-
//
|
|
206
|
+
// fields that can participate in table scan if needed
|
|
207
|
+
model.scans = []
|
|
208
|
+
// sorting only possible using these fields
|
|
207
209
|
model.sortables = []
|
|
208
210
|
for (const index of model.indexes) {
|
|
209
211
|
model.sortables.push(...index.fields)
|
package/lib/factory/driver.js
CHANGED
|
@@ -34,9 +34,11 @@ async function driverFactory () {
|
|
|
34
34
|
array: false,
|
|
35
35
|
datetime: true
|
|
36
36
|
},
|
|
37
|
+
search: false,
|
|
37
38
|
uniqueIndex: false,
|
|
38
39
|
nullableField: true
|
|
39
40
|
}
|
|
41
|
+
this.useUtc = false
|
|
40
42
|
this.maxChunkSize = 500
|
|
41
43
|
this.memory = false
|
|
42
44
|
this.options = options
|
|
@@ -81,18 +83,28 @@ async function driverFactory () {
|
|
|
81
83
|
}
|
|
82
84
|
|
|
83
85
|
sanitizeRecord (model, record = {}) {
|
|
86
|
+
const { dayjs } = this.app.lib
|
|
87
|
+
const { isString } = this.app.lib._
|
|
84
88
|
const item = { ...record }
|
|
85
89
|
if (has(item, this.idField.name) && this.idField.name !== 'id') {
|
|
86
90
|
item.id = item[this.idField.name]
|
|
87
91
|
delete item[this.idField.name]
|
|
88
92
|
}
|
|
89
93
|
for (const prop of model.properties) {
|
|
90
|
-
if (isSet(item[prop.name])
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
94
|
+
if (isSet(item[prop.name])) {
|
|
95
|
+
if (!this.support.propType[prop.type]) {
|
|
96
|
+
try {
|
|
97
|
+
if (prop.type === 'datetime') {
|
|
98
|
+
const dt = this.useUtc ? dayjs.utc(item[prop.name]) : dayjs(item[prop.name])
|
|
99
|
+
item[prop.name] = dt.toDate()
|
|
100
|
+
} else if (['object', 'array'].includes(prop.type)) item[prop.name] = JSON.parse(item[prop.name])
|
|
101
|
+
} catch (err) {
|
|
102
|
+
item[prop.name] = null
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (prop.type === 'datetime' && isString(item[prop.name])) {
|
|
106
|
+
const dt = this.useUtc ? dayjs.utc(item[prop.name]) : dayjs(item[prop.name])
|
|
107
|
+
item[prop.name] = dt.toDate()
|
|
96
108
|
}
|
|
97
109
|
}
|
|
98
110
|
}
|
|
@@ -13,10 +13,11 @@ export async function execHook (name, ...args) {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export async function execModelHook (name, ...args) {
|
|
16
|
+
if (['beforeBuildQuery', 'beforeBuildSearch', 'afterBuildQuery', 'afterBuildSearch'].includes(name)) return
|
|
16
17
|
const { last } = this.app.lib._
|
|
17
|
-
const {
|
|
18
|
+
const { runModelHook } = this.app.dobo
|
|
18
19
|
const { noModelHook } = last(args)
|
|
19
|
-
if (!noModelHook) await
|
|
20
|
+
if (!noModelHook) await runModelHook(this, name, ...args)
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
export async function execDynHook (name, ...args) {
|
|
@@ -51,6 +52,7 @@ export async function execValidation (body, options = {}) {
|
|
|
51
52
|
*/
|
|
52
53
|
export async function getFilterAndOptions (filter = {}, options = {}, action) {
|
|
53
54
|
const { cloneDeep, omit } = this.app.lib._
|
|
55
|
+
const { runModelHook } = this.app.dobo
|
|
54
56
|
const keys = ['req', 'reply']
|
|
55
57
|
const nFilter = cloneDeep(omit(filter, keys))
|
|
56
58
|
const nOptions = cloneDeep(omit(options, keys))
|
|
@@ -62,9 +64,13 @@ export async function getFilterAndOptions (filter = {}, options = {}, action) {
|
|
|
62
64
|
nOptions[key] = options[key]
|
|
63
65
|
}
|
|
64
66
|
|
|
65
|
-
|
|
66
|
-
nFilter.
|
|
67
|
-
|
|
67
|
+
if (!nOptions.noModelHook) await runModelHook(this, 'beforeBuildQuery', nFilter.query, nOptions)
|
|
68
|
+
nFilter.query = buildFilterQuery.call(this, nFilter) ?? {}
|
|
69
|
+
if (!nOptions.noModelHook) await runModelHook(this, 'afterBuildQuery', nFilter.query, nOptions)
|
|
70
|
+
if (!nOptions.noModelHook) await runModelHook(this, 'beforeBuilSearch', nFilter.search, nOptions)
|
|
71
|
+
nFilter.search = buildFilterSearch.call(this, nFilter) ?? {}
|
|
72
|
+
if (!nOptions.noModelHook) await runModelHook(this, 'afterBuildSearch', nFilter.search, nOptions)
|
|
73
|
+
if (this.driver.idField.name !== 'id') replaceIdInQuerySearch.call(this, nFilter)
|
|
68
74
|
const { limit, page, skip, sort } = preparePagination.call(this, nFilter, nOptions)
|
|
69
75
|
nFilter.limit = limit
|
|
70
76
|
nFilter.page = page
|
|
@@ -218,68 +224,85 @@ export async function getMultiRefs (records = [], options = {}) {
|
|
|
218
224
|
}
|
|
219
225
|
}
|
|
220
226
|
|
|
221
|
-
export function buildFilterQuery (filter = {}
|
|
227
|
+
export function buildFilterQuery (filter = {}) {
|
|
222
228
|
const { trim, find, isString, isPlainObject } = this.app.lib._
|
|
223
|
-
let query = {}
|
|
224
|
-
|
|
229
|
+
let query = filter.query ?? {}
|
|
230
|
+
let q = {}
|
|
231
|
+
if (isString(query)) {
|
|
225
232
|
try {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
if (
|
|
229
|
-
else if (
|
|
230
|
-
|
|
231
|
-
|
|
233
|
+
query = trim(query)
|
|
234
|
+
if (query.startsWith('{')) q = JSON.parse(query) // JSON formatted query
|
|
235
|
+
else if (query.includes(':')) q = nql(query).parse() // NQL
|
|
236
|
+
else if (this.driver.support.search) {
|
|
237
|
+
filter.search = q
|
|
238
|
+
return {} //
|
|
239
|
+
} else {
|
|
240
|
+
let scans = [...this.scans]
|
|
241
|
+
if (scans.length === 0) scans = [...this.sortables]
|
|
242
|
+
const fields = scans.filter(f => {
|
|
232
243
|
const field = find(this.properties, { name: f, type: 'string' })
|
|
233
244
|
return !!field
|
|
234
245
|
})
|
|
235
246
|
const parts = fields.map(f => {
|
|
236
|
-
if (
|
|
237
|
-
if (
|
|
238
|
-
return `${f}:~'${
|
|
247
|
+
if (query[0] === '*') return `${f}:~$'${query.replaceAll('*', '')}'`
|
|
248
|
+
if (query[query.length - 1] === '*') return `${f}:~^'${query.replaceAll('*', '')}'`
|
|
249
|
+
return `${f}:~'${query.replaceAll('*', '')}'`
|
|
239
250
|
})
|
|
240
|
-
if (parts.length === 1)
|
|
241
|
-
else if (parts.length > 1)
|
|
251
|
+
if (parts.length === 1) q = nql(parts[0]).parse()
|
|
252
|
+
else if (parts.length > 1) q = nql(parts.join(',')).parse()
|
|
242
253
|
}
|
|
243
254
|
} catch (err) {
|
|
244
|
-
this.
|
|
255
|
+
this.plugin.error('invalidQuery', { orgMessage: err.message })
|
|
245
256
|
}
|
|
246
|
-
} else if (isPlainObject(
|
|
247
|
-
return sanitizeQuery.call(this,
|
|
257
|
+
} else if (isPlainObject(query)) q = query
|
|
258
|
+
return sanitizeQuery.call(this, q)
|
|
248
259
|
}
|
|
249
260
|
|
|
250
261
|
function sanitizeQuery (query = {}, parent) {
|
|
251
|
-
const {
|
|
262
|
+
const { isPlainObject, isArray, find, cloneDeep } = this.app.lib._
|
|
252
263
|
const { isSet } = this.app.lib.aneka
|
|
253
264
|
const { dayjs } = this.app.lib
|
|
254
265
|
const obj = cloneDeep(query)
|
|
255
266
|
const keys = Object.keys(obj)
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
const prop = find(this.properties, { name: key.startsWith('$') ? p : key })
|
|
267
|
+
|
|
268
|
+
const sanitizeField = (prop, val) => {
|
|
259
269
|
if (!prop) return val
|
|
260
|
-
if (
|
|
270
|
+
if (val instanceof RegExp) return val
|
|
271
|
+
if (['datetime'].includes(prop.type)) {
|
|
261
272
|
const dt = dayjs(val)
|
|
262
273
|
return dt.isValid() ? dt.toDate() : val
|
|
263
274
|
} else if (['smallint', 'integer'].includes(prop.type)) return parseInt(val) || val
|
|
264
275
|
else if (['float', 'double'].includes(prop.type)) return parseFloat(val) || val
|
|
265
276
|
else if (['boolean'].includes(prop.type)) return !!val
|
|
277
|
+
else if (['string', 'text'].includes(prop.type)) return val + ''
|
|
266
278
|
return val
|
|
267
279
|
}
|
|
280
|
+
|
|
281
|
+
const sanitizeChild = (key, val, p) => {
|
|
282
|
+
if (!isSet(val)) return val
|
|
283
|
+
const prop = find(this.properties, { name: key.startsWith('$') ? p : key })
|
|
284
|
+
if (!prop) return val
|
|
285
|
+
return sanitizeField(prop, val)
|
|
286
|
+
}
|
|
287
|
+
|
|
268
288
|
keys.forEach(k => {
|
|
269
289
|
const v = obj[k]
|
|
290
|
+
const prop = find(this.properties, { name: k })
|
|
270
291
|
if (isPlainObject(v)) obj[k] = sanitizeQuery.call(this, v, k)
|
|
271
292
|
else if (isArray(v)) {
|
|
272
293
|
v.forEach((i, idx) => {
|
|
273
294
|
if (isPlainObject(i)) obj[k][idx] = sanitizeQuery.call(this, i, k)
|
|
295
|
+
else obj[k][idx] = sanitizeField(prop, i)
|
|
274
296
|
})
|
|
275
|
-
} else obj[k] =
|
|
297
|
+
} else obj[k] = sanitizeChild(k, v, parent)
|
|
276
298
|
})
|
|
277
299
|
return obj
|
|
278
300
|
}
|
|
279
301
|
|
|
280
|
-
export function
|
|
302
|
+
export function buildFilterSearch (filter = {}) {
|
|
281
303
|
const { isPlainObject, trim, has, uniq } = this.app.lib._
|
|
282
|
-
|
|
304
|
+
const search = filter.search ?? {}
|
|
305
|
+
let input = search
|
|
283
306
|
if (isPlainObject(input)) return input
|
|
284
307
|
const split = (value) => {
|
|
285
308
|
let [field, val] = value.split(':').map(i => i.trim())
|
|
@@ -303,37 +326,37 @@ export function buildFilterMatch (filter = {}, options = {}) {
|
|
|
303
326
|
items[part.field].push(...part.value.split(' ').filter(v => ![''].includes(v)))
|
|
304
327
|
}
|
|
305
328
|
}
|
|
306
|
-
const
|
|
329
|
+
const s = {}
|
|
307
330
|
for (const index of this.indexes.filter(i => i.type === 'fulltext')) {
|
|
308
331
|
for (const f of index.fields) {
|
|
309
332
|
const value = []
|
|
310
333
|
if (typeof items[f] === 'string') items[f] = [items[f]]
|
|
311
334
|
if (has(items, f)) value.push(...items[f])
|
|
312
|
-
if (!
|
|
313
|
-
|
|
335
|
+
if (!s[f]) s[f] = []
|
|
336
|
+
s[f] = uniq([...s[f], ...value])
|
|
314
337
|
}
|
|
315
338
|
}
|
|
316
|
-
if (has(items, '*'))
|
|
317
|
-
return
|
|
339
|
+
if (has(items, '*')) s['*'] = items['*']
|
|
340
|
+
return s
|
|
318
341
|
}
|
|
319
342
|
|
|
320
|
-
export function
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
343
|
+
export function replaceIdInQuerySearch (filter) {
|
|
344
|
+
// query
|
|
345
|
+
const query = JSON.stringify(filter.query ?? {}, (key, value) => {
|
|
346
|
+
if (value instanceof RegExp) return ['__REGEXP__', value.source, value.flags]
|
|
347
|
+
return value
|
|
348
|
+
}).replaceAll('"id"', `"${this.driver.idField.name}"`)
|
|
349
|
+
try {
|
|
350
|
+
filter.query = JSON.parse(query, (key, value) => {
|
|
351
|
+
if (Array.isArray(value) && value[0] === '__REGEXP__') return new RegExp(value[1], value[2])
|
|
324
352
|
return value
|
|
325
|
-
})
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
const match = JSON.stringify(filter.match ?? {}).replaceAll('"id"', `"${this.driver.idField.name}"`)
|
|
333
|
-
try {
|
|
334
|
-
filter.match = JSON.parse(match)
|
|
335
|
-
} catch (err) {}
|
|
336
|
-
}
|
|
353
|
+
})
|
|
354
|
+
} catch (err) {}
|
|
355
|
+
// search
|
|
356
|
+
const search = JSON.stringify(filter.search ?? {}).replaceAll('"id"', `"${this.driver.idField.name}"`)
|
|
357
|
+
try {
|
|
358
|
+
filter.search = JSON.parse(search)
|
|
359
|
+
} catch (err) {}
|
|
337
360
|
}
|
|
338
361
|
|
|
339
362
|
/**
|
package/package.json
CHANGED
package/wiki/CHANGES.md
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
# Changes
|
|
2
2
|
|
|
3
|
+
## 2026-02-01
|
|
4
|
+
|
|
5
|
+
- [2.6.0] Add ```model.scans``` for fields that can participate in table scans if necessary
|
|
6
|
+
- [2.6.0] Add ```driver.support.search``` for driver's fulltext search support
|
|
7
|
+
- [2.6.0] Add model hooks ```before/afterBuildQuery/Search```
|
|
8
|
+
|
|
3
9
|
## 2026-01-30
|
|
4
10
|
|
|
5
11
|
- [2.5.0] Add feature to push custom ```options._data```. If provided, this will be used instead of auto generated one.
|
|
6
12
|
- [2.5.0] Remove ```silent``` in ```options``` object
|
|
13
|
+
- [2.5.1] Driver now support ```this.useUtc``` for database that store values in UTC string
|
|
14
|
+
- [2.5.1] Bug fix on ```driver.sanitizeRecord()```
|
|
7
15
|
|
|
8
16
|
## 2026-01-29
|
|
9
17
|
|