dobo 2.23.0 → 2.24.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.
@@ -11,8 +11,7 @@ async function immutable (opts = {}) {
11
11
  return {
12
12
  properties: {
13
13
  name: opts.field,
14
- type: 'boolean',
15
- hidden: true
14
+ type: 'boolean'
16
15
  },
17
16
  hooks: [{
18
17
  name: 'beforeUpdateRecord',
@@ -12,8 +12,9 @@ const defIdField = {
12
12
 
13
13
  async function driverFactory () {
14
14
  const { Tools } = this.app.baseClass
15
- const { pick, cloneDeep, has, uniq, without, isEmpty, omit, isFunction } = this.app.lib._
15
+ const { pick, cloneDeep, has, uniq, without, isEmpty, omit, isFunction, camelCase, last } = this.app.lib._
16
16
  const { isSet } = this.app.lib.aneka
17
+ const { runHook } = this.app.bajo
17
18
 
18
19
  /**
19
20
  * Driver class
@@ -111,6 +112,7 @@ async function driverFactory () {
111
112
  const dt = this.useUtc ? dayjs.utc(item[prop.name]) : dayjs(item[prop.name])
112
113
  item[prop.name] = dt.toDate()
113
114
  }
115
+ if (prop.type === 'boolean' && isSet(item[prop.name])) item[prop.name] = Boolean(item[prop.name])
114
116
  }
115
117
  }
116
118
  return item
@@ -124,6 +126,15 @@ async function driverFactory () {
124
126
  return uniq(items)
125
127
  }
126
128
 
129
+ async _attachHook (name, model, ...args) {
130
+ const { ns } = this.app.dobo
131
+ const options = last(args)
132
+ if (!options.noDriverHook) {
133
+ await runHook(`${ns}:${name}`, model, ...args)
134
+ await runHook(`${ns}.${camelCase(model.name)}:${name}`, model, ...args)
135
+ }
136
+ }
137
+
127
138
  /**
128
139
  * Check uniqueness of fields with unique index
129
140
  *
@@ -245,7 +256,11 @@ async function driverFactory () {
245
256
  if (!isEmpty(resp.data)) throw this.plugin.error('recordExists%s%s', body.id, model.name)
246
257
  }
247
258
  body = this.sanitizeBody(model, body)
259
+
260
+ await this._attachHook('beforeDriverCreateRecord', model, body, options)
248
261
  const result = await this.createRecord(model, body, options)
262
+ await this._attachHook('afterDriverCreateRecord', model, body, result, options)
263
+
249
264
  if (options.noResult) return
250
265
  result.data = this.sanitizeRecord(model, result.data)
251
266
  this._injectMeta(result, options)
@@ -261,14 +276,20 @@ async function driverFactory () {
261
276
  await this._prepIdForCreate(model, body, options)
262
277
  bodies[idx] = this.sanitizeBody(model, body)
263
278
  }
279
+
280
+ await this._attachHook('beforeDriverBulkCreateRecord', model, bodies, options)
264
281
  const items = chunk(bodies, chunkSize)
265
282
  for (const item of items) {
266
283
  await this.bulkCreateRecord(model, item, options)
267
284
  }
285
+ await this._attachHook('afterDriverBulkCreateRecord', model, bodies, [], options)
268
286
  }
269
287
 
270
288
  async _getRecord (model, id, options = {}) {
289
+ await this._attachHook('beforeDriverGetRecord', model, id, options)
271
290
  const result = await this.getRecord(model, id, options)
291
+ await this._attachHook('afterDriverGetRecord', model, id, result, options)
292
+
272
293
  if (isEmpty(result.data) && options.throwNotFound) throw this.plugin.error('recordNotFound%s%s', id, model.name)
273
294
  result.data = this.sanitizeRecord(model, result.data)
274
295
  this._injectMeta(result, options)
@@ -287,7 +308,11 @@ async function driverFactory () {
287
308
  }
288
309
  body = this.sanitizeBody(model, body, true)
289
310
  delete body.id
311
+
312
+ await this._attachHook('beforeDriverUpdateRecord', model, id, body, options)
290
313
  const result = await this.updateRecord(model, id, body, options)
314
+ await this._attachHook('afterDriverUpdateRecord', model, id, body, result, options)
315
+
291
316
  if (options.noResult) return
292
317
  result.oldData = this.sanitizeRecord(model, result.oldData)
293
318
  result.data = this.sanitizeRecord(model, result.data)
@@ -308,7 +333,11 @@ async function driverFactory () {
308
333
  }
309
334
  }
310
335
  body = this.sanitizeBody(model, body)
336
+
337
+ await this._attachHook('beforeDriverUpsertRecord', model, body, options)
311
338
  const result = await this.upsertRecord(model, body, options)
339
+ await this._attachHook('afterDriverUpsertRecord', model, body, result, options)
340
+
312
341
  if (options.noResult) return
313
342
  if (result.oldData) result.oldData = this.sanitizeRecord(model, result.oldData)
314
343
  result.data = this.sanitizeRecord(model, result.data)
@@ -322,7 +351,11 @@ async function driverFactory () {
322
351
  if (!resp.data) throw this.plugin.error('recordNotFound%s%s', id, model.name)
323
352
  options._data = resp.data
324
353
  }
354
+
355
+ await this._attachHook('beforeDriverRemoveRecord', model, id, options)
325
356
  const result = await this.removeRecord(model, id, options)
357
+ await this._attachHook('afterDriverRemoveRecord', model, id, result, options)
358
+
326
359
  if (options.noResult) return
327
360
  result.oldData = this.sanitizeRecord(model, result.oldData)
328
361
  this._injectMeta(result, options)
@@ -330,13 +363,29 @@ async function driverFactory () {
330
363
  }
331
364
 
332
365
  async _clearRecord (model, options = {}) {
366
+ await this._attachHook('beforeDriverClearRecord', model, options)
333
367
  const result = await this.clearRecord(model, options)
368
+ await this._attachHook('afterDriverClearRecord', model, result, options)
369
+
334
370
  this._injectMeta(result, options)
335
371
  return result
336
372
  }
337
373
 
338
374
  async _findRecord (model, filter = {}, options = {}) {
339
- const result = await this.findRecord(model, filter, options)
375
+ let result
376
+ try {
377
+ await this._attachHook('beforeDriverFindRecord', model, filter, options)
378
+ result = await this.findRecord(model, filter, options)
379
+ await this._attachHook('afterDriverFindRecord', model, filter, result, options)
380
+ } catch (err) {
381
+ if (err.message !== '_emptyColumnQuery') throw err
382
+ result = {
383
+ data: [],
384
+ count: 0
385
+ // warnings: [] // TODO: should generate warnings?
386
+ }
387
+ }
388
+
340
389
  for (const idx in result.data) {
341
390
  result.data[idx] = this.sanitizeRecord(model, result.data[idx])
342
391
  }
@@ -345,7 +394,20 @@ async function driverFactory () {
345
394
  }
346
395
 
347
396
  async _findAllRecord (model, filter = {}, options = {}) {
348
- const result = await this.findAllRecord(model, filter, options)
397
+ let result
398
+ try {
399
+ await this._attachHook('beforeDriverFindAllRecord', model, filter, options)
400
+ result = await this.findAllRecord(model, filter, options)
401
+ await this._attachHook('afterDriverFindAllRecord', model, filter, result, options)
402
+ } catch (err) {
403
+ if (err.message !== '_emptyColumnQuery') throw err
404
+ result = {
405
+ data: [],
406
+ count: 0
407
+ // warnings: [] // TODO: should generate warnings?
408
+ }
409
+ }
410
+
349
411
  for (const idx in result.data) {
350
412
  result.data[idx] = this.sanitizeRecord(model, result.data[idx])
351
413
  }
@@ -354,7 +416,17 @@ async function driverFactory () {
354
416
  }
355
417
 
356
418
  async _countRecord (model, filter = {}, options = {}) {
357
- return await this.countRecord(model, filter, options)
419
+ let result
420
+ try {
421
+ await this._attachHook('beforeDriverCountRecord', model, filter, options)
422
+ result = await this.countRecord(model, filter, options)
423
+ await this._attachHook('afterDriverCountRecord', model, filter, result, options)
424
+ } catch (err) {
425
+ if (err.message !== '_emptyColumnQuery') throw err
426
+ result = { data: 0 }
427
+ }
428
+
429
+ return result
358
430
  }
359
431
 
360
432
  async _createAggregate (model, filter = {}, params = {}, options = {}) {
@@ -371,7 +443,16 @@ async function driverFactory () {
371
443
  if (!prop) throw this.plugin.error('unknown%s%s', this.plugin.t('field.field'), field)
372
444
  // if (!fieldPropTypes.includes(prop.type)) throw this.plugin.error('allowedPropType%s%s', field, fieldPropTypes.join(', '))
373
445
 
374
- const result = await this.createAggregate(model, filter, params, options)
446
+ let result
447
+ try {
448
+ await this._attachHook('beforeDriverCreateAggregate', model, filter, params, options)
449
+ result = await this.createAggregate(model, filter, params, options)
450
+ await this._attachHook('afterDriverCreateAggregate', model, filter, params, result, options)
451
+ } catch (err) {
452
+ if (err.message !== '_emptyColumnQuery') throw err
453
+ result = { data: [] }
454
+ }
455
+
375
456
  for (const idx in result.data) {
376
457
  result.data[idx] = this.sanitizeRecord(model, result.data[idx])
377
458
  }
@@ -391,8 +472,17 @@ async function driverFactory () {
391
472
 
392
473
  prop = model.properties.find(p => p.name === field)
393
474
  if (!prop) throw this.plugin.error('unknown%s%s', this.plugin.t('field.field'), field)
394
- // if (!fieldPropTypes.includes(prop.type)) throw this.plugin.error('allowedPropType%s%s', field, fieldPropTypes.join(', '))
395
- const result = await this.createHistogram(model, filter, params, options)
475
+
476
+ let result
477
+ try {
478
+ await this._attachHook('beforeDriverCreateHistogram', model, filter, params, options)
479
+ result = await this.createHistogram(model, filter, params, options)
480
+ await this._attachHook('afterDriverCreateHistogram', model, filter, params, result, options)
481
+ } catch (err) {
482
+ if (err.message !== '_emptyColumnQuery') throw err
483
+ result = { data: [] }
484
+ }
485
+
396
486
  for (const idx in result.data) {
397
487
  result.data[idx] = this.sanitizeRecord(model, result.data[idx])
398
488
  }
@@ -19,9 +19,9 @@ export async function execHook (name, ...args) {
19
19
  let [prefix, ...action] = kebabCase(name).split('-')
20
20
  action = camelCase(action.join(' '))
21
21
  if (!noHook) {
22
- if (prefix === 'before') await runHook(`${ns}:beforeAction`, action, ...args)
22
+ if (prefix === 'before') await runHook(`${ns}:beforeAction`, action, this.name, ...args)
23
23
  await runHook(`${ns}:${name}`, this.name, ...args)
24
- if (prefix === 'after') await runHook(`${ns}:afterAction`, action, ...args)
24
+ if (prefix === 'after') await runHook(`${ns}:afterAction`, action, this.name, ...args)
25
25
  if (prefix === 'before') await runHook(`${ns}.${camelCase(this.name)}:beforeAction`, action, ...args)
26
26
  await runHook(`${ns}.${camelCase(this.name)}:${name}`, ...args)
27
27
  if (prefix === 'after') await runHook(`${ns}.${camelCase(this.name)}:afterAction`, action, ...args)
@@ -29,7 +29,6 @@ export async function execHook (name, ...args) {
29
29
  }
30
30
 
31
31
  export async function execModelHook (name, ...args) {
32
- if (['beforeBuildQuery', 'beforeBuildSearch', 'afterBuildQuery', 'afterBuildSearch'].includes(name)) return
33
32
  const { last } = this.app.lib._
34
33
  const { runModelHook } = this.app.dobo
35
34
  const { noModelHook } = last(args)
@@ -68,7 +67,6 @@ export async function execValidation (body, options = {}) {
68
67
  */
69
68
  export async function getFilterAndOptions (filter = {}, options = {}, action) {
70
69
  const { cloneDeep } = this.app.lib._
71
- const { runModelHook } = this.app.dobo
72
70
  const nFilter = cloneDeep(filter || {})
73
71
  const nOptions = cloneOptions.call(this, options)
74
72
  if (options.noMagic) {
@@ -87,12 +85,8 @@ export async function getFilterAndOptions (filter = {}, options = {}, action) {
87
85
  nOptions.throwNotFound = nOptions.throwNotFound ?? true
88
86
  nFilter.orgQuery = nFilter.query
89
87
  nFilter.orgSearch = nFilter.search
90
- if (!nOptions.noModelHook) await runModelHook(this, 'beforeBuildQuery', nFilter.query, nOptions)
91
88
  nFilter.query = buildFilterQuery.call(this, nFilter) ?? {}
92
- if (!nOptions.noModelHook) await runModelHook(this, 'afterBuildQuery', nFilter.query, nOptions)
93
- if (!nOptions.noModelHook) await runModelHook(this, 'beforeBuilSearch', nFilter.search, nOptions)
94
89
  nFilter.search = buildFilterSearch.call(this, nFilter) ?? {}
95
- if (!nOptions.noModelHook) await runModelHook(this, 'afterBuildSearch', nFilter.search, nOptions)
96
90
  const { limit, page, skip, sort } = preparePagination.call(this, nFilter, nOptions)
97
91
  nFilter.limit = limit
98
92
  nFilter.page = page
@@ -26,8 +26,8 @@ async function exec ({ item, spinner, options, result, items } = {}) {
26
26
 
27
27
  async function loadFixtures ({ spinner, ignoreError = true, collectItems = false, noLookup = false } = {}, options = {}) {
28
28
  const { readConfig } = this.app.bajo
29
- const { resolvePath } = this.app.lib.aneka
30
- const { isEmpty } = this.app.lib._
29
+ const { resolvePath, isSet } = this.app.lib.aneka
30
+ const { isEmpty, isString, isArray } = this.app.lib._
31
31
  if (this.connection.proxy) {
32
32
  this.log.warn('proxiedConnBound%s', this.name)
33
33
  return
@@ -38,13 +38,27 @@ async function loadFixtures ({ spinner, ignoreError = true, collectItems = false
38
38
  const items = await readConfig(pattern, { ns: this.plugin.ns, baseNs: 'dobo', checkOverride: true, defValue: [] })
39
39
  const opts = { ...options, noMagic: true }
40
40
  for (const item of items) {
41
- for (const k in item) {
42
- const v = item[k]
43
- if (!noLookup && typeof v === 'string' && v.slice(0, 2) === '?:') item[k] = await this._simpleLookup(v.slice(2), opts)
44
- if (v === null) item[k] = undefined
41
+ const lv = {}
42
+ for (const key in item) {
43
+ const val = item[key]
44
+ if (!noLookup) {
45
+ if (isString(val) && val.slice(0, 2) === '?:') {
46
+ item[key] = await this._simpleLookup(val.slice(2), lv, opts)
47
+ lv[key] = item[key]
48
+ } else if (isArray(val)) {
49
+ for (const idx in val) {
50
+ if (isString(val[idx]) && val[idx].slice(0, 2) === '?:') {
51
+ item[key][idx] = await this._simpleLookup(val[idx].slice(2), lv, opts)
52
+ if (isSet(item[key][idx])) item[key][idx] += ''
53
+ lv[`${key}.${idx}`] = item[key][idx]
54
+ }
55
+ }
56
+ }
57
+ }
58
+ if (val === null) item[key] = undefined
45
59
  else {
46
- const prop = this.properties.find(item => item.name === k)
47
- if (prop && ['string', 'text'].includes(prop.type)) item[k] += ''
60
+ const prop = this.properties.find(item => item.name === key)
61
+ if (prop && ['string', 'text'].includes(prop.type)) item[key] += ''
48
62
  }
49
63
  }
50
64
  }
@@ -41,7 +41,6 @@ async function sanitizeBody ({ body = {}, partial, strict, extFields = [], noDef
41
41
  continue
42
42
  }
43
43
  result[prop.name] = body[prop.name]
44
- if (result[prop.name] === null) continue
45
44
  if (prop.type === 'array' && isSet(result[prop.name]) && !Array.isArray(result[prop.name])) result[prop.name] = [result[prop.name]]
46
45
  if (isSet(result[prop.name])) sanitize(prop.name, prop.type)
47
46
  else {
@@ -53,8 +52,9 @@ async function sanitizeBody ({ body = {}, partial, strict, extFields = [], noDef
53
52
  } else sanitize(prop.name, prop.type)
54
53
  }
55
54
  }
55
+ if (result[prop.name] === null) continue
56
56
  if (truncateString && isSet(result[prop.name]) && ['string', 'text'].includes(prop.type)) result[prop.name] = result[prop.name].slice(0, prop.maxLength)
57
- if (prop.name.endsWith('Id') && prop.type === 'string' && ['smallint', 'integer'].includes(this.driver.idField.type)) result[prop.name] = result[prop.name] + ''
57
+ if (prop.name.endsWith('Id') && isSet(result[prop.name]) && prop.type === 'string' && ['smallint', 'integer'].includes(this.driver.idField.type)) result[prop.name] = result[prop.name] + ''
58
58
  if (result[prop.name] === undefined) omitted.push(prop.name)
59
59
  } catch (err) {
60
60
  details.push({ field: prop.name, error: err.message, value: body[prop.name], ext: { type: prop.type } })
@@ -1,6 +1,15 @@
1
1
  async function transaction (handler, ...args) {
2
2
  if (!this.driver.support.transaction) return handler.call(this, ...args)
3
- return await this.driver.transaction(this, handler, ...args)
3
+
4
+ const { ns } = this.app.dobo
5
+ const { camelCase } = this.app.lib._
6
+ const { runHook } = this.app.bajo
7
+ const name = 'afterTransaction'
8
+ const result = await this.driver.transaction(this, handler, ...args)
9
+ const [action, ...params] = args
10
+ await runHook(`${ns}:${name}`, this.name, action, result, ...params)
11
+ await runHook(`${ns}.${camelCase(this.name)}:${name}`, action, result, ...params)
12
+ return result
4
13
  }
5
14
 
6
15
  export default transaction
@@ -66,7 +66,7 @@ async function updateRecord (...args) {
66
66
  await execModelHook.call(this, 'beforeUpdateRecord', id, input, options)
67
67
  await execDynHook.call(this, 'beforeUpdateRecord', id, input, options)
68
68
  if (!noValidation) await execValidation.call(this, input, options)
69
- const result = options.record ?? (await this.driver._updateRecord(this, id, input, options)) ?? {}
69
+ const result = await this.driver._updateRecord(this, id, input, options)
70
70
  await handleReq.call(this, result.data.id, 'updated', options)
71
71
  await clearCache.call(this, id)
72
72
  if (noResult) return
@@ -119,7 +119,7 @@ async function modelFactory () {
119
119
  this.properties.unshift(idField)
120
120
  }
121
121
 
122
- _simpleLookup = async (value, options = {}) => {
122
+ _simpleLookup = async (value, lookupValue, options = {}) => {
123
123
  const { get, isEmpty, isString, isPlainObject, isArray } = this.app.lib._
124
124
  let model
125
125
  let field
@@ -130,10 +130,13 @@ async function modelFactory () {
130
130
  } else if (isPlainObject(value)) ({ model, field, query } = value)
131
131
  else if (isArray(value)) [model, field, query] = value
132
132
  else return
133
+ for (const key in lookupValue) {
134
+ query = query.replaceAll(`{${key}}`, lookupValue[key])
135
+ }
133
136
  if (isEmpty(field)) field = 'id'
134
137
  const { getModel } = this.app.dobo
135
138
  const ref = getModel(model)
136
- const opts = { noHook: true, noCache: true, ...options }
139
+ const opts = { ...options, noCache: true, noMagic: true }
137
140
  opts.dataOnly = true
138
141
  const rec = await ref.findOneRecord({ query }, opts)
139
142
  return get(rec, field, null)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dobo",
3
- "version": "2.23.0",
3
+ "version": "2.24.0",
4
4
  "description": "DBMS for Bajo Framework",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/wiki/CHANGES.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changes
2
2
 
3
+ ## 2026-05-16
4
+
5
+ - [2.24.0] Change ```dobo:immutable``` feature, field no longer hidden
6
+ - [2.24.0] Add ```dobo:[before|after]Driver<Action>``` hook
7
+ - [2.24.0] Add ```dobo.<modelName>:[before|after]Driver<Action>``` hook
8
+ - [2.24.0] Change ```model._simpleLookup()```
9
+ - [2.24.0] Remove ```dobo:[before|after]Build[Query|Search]``` hook
10
+ - [2.24.0] Change ```model.loadFixtures()```
11
+ - [2.24.0] Bugfix in ```model.sanitizeBody()```
12
+ - [2.24.0] Add ```dobo:afterTransaction``` hook
13
+ - [2.24.0] Add ```dobo.<modelName>:afterTransaction``` hook
14
+
3
15
  ## 2026-05-11
4
16
 
5
17
  - [2.23.0] Add ```beforeBulkCreate``` model hook on ```dobo:unique``` feature