dobo 2.22.1 → 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/bajo/intl/en-US.json +1 -1
- package/extend/bajo/intl/id.json +1 -0
- package/extend/dobo/driver/memory.js +26 -25
- package/extend/dobo/feature/created-at.js +14 -3
- package/extend/dobo/feature/immutable.js +1 -2
- package/extend/dobo/feature/removed-at.js +7 -0
- package/extend/dobo/feature/unique.js +18 -6
- package/extend/dobo/feature/updated-at.js +15 -5
- package/index.js +1 -1
- package/lib/collect-connections.js +3 -11
- package/lib/collect-drivers.js +2 -2
- package/lib/collect-features.js +2 -2
- package/lib/collect-models.js +25 -16
- package/lib/factory/action.js +0 -1
- package/lib/factory/connection.js +26 -4
- package/lib/factory/driver.js +131 -26
- package/lib/factory/feature.js +0 -1
- package/lib/factory/model/_util.js +37 -67
- package/lib/factory/model/{bulk-create-records.js → bulk-create-record.js} +9 -9
- package/lib/factory/model/create-attachment.js +1 -1
- package/lib/factory/model/create-record.js +2 -2
- package/lib/factory/model/find-all-record.js +9 -8
- package/lib/factory/model/find-attachment.js +1 -1
- package/lib/factory/model/find-record.js +3 -2
- package/lib/factory/model/get-attachment.js +1 -1
- package/lib/factory/model/get-record.js +2 -2
- package/lib/factory/model/list-attachment.js +1 -1
- package/lib/factory/model/load-fixtures.js +24 -10
- package/lib/factory/model/remove-attachment.js +1 -1
- package/lib/factory/model/remove-record.js +2 -2
- package/lib/factory/model/sanitize-body.js +11 -6
- package/lib/factory/model/sanitize-record.js +2 -1
- package/lib/factory/model/transaction.js +10 -1
- package/lib/factory/model/update-record.js +3 -3
- package/lib/factory/model/upsert-record.js +2 -2
- package/lib/factory/model.js +18 -5
- package/package.json +1 -1
- package/wiki/CHANGES.md +33 -3
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const action = 'getAttachment'
|
|
2
2
|
|
|
3
3
|
async function getAttachment (...args) {
|
|
4
|
-
if (!this.attachment) return
|
|
4
|
+
if (!this.options.attachment) return
|
|
5
5
|
if (args.length === 0) return this.action(action, ...args)
|
|
6
6
|
let [id, field, file, opts = {}] = args
|
|
7
7
|
const { find } = this.app.lib._
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getFilterAndOptions, execHook, execModelHook, execDynHook,
|
|
1
|
+
import { getFilterAndOptions, execHook, execModelHook, execDynHook, getRefs } from './_util.js'
|
|
2
2
|
const action = 'getRecord'
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -68,7 +68,7 @@ async function getRecord (...args) {
|
|
|
68
68
|
const { warnings } = getDefaultValues(options)
|
|
69
69
|
if (!warnings) delete result.warnings
|
|
70
70
|
if (isEmpty(result.data) && !options.throwNotFound) return dataOnly ? undefined : { data: undefined }
|
|
71
|
-
if (isSet(options.refs)) await
|
|
71
|
+
if (isSet(options.refs)) await getRefs.call(this, [result.data], options)
|
|
72
72
|
if (!noResultSanitizer) result.data = await this.sanitizeRecord(result.data, options)
|
|
73
73
|
await execDynHook.call(this, 'afterGetRecord', id, result, options)
|
|
74
74
|
await execModelHook.call(this, 'afterGetRecord', id, result, options)
|
|
@@ -2,7 +2,7 @@ import path from 'path'
|
|
|
2
2
|
const action = 'listAttachment'
|
|
3
3
|
|
|
4
4
|
async function listAttachment (...args) {
|
|
5
|
-
if (!this.attachment) return
|
|
5
|
+
if (!this.options.attachment) return
|
|
6
6
|
if (args.length === 0) return this.action(action, ...args)
|
|
7
7
|
const [params = {}, opts = {}] = args
|
|
8
8
|
const { map, kebabCase } = this.app.lib._
|
|
@@ -26,25 +26,39 @@ 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
|
|
34
34
|
}
|
|
35
35
|
const result = { success: 0, failed: 0 }
|
|
36
|
-
const base = path.basename(this.file, path.extname(this.file))
|
|
37
|
-
const pattern = resolvePath(`${path.dirname(this.file)}/../fixture/${base}.*`)
|
|
36
|
+
const base = path.basename(this.options.file, path.extname(this.options.file))
|
|
37
|
+
const pattern = resolvePath(`${path.dirname(this.options.file)}/../fixture/${base}.*`)
|
|
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
|
}
|
|
@@ -3,7 +3,7 @@ import path from 'path'
|
|
|
3
3
|
const action = 'removeAttachment'
|
|
4
4
|
|
|
5
5
|
async function removeAttachment (...args) {
|
|
6
|
-
if (!this.attachment) return
|
|
6
|
+
if (!this.options.attachment) return
|
|
7
7
|
if (args.length === 0) return this.action(action, ...args)
|
|
8
8
|
const [id, field, file, opts = {}] = args
|
|
9
9
|
const { fs, fastGlob } = this.app.lib
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getFilterAndOptions, execHook, execModelHook, execDynHook,
|
|
1
|
+
import { getFilterAndOptions, execHook, execModelHook, execDynHook, getRefs, handleReq, clearCache } from './_util.js'
|
|
2
2
|
const action = 'removeRecord'
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -51,7 +51,7 @@ async function removeRecord (...args) {
|
|
|
51
51
|
const { warnings } = getDefaultValues(options)
|
|
52
52
|
if (!warnings) delete result.warnings
|
|
53
53
|
if (!noResultSanitizer) result.oldData = await this.sanitizeRecord(result.oldData, options)
|
|
54
|
-
if (isSet(options.refs)) await
|
|
54
|
+
if (isSet(options.refs)) await getRefs.call(this, [result.data], options)
|
|
55
55
|
await execDynHook.call(this, 'afterRemoveRecord', id, result, options)
|
|
56
56
|
await execModelHook.call(this, 'afterRemoveRecord', id, result, options)
|
|
57
57
|
await execHook.call(this, 'afterRemoveRecord', id, result, options)
|
|
@@ -33,12 +33,16 @@ async function sanitizeBody ({ body = {}, partial, strict, extFields = [], noDef
|
|
|
33
33
|
|
|
34
34
|
const omitted = []
|
|
35
35
|
const details = []
|
|
36
|
-
|
|
36
|
+
const properties = [...this.properties, ...extFields]
|
|
37
|
+
for (const prop of properties) {
|
|
37
38
|
try {
|
|
38
|
-
if (partial && !has(body, prop.name))
|
|
39
|
+
if (partial && !has(body, prop.name)) {
|
|
40
|
+
if (prop.type === 'array') result[prop.name] = null
|
|
41
|
+
continue
|
|
42
|
+
}
|
|
39
43
|
result[prop.name] = body[prop.name]
|
|
40
|
-
if (
|
|
41
|
-
if (isSet(
|
|
44
|
+
if (prop.type === 'array' && isSet(result[prop.name]) && !Array.isArray(result[prop.name])) result[prop.name] = [result[prop.name]]
|
|
45
|
+
if (isSet(result[prop.name])) sanitize(prop.name, prop.type)
|
|
42
46
|
else {
|
|
43
47
|
if (isSet(prop.default) && !noDefault) {
|
|
44
48
|
result[prop.name] = prop.default
|
|
@@ -48,9 +52,10 @@ async function sanitizeBody ({ body = {}, partial, strict, extFields = [], noDef
|
|
|
48
52
|
} else sanitize(prop.name, prop.type)
|
|
49
53
|
}
|
|
50
54
|
}
|
|
55
|
+
if (result[prop.name] === null) continue
|
|
51
56
|
if (truncateString && isSet(result[prop.name]) && ['string', 'text'].includes(prop.type)) result[prop.name] = result[prop.name].slice(0, prop.maxLength)
|
|
52
|
-
if (prop.name.endsWith('Id') && prop.type === 'string' && ['smallint', 'integer'].includes(this.driver.idField.type)) result[prop.name] = result[prop.name] + ''
|
|
53
|
-
if (
|
|
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
|
+
if (result[prop.name] === undefined) omitted.push(prop.name)
|
|
54
59
|
} catch (err) {
|
|
55
60
|
details.push({ field: prop.name, error: err.message, value: body[prop.name], ext: { type: prop.type } })
|
|
56
61
|
}
|
|
@@ -24,7 +24,7 @@ async function sanitizeRecord (record = {}, opts = {}) {
|
|
|
24
24
|
if (!newFields.includes('id')) newFields.unshift('id')
|
|
25
25
|
newFields = without(newFields, ...allHidden)
|
|
26
26
|
const body = fillObject(record, newFields, null)
|
|
27
|
-
const newRecord = await this.sanitizeBody({ body, noDefault: true })
|
|
27
|
+
const newRecord = await this.sanitizeBody({ body, partial: true, noDefault: true })
|
|
28
28
|
if (record._ref) newRecord._ref = cloneDeep(record._ref)
|
|
29
29
|
for (const key in newRecord) {
|
|
30
30
|
const prop = this.getProperty(key)
|
|
@@ -35,6 +35,7 @@ async function sanitizeRecord (record = {}, opts = {}) {
|
|
|
35
35
|
}
|
|
36
36
|
if (opts.fmt) {
|
|
37
37
|
newRecord._fmt = cloneDeep(newRecord)
|
|
38
|
+
delete newRecord._fmt._ref
|
|
38
39
|
for (const key in newRecord) {
|
|
39
40
|
const prop = this.getProperty(key)
|
|
40
41
|
if (!prop) continue
|
|
@@ -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
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getFilterAndOptions, execHook, execValidation, execModelHook, execDynHook,
|
|
1
|
+
import { getFilterAndOptions, execHook, execValidation, execModelHook, execDynHook, getRefs, handleReq, clearCache } from './_util.js'
|
|
2
2
|
import { onlyTypes } from './create-record.js'
|
|
3
3
|
const action = 'updateRecord'
|
|
4
4
|
|
|
@@ -66,13 +66,13 @@ 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
|
|
73
73
|
const { warnings } = getDefaultValues(options)
|
|
74
74
|
if (!warnings) delete result.warnings
|
|
75
|
-
if (isSet(options.refs)) await
|
|
75
|
+
if (isSet(options.refs)) await getRefs.call(this, [result.data], options)
|
|
76
76
|
if (!noResultSanitizer) {
|
|
77
77
|
result.data = await this.sanitizeRecord(result.data, options)
|
|
78
78
|
result.oldData = await this.sanitizeRecord(result.oldData, options)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getFilterAndOptions, execHook, execModelHook, execDynHook, execValidation,
|
|
1
|
+
import { getFilterAndOptions, execHook, execModelHook, execDynHook, execValidation, getRefs, handleReq, clearCache } from './_util.js'
|
|
2
2
|
const action = 'upsertRecord'
|
|
3
3
|
|
|
4
4
|
async function native (body = {}, opts = {}) {
|
|
@@ -21,7 +21,7 @@ async function native (body = {}, opts = {}) {
|
|
|
21
21
|
if (noResult) return
|
|
22
22
|
const { warnings } = getDefaultValues(options)
|
|
23
23
|
if (!warnings) delete result.warnings
|
|
24
|
-
if (isSet(options.refs)) await
|
|
24
|
+
if (isSet(options.refs)) await getRefs.call(this, [result.data], options)
|
|
25
25
|
if (!noResultSanitizer) result.data = await this.sanitizeRecord(result.data, options)
|
|
26
26
|
await execDynHook.call(this, 'afterUpsertRecord', input, result, options)
|
|
27
27
|
await execModelHook.call(this, 'afterUpsertRecord', input, result, options)
|
package/lib/factory/model.js
CHANGED
|
@@ -23,7 +23,7 @@ import sanitizeBody from './model/sanitize-body.js'
|
|
|
23
23
|
import sanitizeRecord from './model/sanitize-record.js'
|
|
24
24
|
import sanitizeId from './model/sanitize-id.js'
|
|
25
25
|
import upsertRecord from './model/upsert-record.js'
|
|
26
|
-
import
|
|
26
|
+
import bulkCreateRecord from './model/bulk-create-record.js'
|
|
27
27
|
import listAttachment from './model/list-attachment.js'
|
|
28
28
|
import transaction from './model/transaction.js'
|
|
29
29
|
import validate from './model/validate.js'
|
|
@@ -109,7 +109,17 @@ async function modelFactory () {
|
|
|
109
109
|
return !!this.getProperty(name)
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
|
|
112
|
+
syncIdField = (idField) => {
|
|
113
|
+
const { cloneDeep, findIndex } = this.app.lib._
|
|
114
|
+
if (!this.driver) return
|
|
115
|
+
if (!idField) idField = cloneDeep(this.driver.idField)
|
|
116
|
+
const idx = findIndex(this.properties, { name: 'id' })
|
|
117
|
+
if (idx === -1) return
|
|
118
|
+
this.properties.splice(idx, 1)
|
|
119
|
+
this.properties.unshift(idField)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
_simpleLookup = async (value, lookupValue, options = {}) => {
|
|
113
123
|
const { get, isEmpty, isString, isPlainObject, isArray } = this.app.lib._
|
|
114
124
|
let model
|
|
115
125
|
let field
|
|
@@ -120,10 +130,13 @@ async function modelFactory () {
|
|
|
120
130
|
} else if (isPlainObject(value)) ({ model, field, query } = value)
|
|
121
131
|
else if (isArray(value)) [model, field, query] = value
|
|
122
132
|
else return
|
|
133
|
+
for (const key in lookupValue) {
|
|
134
|
+
query = query.replaceAll(`{${key}}`, lookupValue[key])
|
|
135
|
+
}
|
|
123
136
|
if (isEmpty(field)) field = 'id'
|
|
124
137
|
const { getModel } = this.app.dobo
|
|
125
138
|
const ref = getModel(model)
|
|
126
|
-
const opts = {
|
|
139
|
+
const opts = { ...options, noCache: true, noMagic: true }
|
|
127
140
|
opts.dataOnly = true
|
|
128
141
|
const rec = await ref.findOneRecord({ query }, opts)
|
|
129
142
|
return get(rec, field, null)
|
|
@@ -144,7 +157,7 @@ async function modelFactory () {
|
|
|
144
157
|
findAllRecord = findAllRecord
|
|
145
158
|
|
|
146
159
|
transaction = transaction
|
|
147
|
-
|
|
160
|
+
bulkCreateRecord = bulkCreateRecord
|
|
148
161
|
|
|
149
162
|
createAggregate = createAggregate
|
|
150
163
|
createHistogram = createHistogram
|
|
@@ -170,6 +183,7 @@ async function modelFactory () {
|
|
|
170
183
|
findRecords = findRecord
|
|
171
184
|
findAllRecords = findAllRecord
|
|
172
185
|
listAttachments = listAttachment
|
|
186
|
+
bulkCreateRecords = bulkCreateRecord
|
|
173
187
|
|
|
174
188
|
getField = (name) => this.getProperty(name)
|
|
175
189
|
hasField = (name) => this.hasProperty(name)
|
|
@@ -182,7 +196,6 @@ async function modelFactory () {
|
|
|
182
196
|
}
|
|
183
197
|
|
|
184
198
|
this.app.baseClass.DoboModel = DoboModel
|
|
185
|
-
return DoboModel
|
|
186
199
|
}
|
|
187
200
|
|
|
188
201
|
export default modelFactory
|
package/package.json
CHANGED
package/wiki/CHANGES.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
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
|
+
|
|
15
|
+
## 2026-05-11
|
|
16
|
+
|
|
17
|
+
- [2.23.0] Add ```beforeBulkCreate``` model hook on ```dobo:unique``` feature
|
|
18
|
+
- [2.23.0] Add ```beforeBulkCreate``` model hook on ```dobo:updatedAt``` feature
|
|
19
|
+
- [2.23.0] Add ```beforeBulkCreate``` model hook on ```dobo:unique``` feature
|
|
20
|
+
- [2.23.0] Add ```connection.initDriver()```
|
|
21
|
+
- [2.23.0] Move ```model.file``` in model definition to ```model.options.file```
|
|
22
|
+
- [2.23.0] Move ```model.attachment``` in model definition to ```model.options.attachment```
|
|
23
|
+
- [2.23.0] Add ```model.buildStart()``` and ```model.buildEnd()``` in model definition
|
|
24
|
+
- [2.23.0] Add ```null``` driver
|
|
25
|
+
- [2.23.0] Add ```model.syncIdField()```
|
|
26
|
+
- [2.23.0] Rename method to ```model.bulkCreateRecord``` instead ```bulkCreateRecords```. The later name now serve only as alias
|
|
27
|
+
- [2.23.0] Remove ```getSingleRef()``` and ```getMultiRefs()```, use ```getRefs()``` instead
|
|
28
|
+
- [2.23.0] Add reference support for ```array``` column type
|
|
29
|
+
- [2.23.0] Bug fix in ```model.findAllRecord()```, now use correctly hook names
|
|
30
|
+
- [2.23.0] Bug fix in ```model.sanitizeBody()```
|
|
31
|
+
- [2.23.0] Bug fix in ```model.sanitizeRecord()```
|
|
32
|
+
|
|
3
33
|
## 2026-05-03
|
|
4
34
|
|
|
5
35
|
- [2.22.1] Bug fix in ```dobo:image``` feature
|
|
@@ -87,7 +117,7 @@
|
|
|
87
117
|
- [2.16.0] Rewrite ```getDefaultValues()``` to base on ```req.getSetting()```
|
|
88
118
|
- [2.16.0] All inter site admins are now exempts from ```immutable``` row
|
|
89
119
|
- [2.16.0] Bug fix in ```collect-models.js```
|
|
90
|
-
- [2.16.0] Bug fix in ```
|
|
120
|
+
- [2.16.0] Bug fix in ```getRefs()``` and ```getRefs()```
|
|
91
121
|
- [2.16.0] Add feature to return formatted row(s) with ```options.formatValue```
|
|
92
122
|
- [2.16.0] If row is formatted, add feature to save original row in ```_orig``` with ```options.retainOriginalValue```
|
|
93
123
|
|
|
@@ -121,7 +151,7 @@
|
|
|
121
151
|
|
|
122
152
|
## 2026-03-26
|
|
123
153
|
|
|
124
|
-
- [2.11.4] Exceptions thrown in ```
|
|
154
|
+
- [2.11.4] Exceptions thrown in ```getRefs()``` && ```getRefs()``` will be catched and are ignored
|
|
125
155
|
|
|
126
156
|
## 2026-03-25
|
|
127
157
|
|
|
@@ -226,7 +256,7 @@
|
|
|
226
256
|
|
|
227
257
|
## 2026-01-29
|
|
228
258
|
|
|
229
|
-
- [2.4.0] Add ```
|
|
259
|
+
- [2.4.0] Add ```bulkCreateRecord()``` on model & driver
|
|
230
260
|
- [2.4.0] Add ```execModelHook()```
|
|
231
261
|
- [2.4.0] Bug fix in models collection
|
|
232
262
|
- [2.4.0] Add ```DoboAction``` to the ```app.baseClass```
|