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.
- package/extend/dobo/feature/immutable.js +1 -2
- package/lib/factory/driver.js +97 -7
- package/lib/factory/model/_util.js +2 -8
- package/lib/factory/model/load-fixtures.js +22 -8
- package/lib/factory/model/sanitize-body.js +2 -2
- package/lib/factory/model/transaction.js +10 -1
- package/lib/factory/model/update-record.js +1 -1
- package/lib/factory/model.js +5 -2
- package/package.json +1 -1
- package/wiki/CHANGES.md +12 -0
package/lib/factory/driver.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
395
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (
|
|
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 ===
|
|
47
|
-
if (prop && ['string', 'text'].includes(prop.type)) item[
|
|
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
|
-
|
|
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 =
|
|
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
|
package/lib/factory/model.js
CHANGED
|
@@ -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 = {
|
|
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
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
|