dobo 2.5.1 → 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,6 +34,7 @@ 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
  }
@@ -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,37 +224,42 @@ 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)
@@ -256,6 +267,7 @@ function sanitizeQuery (query = {}, parent) {
256
267
 
257
268
  const sanitizeField = (prop, val) => {
258
269
  if (!prop) return val
270
+ if (val instanceof RegExp) return val
259
271
  if (['datetime'].includes(prop.type)) {
260
272
  const dt = dayjs(val)
261
273
  return dt.isValid() ? dt.toDate() : val
@@ -287,9 +299,10 @@ function sanitizeQuery (query = {}, parent) {
287
299
  return obj
288
300
  }
289
301
 
290
- export function buildFilterMatch (filter = {}, options = {}) {
302
+ export function buildFilterSearch (filter = {}) {
291
303
  const { isPlainObject, trim, has, uniq } = this.app.lib._
292
- let input = filter.match
304
+ const search = filter.search ?? {}
305
+ let input = search
293
306
  if (isPlainObject(input)) return input
294
307
  const split = (value) => {
295
308
  let [field, val] = value.split(':').map(i => i.trim())
@@ -313,37 +326,37 @@ export function buildFilterMatch (filter = {}, options = {}) {
313
326
  items[part.field].push(...part.value.split(' ').filter(v => ![''].includes(v)))
314
327
  }
315
328
  }
316
- const matcher = {}
329
+ const s = {}
317
330
  for (const index of this.indexes.filter(i => i.type === 'fulltext')) {
318
331
  for (const f of index.fields) {
319
332
  const value = []
320
333
  if (typeof items[f] === 'string') items[f] = [items[f]]
321
334
  if (has(items, f)) value.push(...items[f])
322
- if (!matcher[f]) matcher[f] = []
323
- matcher[f] = uniq([...matcher[f], ...value])
335
+ if (!s[f]) s[f] = []
336
+ s[f] = uniq([...s[f], ...value])
324
337
  }
325
338
  }
326
- if (has(items, '*')) matcher['*'] = items['*']
327
- return matcher
339
+ if (has(items, '*')) s['*'] = items['*']
340
+ return s
328
341
  }
329
342
 
330
- export function handleRegexInQuery (filter) {
331
- if (this.driver.idField.name !== 'id') {
332
- const query = JSON.stringify(filter.query ?? {}, (key, value) => {
333
- 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])
334
352
  return value
335
- }).replaceAll('"id"', `"${this.driver.idField.name}"`)
336
- try {
337
- filter.query = JSON.parse(query, (key, value) => {
338
- if (Array.isArray(value) && value[0] === '__REGEXP__') return new RegExp(value[1], value[2])
339
- return value
340
- })
341
- } catch (err) {}
342
- const match = JSON.stringify(filter.match ?? {}).replaceAll('"id"', `"${this.driver.idField.name}"`)
343
- try {
344
- filter.match = JSON.parse(match)
345
- } catch (err) {}
346
- }
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) {}
347
360
  }
348
361
 
349
362
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dobo",
3
- "version": "2.5.1",
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,5 +1,11 @@
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.