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 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
- execModelHook = async (model, hookName, ...args) => {
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) {
@@ -203,7 +203,9 @@ export async function sanitizeAll (model) {
203
203
  }
204
204
  }
205
205
  await runHook(`dobo.${camelCase(model.name)}:afterSanitizeModel`, model)
206
- // sortables
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)
@@ -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]) && !this.support.propType[prop.type]) {
91
- try {
92
- if (prop.type === 'datetime') item[prop.name] = new Date(item[prop.name])
93
- else if (['object', 'array'].includes(prop.type)) item[prop.name] = JSON.parse(item[prop.name])
94
- } catch (err) {
95
- item[prop.name] = null
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 { execModelHook } = this.app.dobo
18
+ const { runModelHook } = this.app.dobo
18
19
  const { noModelHook } = last(args)
19
- if (!noModelHook) await execModelHook(this, name, ...args)
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
- nFilter.query = buildFilterQuery.call(this, nFilter, nOptions) ?? {}
66
- nFilter.match = buildFilterMatch.call(this, nFilter, nOptions) ?? {}
67
- handleRegexInQuery.call(this, nFilter)
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 = {}, options = {}) {
227
+ export function buildFilterQuery (filter = {}) {
222
228
  const { trim, find, isString, isPlainObject } = this.app.lib._
223
- let query = {}
224
- if (isString(filter.query)) {
229
+ let query = filter.query ?? {}
230
+ let q = {}
231
+ if (isString(query)) {
225
232
  try {
226
- filter.query = trim(filter.query)
227
- filter.orgQuery = filter.query
228
- if (trim(filter.query).startsWith('{')) query = JSON.parse(filter.query)
229
- else if (filter.query.includes(':')) query = nql(filter.query).parse()
230
- else {
231
- const fields = this.sortables.filter(f => {
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 (filter.query[0] === '*') return `${f}:~$'${filter.query.replaceAll('*', '')}'`
237
- if (filter.query[filter.length - 1] === '*') return `${f}:~^'${filter.query.replaceAll('*', '')}'`
238
- return `${f}:~'${filter.query.replaceAll('*', '')}'`
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) query = nql(parts[0]).parse()
241
- else if (parts.length > 1) query = nql(parts.join(',')).parse()
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.app.dobo.error('invalidQuery', { orgMessage: err.message })
255
+ this.plugin.error('invalidQuery', { orgMessage: err.message })
245
256
  }
246
- } else if (isPlainObject(filter.query)) query = filter.query
247
- return sanitizeQuery.call(this, query)
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 { cloneDeep, isPlainObject, isArray, find } = this.app.lib._
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
- const sanitize = (key, val, p) => {
257
- if (!isSet(val)) return val
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 (['datetime', 'date', 'time'].includes(prop.type)) {
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] = sanitize(k, v, parent)
297
+ } else obj[k] = sanitizeChild(k, v, parent)
276
298
  })
277
299
  return obj
278
300
  }
279
301
 
280
- export function buildFilterMatch (filter = {}, options = {}) {
302
+ export function buildFilterSearch (filter = {}) {
281
303
  const { isPlainObject, trim, has, uniq } = this.app.lib._
282
- let input = filter.match
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 matcher = {}
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 (!matcher[f]) matcher[f] = []
313
- matcher[f] = uniq([...matcher[f], ...value])
335
+ if (!s[f]) s[f] = []
336
+ s[f] = uniq([...s[f], ...value])
314
337
  }
315
338
  }
316
- if (has(items, '*')) matcher['*'] = items['*']
317
- return matcher
339
+ if (has(items, '*')) s['*'] = items['*']
340
+ return s
318
341
  }
319
342
 
320
- export function handleRegexInQuery (filter) {
321
- if (this.driver.idField.name !== 'id') {
322
- const query = JSON.stringify(filter.query ?? {}, (key, value) => {
323
- if (value instanceof RegExp) return ['__REGEXP__', value.source, value.flags]
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
- }).replaceAll('"id"', `"${this.driver.idField.name}"`)
326
- try {
327
- filter.query = JSON.parse(query, (key, value) => {
328
- if (Array.isArray(value) && value[0] === '__REGEXP__') return new RegExp(value[1], value[2])
329
- return value
330
- })
331
- } catch (err) {}
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dobo",
3
- "version": "2.5.0",
3
+ "version": "2.6.0",
4
4
  "description": "DBMS for Bajo Framework",
5
5
  "main": "index.js",
6
6
  "scripts": {
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