dobo 2.20.1 → 2.21.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.
@@ -150,6 +150,7 @@
150
150
  "maxPageError%s%s": "Page number (%s) above the allowed threshold (%s)",
151
151
  "duplicateRefKeys%s%s": "Duplicate reference keys found in '%s' (%s)",
152
152
  "sanitizeBodyError": "Error sanitizing body",
153
+ "virtualFieldIn%s%s": "Virtual field can't be used in '%s' on %s",
153
154
  "field": {
154
155
  "id": "ID",
155
156
  "code": "Kode",
@@ -148,6 +148,7 @@
148
148
  "maxPageError%s%s": "Nomor halaman (%s) melampaui batas yang diijinkan (%s)",
149
149
  "duplicateRefKeys%s%s": "Ditemukan kunci referensi duplikat di '%s' (%s)",
150
150
  "sanitizeBodyError": "Kesalahan saat sanitasi body",
151
+ "virtualFieldIn%s%s": "Kolom virtual tidak bisa digunakan di '%s' pada %s",
151
152
  "field": {
152
153
  "id": "ID",
153
154
  "code": "Kode",
@@ -1,6 +1,6 @@
1
1
  async function dt (opts = {}) {
2
2
  opts.field = opts.field ?? 'dt'
3
- opts.type = opts.type ??'datetime'
3
+ opts.type = opts.type ?? 'datetime'
4
4
  opts.formatInt = opts.formatInt ?? false
5
5
  opts.formatValueInt = opts.formatValueInt ?? false
6
6
  const prop = {
@@ -12,13 +12,13 @@ async function dt (opts = {}) {
12
12
  if (opts.type === 'integer') {
13
13
  if (opts.formatInt) {
14
14
  prop.format = async function (val, data, { req } = {}) {
15
- const dt = new Date(data._orig[opts.field])
15
+ const dt = new Date(data[opts.field])
16
16
  return req ? req.format(dt, 'datetime') : this.app.bajo.format(dt, 'datetime', { lang: req.lang })
17
17
  }
18
18
  }
19
19
  if (opts.formatValueInt) {
20
20
  prop.format = async function (val, data, { req } = {}) {
21
- return new Date(data._orig[opts.field])
21
+ return new Date(data[opts.field])
22
22
  }
23
23
  }
24
24
  }
package/index.js CHANGED
@@ -82,7 +82,7 @@ const propertyType = {
82
82
  }
83
83
  }
84
84
 
85
- const commonPropertyTypes = ['name', 'type', 'required', 'rules', 'validator', 'ref', 'default', 'values', 'rulesMsg', 'immutable', 'feature', 'format']
85
+ const commonPropertyTypes = ['name', 'type', 'required', 'rules', 'validator', 'ref', 'default', 'values', 'rulesMsg', 'immutable', 'feature', 'format', 'getValue', 'virtual']
86
86
 
87
87
  /**
88
88
  * Plugin factory
@@ -28,20 +28,29 @@ async function sanitizeProp (model, prop, indexes) {
28
28
  return { value: item, text: item }
29
29
  })
30
30
  } else if (!isString(prop.values)) delete prop.values
31
- if (prop.index) {
32
- if (prop.index === true || prop.index === 'true') prop.index = 'index'
33
- const [idx, idxName] = prop.index.split(':')
34
- const index = { name: idxName ?? `${model.collName}_${prop.name}_${idx}`, fields: [prop.name], type: idx }
35
- indexes.push(index)
36
- }
37
31
  if (prop.hidden) model.hidden.push(prop.name)
38
- if (keys(propType).includes(prop.type)) model.properties.push(pick(prop, allPropKeys))
39
- else {
40
- const feature = this.getFeature(prop.type)
41
- if (!feature) this.fatal('unknownPropType%s%s', prop.type, model.name)
42
- const opts = omit(prop, ['name', 'type'])
43
- opts.field = prop.name
44
- await applyFeature.call(this, model, feature, opts, indexes)
32
+ if (prop.virtual) {
33
+ const keys = Object.keys(propType)
34
+ if (!keys.includes(prop.type)) this.fatal('unknownPropType%s%s', `${prop.name}:${prop.type}`, model.name)
35
+ for (const key of ['required', 'rules', 'index', 'validator', 'ref', 'rulesMsg', 'immutable', 'feature']) {
36
+ delete prop[key]
37
+ }
38
+ model.properties.push(prop)
39
+ } else {
40
+ if (prop.index) {
41
+ if (prop.index === true || prop.index === 'true') prop.index = 'index'
42
+ const [idx, idxName] = prop.index.split(':')
43
+ const index = { name: idxName ?? `${model.collName}_${prop.name}_${idx}`, fields: [prop.name], type: idx }
44
+ indexes.push(index)
45
+ }
46
+ if (keys(propType).includes(prop.type)) model.properties.push(pick(prop, allPropKeys))
47
+ else {
48
+ const feature = this.getFeature(prop.type)
49
+ if (!feature) this.fatal('unknownPropType%s%s', prop.type, model.name)
50
+ const opts = omit(prop, ['name', 'type'])
51
+ opts.field = prop.name
52
+ await applyFeature.call(this, model, feature, opts, indexes)
53
+ }
45
54
  }
46
55
  }
47
56
 
@@ -330,8 +339,19 @@ async function collectModels () {
330
339
  const model = new DoboModel(plugin, schema)
331
340
  me.models.push(model)
332
341
  }
342
+ // last sanitizing & checking
333
343
  for (const model of me.models) {
334
344
  await sanitizeRef.call(this, model, me.models)
345
+ for (const item of model.indexes) {
346
+ for (const field of item.fields) {
347
+ const prop = model.properties.find(p => p.name === field)
348
+ if (!prop || (prop && prop.virtual)) throw this.error('virtualFieldIn%s%s', 'index', model.name)
349
+ }
350
+ }
351
+ for (const field of model.scanable) {
352
+ const prop = model.properties.find(p => p.name === field)
353
+ if (!prop || (prop && prop.virtual)) throw this.error('virtualFieldIn%s%s', 'scanable', model.name)
354
+ }
335
355
  }
336
356
  this.log.debug('collected%s%d', this.t('model'), this.models.length)
337
357
  }
@@ -12,7 +12,7 @@ const defIdField = {
12
12
 
13
13
  async function driverFactory () {
14
14
  const { Tools } = this.app.baseClass
15
- const { cloneDeep, has, uniq, without, isEmpty } = this.app.lib._
15
+ const { pick, cloneDeep, has, uniq, without, isEmpty, omit, isFunction } = this.app.lib._
16
16
  const { isSet } = this.app.lib.aneka
17
17
 
18
18
  /**
@@ -167,10 +167,17 @@ async function driverFactory () {
167
167
  return await this.dropModel(model, options)
168
168
  }
169
169
 
170
+ getRealFields (model) {
171
+ return model.getProperties({ noVirtual: true, namesOnly: true })
172
+ }
173
+
174
+ getVirtualFields (model) {
175
+ return model.getVirtualProperties({ namesOnly: true })
176
+ }
177
+
170
178
  async _prepBodyForCreate (model, body = {}, options = {}) {
171
179
  const { isSet, generateId } = this.app.lib.aneka
172
- const { isFunction } = this.app.lib._
173
- for (const prop of model.properties) {
180
+ for (const prop of model.getProperties({ noVirtual: true })) {
174
181
  if (!isSet(body[prop.name]) && isSet(prop.default)) {
175
182
  if (isFunction(prop.default)) body[prop.name] = await prop.default.call(model)
176
183
  else if (typeof prop.default !== 'string') body[prop.name] = prop.default
@@ -198,6 +205,7 @@ async function driverFactory () {
198
205
  }
199
206
  }
200
207
  }
208
+ return pick(body, this.getRealFields(model))
201
209
  }
202
210
 
203
211
  async _prepIdForCreate (model, body = {}, options = {}) {
@@ -221,19 +229,19 @@ async function driverFactory () {
221
229
  result.warnings.push(...(options.warnings ?? []))
222
230
  }
223
231
 
224
- async _createRecord (model, body = {}, options = {}) {
232
+ async _createRecord (model, input = {}, options = {}) {
225
233
  const { isSet } = this.app.lib.aneka
226
- await this._prepBodyForCreate(model, body, options)
234
+ let body = await this._prepBodyForCreate(model, input, options)
227
235
  await this._prepIdForCreate(model, body, options)
228
236
  if (!options.noUniqueCheck) {
229
237
  if (!this.support.uniqueIndex) await this._checkUnique(model, body, options)
230
238
  }
231
239
  if (!options.noIdCheck && isSet(body.id)) {
232
- const resp = await this.getRecord(model, body.id, { noHook: true })
240
+ const resp = await this.getRecord(model, body.id, { noMagic: true })
233
241
  if (!isEmpty(resp.data)) throw this.plugin.error('recordExists%s%s', body.id, model.name)
234
242
  }
235
- const input = this.sanitizeBody(model, body)
236
- const result = await this.createRecord(model, input, options)
243
+ body = this.sanitizeBody(model, body)
244
+ const result = await this.createRecord(model, body, options)
237
245
  if (options.noResult) return
238
246
  result.data = this.sanitizeRecord(model, result.data)
239
247
  this._injectMeta(result, options)
@@ -245,8 +253,7 @@ async function driverFactory () {
245
253
  let { chunkSize = this.maxChunkSize } = options
246
254
  if (chunkSize > this.maxChunkSize) chunkSize = this.maxChunkSize
247
255
  for (const idx in bodies) {
248
- const body = bodies[idx]
249
- await this._prepBodyForCreate(model, body, options)
256
+ const body = await this._prepBodyForCreate(model, bodies[idx], options)
250
257
  await this._prepIdForCreate(model, body, options)
251
258
  bodies[idx] = this.sanitizeBody(model, body)
252
259
  }
@@ -264,18 +271,19 @@ async function driverFactory () {
264
271
  return result
265
272
  }
266
273
 
267
- async _updateRecord (model, id, body = {}, options = {}) {
274
+ async _updateRecord (model, id, input = {}, options = {}) {
275
+ let body = omit(input, this.getVirtualFields())
268
276
  if (!options.noUniqueCheck) {
269
277
  if (!this.support.uniqueIndex) await this._checkUnique(model, body, options)
270
278
  }
271
279
  if (!options._data) {
272
- const resp = await this.getRecord(model, id, { noHook: true })
280
+ const resp = await this.getRecord(model, id, { noMagic: true })
273
281
  if (!resp.data) throw this.plugin.error('recordNotFound%s%s', id, model.name)
274
282
  options._data = resp.data
275
283
  }
276
- const input = this.sanitizeBody(model, body, true)
277
- delete input.id
278
- const result = await this.updateRecord(model, id, input, options)
284
+ body = this.sanitizeBody(model, body, true)
285
+ delete body.id
286
+ const result = await this.updateRecord(model, id, body, options)
279
287
  if (options.noResult) return
280
288
  result.oldData = this.sanitizeRecord(model, result.oldData)
281
289
  result.data = this.sanitizeRecord(model, result.data)
@@ -283,19 +291,20 @@ async function driverFactory () {
283
291
  return result
284
292
  }
285
293
 
286
- async _upsertRecord (model, body = {}, options = {}) {
294
+ async _upsertRecord (model, input = {}, options = {}) {
295
+ let body = omit(input, this.getVirtualFields())
287
296
  if (!options.noUniqueCheck) {
288
297
  if (!this.uniqueIndexSupport) await this._checkUnique(model, body, options)
289
298
  }
290
299
  if (isSet(body.id)) {
291
300
  if (!options._data) {
292
- const resp = await this.getRecord(model, body.id, { noHook: true })
301
+ const resp = await this.getRecord(model, body.id, { noMagic: true })
293
302
  if (!resp.data) throw this.plugin.error('recordNotFound%s%s', body.id, model.name)
294
303
  options._data = resp.data
295
304
  }
296
305
  }
297
- const input = this.sanitizeBody(model, body)
298
- const result = await this.upsertRecord(model, input, options)
306
+ body = this.sanitizeBody(model, body)
307
+ const result = await this.upsertRecord(model, body, options)
299
308
  if (options.noResult) return
300
309
  if (result.oldData) result.oldData = this.sanitizeRecord(model, result.oldData)
301
310
  result.data = this.sanitizeRecord(model, result.data)
@@ -305,7 +314,7 @@ async function driverFactory () {
305
314
 
306
315
  async _removeRecord (model, id, options = {}) {
307
316
  if (!options._data) {
308
- const resp = await this.getRecord(model, id, { noHook: true })
317
+ const resp = await this.getRecord(model, id, { noMagic: true })
309
318
  if (!resp.data) throw this.plugin.error('recordNotFound%s%s', id, model.name)
310
319
  options._data = resp.data
311
320
  }
@@ -173,10 +173,10 @@ export async function handleAttachmentUpload (id, trigger, options = {}) {
173
173
  async function _getRef ({ ref, rModel, prop, key, options, filter } = {}) {
174
174
  if (!((typeof options.refs === 'string' && ['*', 'all'].includes(options.refs)) || options.refs.includes(key))) return
175
175
  if (ref.fields.length === 0) return
176
- const { formatValue, retainOriginalValue } = options
176
+ const { fmt } = options
177
177
  const fields = [...ref.fields]
178
178
  if (!fields.includes(prop.name)) fields.push(prop.name)
179
- const rOptions = { dataOnly: true, refs: [], formatValue, retainOriginalValue, fields }
179
+ const rOptions = { dataOnly: true, refs: [], fmt, fields }
180
180
  const results = await rModel.findRecord(filter, rOptions)
181
181
  return { rOptions, results }
182
182
  }
@@ -234,7 +234,7 @@ export async function getMultiRefs (records = [], options = {}) {
234
234
  for (const r of records) {
235
235
  matches.push(rModel.sanitizeId(r[prop.name]))
236
236
  }
237
- matches = uniq(without(matches, undefined, null, NaN))
237
+ matches = uniq(without(matches, undefined, null, NaN)).map(i => i + '')
238
238
  let query = {}
239
239
  query[ref.field] = { $in: matches }
240
240
  if (ref.query) query = { $and: [query, parseQuery(ref.query, rModel)] }
@@ -290,14 +290,20 @@ function sanitizeQuery (query = {}, parent) {
290
290
 
291
291
  keys.forEach(k => {
292
292
  const v = obj[k]
293
- const prop = find(this.properties, { name: k })
294
- if (isPlainObject(v)) obj[k] = sanitizeQuery.call(this, v, k)
295
- else if (isArray(v)) {
296
- v.forEach((i, idx) => {
297
- if (isPlainObject(i)) obj[k][idx] = sanitizeQuery.call(this, i, k)
298
- else obj[k][idx] = sanitizeField(prop, i)
299
- })
300
- } else obj[k] = sanitizeChild(k, v, parent)
293
+ const props = this.getProperties({ noVirtual: true })
294
+ const fields = props.map(p => p.name)
295
+ const prop = find(props, { name: k })
296
+ if (k[0] !== '$' && !fields.includes(k)) {
297
+ delete keys[k]
298
+ } else {
299
+ if (isPlainObject(v)) obj[k] = sanitizeQuery.call(this, v, k)
300
+ else if (isArray(v)) {
301
+ v.forEach((i, idx) => {
302
+ if (isPlainObject(i)) obj[k][idx] = sanitizeQuery.call(this, i, k)
303
+ else obj[k][idx] = sanitizeField(prop, i)
304
+ })
305
+ } else obj[k] = sanitizeChild(k, v, parent)
306
+ }
301
307
  })
302
308
  return obj
303
309
  }
@@ -439,3 +445,17 @@ export async function clearCache (id) {
439
445
  await clear({ key: `dobo|${this.name}|findAllRecord` })
440
446
  await clear({ key: `dobo|${this.name}|findOneRecord` })
441
447
  }
448
+
449
+ export async function buildPropValues (prop, opts) {
450
+ const { isString, camelCase } = this.app.lib._
451
+ const { callHandler } = this.app.bajo
452
+ const values = (isString(prop.values) ? await callHandler(prop.values) : [...prop.values]).map(v => {
453
+ if (isString(v)) v = { value: v, text: v }
454
+ if (opts.req) {
455
+ const key = camelCase(`${prop.name} ${v.text}`)
456
+ if (opts.req.te(key)) v.text = opts.req.t(key)
457
+ }
458
+ return v
459
+ })
460
+ return values
461
+ }
@@ -4,49 +4,39 @@ async function exec ({ item, spinner, options, result, items } = {}) {
4
4
  const { isArray, isString } = this.app.lib._
5
5
  const { getPluginDataDir } = this.app.bajo
6
6
  const { fs } = this.app.lib
7
- const resp = await this.createRecord(item, options)
8
- if (isArray(item._attachments) && item._attachments.length > 0) {
9
- for (let att of item._attachments) {
10
- if (isString(att)) att = { field: 'file', file: att }
11
- const fname = path.basename(att.file)
12
- if (fs.existsSync(att.file)) {
13
- const dest = `${getPluginDataDir(this.plugin.ns)}/${resp.id}/${att.field}/${fname}`
14
- try {
15
- fs.copySync(att.file, dest)
16
- } catch (err) {}
7
+
8
+ await this.transaction(async (trx) => {
9
+ const resp = await this.createRecord(item, { ...options, trx })
10
+ if (isArray(item._attachments) && item._attachments.length > 0) {
11
+ for (let att of item._attachments) {
12
+ if (isString(att)) att = { field: 'file', file: att }
13
+ const fname = path.basename(att.file)
14
+ if (fs.existsSync(att.file)) {
15
+ const dest = `${getPluginDataDir(this.plugin.ns)}/${resp.id}/${att.field}/${fname}`
16
+ try {
17
+ fs.copySync(att.file, dest)
18
+ } catch (err) {}
19
+ }
17
20
  }
18
21
  }
19
- }
22
+ })
20
23
  result.success++
21
24
  if (spinner) spinner.setText('recordsAdded%s%d%d', this.name, result.success, items.length)
22
25
  }
23
26
 
24
27
  async function loadFixtures ({ spinner, ignoreError = true, collectItems = false, noLookup = false } = {}, options = {}) {
25
- const { readConfig, eachPlugins } = this.app.bajo
28
+ const { readConfig } = this.app.bajo
26
29
  const { resolvePath } = this.app.lib.aneka
27
- const { isEmpty, isArray, omit } = this.app.lib._
30
+ const { isEmpty } = this.app.lib._
28
31
  if (this.connection.proxy) {
29
32
  this.log.warn('proxiedConnBound%s', this.name)
30
33
  return
31
34
  }
32
35
  const result = { success: 0, failed: 0 }
33
36
  const base = path.basename(this.file, path.extname(this.file))
34
- // original
35
37
  const pattern = resolvePath(`${path.dirname(this.file)}/../fixture/${base}.*`)
36
- let items = await readConfig(pattern, { ns: this.plugin.ns, ignoreError: true })
37
- if (isEmpty(items)) items = []
38
- // override
39
- const overrides = await readConfig(`${this.app.main.dir.pkg}/extend/dobo/override/${this.plugin.ns}/fixture/${base}.*`, { ns: this.plugin.ns, ignoreError: true })
40
-
41
- if (isArray(overrides) && !isEmpty(overrides)) items = overrides
42
- // extend
43
- const me = this
44
- await eachPlugins(async function ({ dir }) {
45
- const { ns } = this
46
- const extend = await readConfig(`${dir}/extend/dobo/extend/${me.plugin.ns}/fixture/${base}.*`, { ns, ignoreError: true })
47
- if (isArray(extend) && !isEmpty(extend)) items.push(...extend)
48
- })
49
- const opts = { noHook: true, noCache: true, ...(omit(options, ['noHook', 'noCache'])) }
38
+ const items = await readConfig(pattern, { ns: this.plugin.ns, baseNs: 'dobo', checkOverride: true, defValue: [] })
39
+ const opts = { ...options, noMagic: true }
50
40
  for (const item of items) {
51
41
  for (const k in item) {
52
42
  const v = item[k]
@@ -63,14 +53,14 @@ async function loadFixtures ({ spinner, ignoreError = true, collectItems = false
63
53
  for (const item of items) {
64
54
  if (ignoreError) {
65
55
  try {
66
- await exec.call(this, { item, spinner, opts, options, result, items })
56
+ await exec.call(this, { item, spinner, options, result, items })
67
57
  } catch (err) {
68
- // console.log(err)
58
+ if (this.app.bajo.config.log.applet) console.error(err)
69
59
  err.model = this.name
70
60
  if (this.app.applet) this.plugin.print.fail(this.app.dobo.validationErrorMessage(err))
71
61
  result.failed++
72
62
  }
73
- } else await exec.call(this, { item, spinner, opts, options, result, items })
63
+ } else await exec.call(this, { item, spinner, options, result, items })
74
64
  }
75
65
  return result
76
66
  }
@@ -1,3 +1,5 @@
1
+ import { buildPropValues } from './_util.js'
2
+
1
3
  /**
2
4
  * Sanitize record to conform with the model's definition
3
5
  *
@@ -23,39 +25,33 @@ async function sanitizeRecord (record = {}, opts = {}) {
23
25
  newFields = without(newFields, ...allHidden)
24
26
  const body = fillObject(record, newFields, null)
25
27
  const newRecord = await this.sanitizeBody({ body, noDefault: true })
26
- if (opts.formatValue) {
27
- if (opts.retainOriginalValue) newRecord._orig = cloneDeep(newRecord)
28
+ for (const key in newRecord) {
29
+ const prop = this.getProperty(key)
30
+ const val = ['object', 'array'].includes(prop.type) ? cloneDeep(newRecord[key]) : newRecord[key]
31
+ if (isFunction(prop.getValue)) newRecord[key] = await prop.getValue.call(this, val, newRecord, opts)
32
+ else if (isString(prop.getValue)) newRecord[key] = await callHandler(this.plugin, this, val, newRecord, opts)
33
+ }
34
+
35
+ if (opts.fmt) {
36
+ newRecord._fmt = cloneDeep(newRecord)
28
37
  for (const key in newRecord) {
29
38
  const prop = this.getProperty(key)
30
39
  if (!prop) continue
31
- if (prop.formatValue && opts.retainOriginalValue) {
32
- const val = ['object', 'array'].includes(prop.type) ? cloneDeep(newRecord._orig[key]) : newRecord._orig[key]
33
- if (isFunction(prop.formatValue)) newRecord._orig[key] = await prop.formatValue.call(this, val, newRecord._orig, opts)
34
- else if (isString(prop.formatValue)) newRecord._orig[key] = await callHandler(this.plugin, this, val, newRecord._orig, opts)
35
- }
36
40
  let value = ['object', 'array'].includes(prop.type) ? cloneDeep(newRecord[key]) : newRecord[key]
37
41
  if (prop.values) {
38
- const values = (isString(prop.values) ? await callHandler(prop.values) : [...prop.values]).map(v => {
39
- if (isString(v)) v = { value: v, text: v }
40
- if (opts.req) {
41
- const { camelCase } = this.app.lib._
42
- const key = camelCase(`${prop.name} ${v.text}`)
43
- if (opts.req.te(key)) v.text = opts.req.t(key)
44
- }
45
- return v
46
- })
42
+ const values = await buildPropValues.call(this, prop, opts)
47
43
  value = (values.find(v => v.value === value) ?? {}).text ?? value
48
44
  }
49
- if (prop.format === false) newRecord[key] = value + ''
50
- else if (isFunction(prop.format)) newRecord[key] = await prop.format.call(this, value, newRecord, opts)
51
- else if (isString(prop.format)) newRecord[key] = await callHandler(this.plugin, this, value, newRecord, opts)
45
+ if (prop.format === false) newRecord._fmt[key] = value + ''
46
+ else if (isFunction(prop.format)) newRecord._fmt[key] = await prop.format.call(this, value, newRecord, opts)
47
+ else if (isString(prop.format)) newRecord._fmt[key] = await callHandler(this.plugin, this, value, newRecord, opts)
52
48
  else {
53
49
  const options = {
54
50
  lang: get(opts, 'req.lang'),
55
51
  latitude: ['lat', 'latitude'].includes(key),
56
52
  longitude: ['lon', 'lng', 'longitude'].includes(key)
57
53
  }
58
- newRecord[key] = format(value, prop.type, options)
54
+ newRecord._fmt[key] = format(value, prop.type, options)
59
55
  }
60
56
  }
61
57
  }
@@ -91,6 +91,20 @@ async function modelFactory () {
91
91
  return this.properties.find(prop => prop.name === name)
92
92
  }
93
93
 
94
+ getProperties = ({ noVirtual, namesOnly } = {}) => {
95
+ const items = noVirtual ? this.properties.filter(prop => !prop.virtual) : this.properties
96
+ return namesOnly ? items.map(item => item.name) : items
97
+ }
98
+
99
+ getVirtualProperties = ({ namesOnly }) => {
100
+ const items = this.properties.filter(prop => prop.virtual)
101
+ return namesOnly ? items.map(item => item.name) : items
102
+ }
103
+
104
+ getIndexes = () => {
105
+ return this.indexes
106
+ }
107
+
94
108
  hasProperty = (name) => {
95
109
  return !!this.getProperty(name)
96
110
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dobo",
3
- "version": "2.20.1",
3
+ "version": "2.21.0",
4
4
  "description": "DBMS for Bajo Framework",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/wiki/CHANGES.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changes
2
2
 
3
+ ## 2026-04-25
4
+
5
+ - [2.21.0] Change ```options.formatValue``` to ```options.fmt```
6
+ - [2.21.0] Remove ```options.retainOriginalValue``` since it is not needed anymore
7
+ - [2.21.0] Remove ```formatValue``` altogether
8
+ - [2.21.0] Remove ```record._orig```, in exchange switch the formatted value to ```record._fmt```
9
+ - [2.21.0] Add ```property.virtual``` to set property is a virtual/calculated property
10
+ - [2.21.0] Add necessary safe guards for virtual property
11
+ - [2.21.0] Add ```model.getProperties()```
12
+ - [2.21.0] Add ```model.getIndexes()```
13
+ - [2.21.0] Add ```model.getVirtualPropertties()```
14
+ - [2.21.0] Activate transaction on ```loadFixtures()```
15
+
3
16
  ## 2026-04-23
4
17
 
5
18
  - [2.20.1] Bug fix in ```collect-models.js```, now with deep merge in advance