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.
Files changed (38) hide show
  1. package/extend/bajo/intl/en-US.json +1 -1
  2. package/extend/bajo/intl/id.json +1 -0
  3. package/extend/dobo/driver/memory.js +26 -25
  4. package/extend/dobo/feature/created-at.js +14 -3
  5. package/extend/dobo/feature/immutable.js +1 -2
  6. package/extend/dobo/feature/removed-at.js +7 -0
  7. package/extend/dobo/feature/unique.js +18 -6
  8. package/extend/dobo/feature/updated-at.js +15 -5
  9. package/index.js +1 -1
  10. package/lib/collect-connections.js +3 -11
  11. package/lib/collect-drivers.js +2 -2
  12. package/lib/collect-features.js +2 -2
  13. package/lib/collect-models.js +25 -16
  14. package/lib/factory/action.js +0 -1
  15. package/lib/factory/connection.js +26 -4
  16. package/lib/factory/driver.js +131 -26
  17. package/lib/factory/feature.js +0 -1
  18. package/lib/factory/model/_util.js +37 -67
  19. package/lib/factory/model/{bulk-create-records.js → bulk-create-record.js} +9 -9
  20. package/lib/factory/model/create-attachment.js +1 -1
  21. package/lib/factory/model/create-record.js +2 -2
  22. package/lib/factory/model/find-all-record.js +9 -8
  23. package/lib/factory/model/find-attachment.js +1 -1
  24. package/lib/factory/model/find-record.js +3 -2
  25. package/lib/factory/model/get-attachment.js +1 -1
  26. package/lib/factory/model/get-record.js +2 -2
  27. package/lib/factory/model/list-attachment.js +1 -1
  28. package/lib/factory/model/load-fixtures.js +24 -10
  29. package/lib/factory/model/remove-attachment.js +1 -1
  30. package/lib/factory/model/remove-record.js +2 -2
  31. package/lib/factory/model/sanitize-body.js +11 -6
  32. package/lib/factory/model/sanitize-record.js +2 -1
  33. package/lib/factory/model/transaction.js +10 -1
  34. package/lib/factory/model/update-record.js +3 -3
  35. package/lib/factory/model/upsert-record.js +2 -2
  36. package/lib/factory/model.js +18 -5
  37. package/package.json +1 -1
  38. 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, getSingleRef } from './_util.js'
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 getSingleRef.call(this, result.data, options)
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
- 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
  }
@@ -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, getSingleRef, handleReq, clearCache } from './_util.js'
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 getSingleRef.call(this, result.data, options)
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
- for (const prop of [...this.properties, ...extFields]) {
36
+ const properties = [...this.properties, ...extFields]
37
+ for (const prop of properties) {
37
38
  try {
38
- if (partial && !has(body, prop.name)) continue
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 (body[prop.name] === null) continue
41
- if (isSet(body[prop.name])) sanitize(prop.name, prop.type)
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 (body[prop.name] === undefined) omitted.push(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
+ 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
- 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
@@ -1,4 +1,4 @@
1
- import { getFilterAndOptions, execHook, execValidation, execModelHook, execDynHook, getSingleRef, handleReq, clearCache } from './_util.js'
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 = 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
73
73
  const { warnings } = getDefaultValues(options)
74
74
  if (!warnings) delete result.warnings
75
- if (isSet(options.refs)) await getSingleRef.call(this, result.data, options)
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, getSingleRef, handleReq, clearCache } from './_util.js'
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 getSingleRef.call(this, result.data, options)
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)
@@ -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 bulkCreateRecords from './model/bulk-create-records.js'
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
- _simpleLookup = async (value, options = {}) => {
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 = { noHook: true, noCache: true, ...options }
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
- bulkCreateRecords = bulkCreateRecords
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dobo",
3
- "version": "2.22.1",
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,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 ```getSingleRef()``` and ```getMultiRefs()```
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 ```getSingleRef()``` && ```getMultiRefs()``` will be catched and are ignored
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 ```bulkCreateRecords()``` on model & driver
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```