dobo 2.23.0 → 2.25.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.
@@ -225,6 +225,9 @@
225
225
  "required": "Required",
226
226
  "only": "Must match with %(ref)s"
227
227
  },
228
+ "array": {
229
+ "base": "Required"
230
+ },
228
231
  "string": {
229
232
  "alphanum": "Must only contain alpha-numeric characters",
230
233
  "base": "Must be a string",
@@ -224,6 +224,9 @@
224
224
  "required": "Harus diisi/dipilih",
225
225
  "only": "Harus sesuai dengan %(ref)s"
226
226
  },
227
+ "array": {
228
+ "base": "Required"
229
+ },
227
230
  "string": {
228
231
  "alphanum": "Harus berupa alfa numerik karakter saja",
229
232
  "base": "Harus berupa string",
@@ -1,6 +1,6 @@
1
1
  async function beforeRemoveRecord (id, opts, options) {
2
2
  const { get } = this.app.lib._
3
- if (get(options, 'req.user.interSiteAdmin')) return
3
+ if (get(options, 'req.user.isXSiteAdmin')) return
4
4
  const record = await this.driver.getRecord(this, id)
5
5
  const immutable = get(record.data, opts.field)
6
6
  if (immutable) throw this.plugin.error('recordImmutable%s%s', id, this.name, { statusCode: 423 })
@@ -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',
package/index.js CHANGED
@@ -73,11 +73,11 @@ const propertyType = {
73
73
  rules: []
74
74
  },
75
75
  object: {
76
- validator: null,
76
+ validator: 'object',
77
77
  rules: []
78
78
  },
79
79
  array: {
80
- validator: null,
80
+ validator: 'array',
81
81
  rules: []
82
82
  }
83
83
  }
@@ -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
@@ -194,7 +188,7 @@ export async function getRefs (records = [], options = {}) {
194
188
  if (!rModel) return
195
189
  let matches = []
196
190
  for (const rec of records) {
197
- const items = isValues ? [...rec[prop.name]] : [rec[prop.name]]
191
+ const items = isValues ? [...(rec[prop.name] ?? [])] : (rec[prop.name] ? [rec[prop.name]] : [])
198
192
  matches.push(...items.map(item => prop.name === 'id' ? rModel.sanitizeId(item) : item))
199
193
  }
200
194
  matches = uniq(without(matches, undefined, null, NaN)).map(i => i + '')
@@ -217,7 +211,7 @@ export async function getRefs (records = [], options = {}) {
217
211
  for (const i in records) {
218
212
  records[i]._ref = records[i]._ref ?? {}
219
213
  const rec = records[i]
220
- let items = isValues ? [...rec[prop.name]] : [rec[prop.name]]
214
+ let items = isValues ? [...(rec[prop.name] ?? [])] : (rec[prop.name] ? [rec[prop.name]] : [])
221
215
  items = items.map(item => item + '')
222
216
  const res = results.filter(r => items.includes(r[ref.field] + ''))
223
217
  if (res.length === 0) records[i]._ref[key] = isValues ? [] : {}
@@ -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, pullAt } = this.app.lib._
31
31
  if (this.connection.proxy) {
32
32
  this.log.warn('proxiedConnBound%s', this.name)
33
33
  return
@@ -38,13 +38,32 @@ 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
+ const deleted = {}
43
+ for (const key in item) {
44
+ const val = item[key]
45
+ deleted[key] = deleted[key] ?? []
46
+ if (!noLookup) {
47
+ if (isString(val) && val.slice(0, 2) === '?:') {
48
+ item[key] = await this._simpleLookup(val.slice(2), lv, opts)
49
+ lv[key] = item[key]
50
+ } else if (isArray(val)) {
51
+ for (const idx in val) {
52
+ if (isString(val[idx]) && val[idx].slice(0, 2) === '?:') {
53
+ item[key][idx] = await this._simpleLookup(val[idx].slice(2), lv, opts)
54
+ if (isSet(item[key][idx])) item[key][idx] += ''
55
+ else deleted[key].push(idx)
56
+ lv[`${key}.${idx}`] = item[key][idx]
57
+ }
58
+ }
59
+ if (deleted[key].length > 0) pullAt(item[key], deleted[key])
60
+ }
61
+ }
62
+ delete deleted[key]
63
+ if (val === null) item[key] = undefined
45
64
  else {
46
- const prop = this.properties.find(item => item.name === k)
47
- if (prop && ['string', 'text'].includes(prop.type)) item[k] += ''
65
+ const prop = this.properties.find(item => item.name === key)
66
+ if (prop && ['string', 'text'].includes(prop.type)) item[key] += ''
48
67
  }
49
68
  }
50
69
  }
@@ -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
@@ -1,6 +1,6 @@
1
1
  import joi from 'joi'
2
2
 
3
- const excludedTypes = ['object', 'array']
3
+ const excludedTypes = ['object']
4
4
  const excludedNames = []
5
5
 
6
6
  /**
@@ -142,7 +142,9 @@ async function buildFromDbModel (opts = {}) {
142
142
  const resp = await callHandler(prop.values)
143
143
  items = resp.map(item => item.value)
144
144
  }
145
- obj = obj.valid(...items)
145
+ if (prop.type === 'array') {
146
+ obj = obj.items(joi.string().valid(...items))
147
+ } else obj = obj.valid(...items)
146
148
  }
147
149
  if (prop.rulesMsg) {
148
150
  const msgs = {}
@@ -185,6 +187,9 @@ async function buildFromDbModel (opts = {}) {
185
187
  case 'boolean':
186
188
  item = await applyFieldRules(p, joi.boolean())
187
189
  break
190
+ case 'array':
191
+ item = await applyFieldRules(p, joi.array())
192
+ break
188
193
  }
189
194
  if (item) {
190
195
  if (item.$_root && !p.required) obj[p.name] = item.allow(null, '')
@@ -96,11 +96,16 @@ async function modelFactory () {
96
96
  return namesOnly ? items.map(item => item.name) : items
97
97
  }
98
98
 
99
- getVirtualProperties = ({ namesOnly }) => {
99
+ getVirtualProperties = (namesOnly) => {
100
100
  const items = this.properties.filter(prop => prop.virtual)
101
101
  return namesOnly ? items.map(item => item.name) : items
102
102
  }
103
103
 
104
+ getNonVirtualProperties = (namesOnly) => {
105
+ const items = this.properties.filter(prop => !prop.virtual)
106
+ return namesOnly ? items.map(item => item.name) : items
107
+ }
108
+
104
109
  getIndexes = () => {
105
110
  return this.indexes
106
111
  }
@@ -119,7 +124,7 @@ async function modelFactory () {
119
124
  this.properties.unshift(idField)
120
125
  }
121
126
 
122
- _simpleLookup = async (value, options = {}) => {
127
+ _simpleLookup = async (value, lookupValue, options = {}) => {
123
128
  const { get, isEmpty, isString, isPlainObject, isArray } = this.app.lib._
124
129
  let model
125
130
  let field
@@ -130,10 +135,13 @@ async function modelFactory () {
130
135
  } else if (isPlainObject(value)) ({ model, field, query } = value)
131
136
  else if (isArray(value)) [model, field, query] = value
132
137
  else return
138
+ for (const key in lookupValue) {
139
+ query = query.replaceAll(`{${key}}`, lookupValue[key])
140
+ }
133
141
  if (isEmpty(field)) field = 'id'
134
142
  const { getModel } = this.app.dobo
135
143
  const ref = getModel(model)
136
- const opts = { noHook: true, noCache: true, ...options }
144
+ const opts = { ...options, noCache: true, noMagic: true }
137
145
  opts.dataOnly = true
138
146
  const rec = await ref.findOneRecord({ query }, opts)
139
147
  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.25.0",
4
4
  "description": "DBMS for Bajo Framework",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/wiki/CHANGES.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changes
2
2
 
3
+ ## 2026-05-22
4
+
5
+ - [2.25.0] Add ```array``` & ```object``` validator handling
6
+ - [2.25.0] Change ```interSite``` definition to ```xSite```
7
+ - [2.25.0] Add ```model.getNonVirtualProperties()```
8
+ - [2.25.0] Bug fix in ```model.loadFixtures()```
9
+ - [2.25.0] Handle ```array``` validation schema
10
+
11
+ ## 2026-05-16
12
+
13
+ - [2.24.0] Change ```dobo:immutable``` feature, field no longer hidden
14
+ - [2.24.0] Add ```dobo:[before|after]Driver<Action>``` hook
15
+ - [2.24.0] Add ```dobo.<modelName>:[before|after]Driver<Action>``` hook
16
+ - [2.24.0] Change ```model._simpleLookup()```
17
+ - [2.24.0] Remove ```dobo:[before|after]Build[Query|Search]``` hook
18
+ - [2.24.0] Change ```model.loadFixtures()```
19
+ - [2.24.0] Bugfix in ```model.sanitizeBody()```
20
+ - [2.24.0] Add ```dobo:afterTransaction``` hook
21
+ - [2.24.0] Add ```dobo.<modelName>:afterTransaction``` hook
22
+
3
23
  ## 2026-05-11
4
24
 
5
25
  - [2.23.0] Add ```beforeBulkCreate``` model hook on ```dobo:unique``` feature