dobo 2.20.0 → 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
 
@@ -52,12 +61,12 @@ async function sanitizeProp (model, prop, indexes) {
52
61
  * @param {Array} [inputs] - Array of properties
53
62
  * @param {Array} [indexes] - Container array to fill up found index
54
63
  */
55
- async function findAllProps (model, inputs = [], indexes = [], isExtender) {
64
+ async function findAllProps (model, inputs = [], indexes = []) {
56
65
  const { isPlainObject, cloneDeep } = this.app.lib._
57
66
  const isIdProp = inputs.find(p => {
58
67
  return isPlainObject(p) ? p.name === 'id' : p.startsWith('id,')
59
68
  })
60
- if (!isExtender && !isIdProp) {
69
+ if (!isIdProp) {
61
70
  const idField = cloneDeep(model.connection.driver.idField)
62
71
  idField.name = 'id'
63
72
  inputs.unshift(idField)
@@ -74,7 +83,7 @@ async function findAllProps (model, inputs = [], indexes = [], isExtender) {
74
83
  * @param {Object} options - Options to the feature
75
84
  * @returns {Array} New properties found in feature
76
85
  */
77
- async function applyFeature (model, feature, options, indexes, isExtender) {
86
+ async function applyFeature (model, feature, options, indexes) {
78
87
  const { isArray, findIndex } = this.app.lib._
79
88
  if (feature.name === 'dobo:unique' && options.field === 'id') {
80
89
  const idx = findIndex(model.properties, { name: 'id' })
@@ -103,14 +112,14 @@ async function applyFeature (model, feature, options, indexes, isExtender) {
103
112
  * @param {Object} model - Model
104
113
  * @param {Array} [inputs] - Array of properties
105
114
  */
106
- async function findAllFeats (model, inputs = [], indexes = [], isExtender) {
115
+ async function findAllFeats (model, inputs = [], indexes = []) {
107
116
  const { isString, omit } = this.app.lib._
108
117
  for (let feat of inputs) {
109
118
  if (isString(feat)) feat = { name: feat }
110
119
  const featName = feat.name.indexOf(':') === -1 ? `dobo:${feat.name}` : feat.name
111
120
  const feature = this.app.dobo.getFeature(featName)
112
121
  if (!feature) this.fatal('invalidFeature%s%s', model.name, featName)
113
- await applyFeature.call(this, model, feature, omit(feat, 'name'), indexes, isExtender)
122
+ await applyFeature.call(this, model, feature, omit(feat, 'name'), indexes)
114
123
  }
115
124
  }
116
125
 
@@ -235,9 +244,7 @@ export async function sanitizeAll (model) {
235
244
  * @returns {Object} Sanitized model
236
245
  */
237
246
  async function createSchema (item) {
238
- const { readConfig } = this.app.bajo
239
- const { fastGlob } = this.app.lib
240
- const { find, isPlainObject, orderBy, get, cloneDeep } = this.app.lib._
247
+ const { find, orderBy, get } = this.app.lib._
241
248
  const { mergeObjectsByKey, defaultsDeep, parseObject } = this.app.lib.aneka
242
249
  if (item.file && !item.base) item.base = path.basename(item.file, path.extname(item.file))
243
250
  item.attachment = item.attachment ?? true
@@ -265,29 +272,18 @@ async function createSchema (item) {
265
272
  if (!item.connection && conn === 'default') item.connection = this.getConnection('memory')
266
273
  }
267
274
  if (!item.connection) this.fatal('unknownConn%s%s', conn, item.name)
268
- const defCache = cloneDeep(get(this, 'app.bajoCache.config.default', this.config.default.cache))
269
- item.cache = item.cache ?? item.connection.options.cache ?? defCache
270
- if (item.cache === true) item.cache = get(item, 'connection.options.cache', defCache)
271
- else if (item.cache === false) item.cache = { ttlDur: 0 }
272
- item.cache = parseObject(defaultsDeep(get(this, `app.bajoCache.config.dobo.${item.name}`), item.cache))
275
+ // cache settings
276
+ const defCache = defaultsDeep({}, item.connection.options.cache, get(this, 'app.bajoCache.config.default', this.config.default.cache))
277
+ if (item.cache === false) item.cache = { ttlDur: 0 }
278
+ else if (item.cache === true) item.cache = defCache
279
+ else item.cache = defaultsDeep({}, item.cache, defCache)
280
+ item.cache = parseObject(item.cache)
281
+ if (item.connection.name === 'memory') item.cache.ttlDur = 0
282
+
283
+ // let's run
273
284
  await findAllProps.call(this, item, props, indexes)
274
285
  await findAllFeats.call(this, item, feats, indexes)
275
286
  await findAllIndexes.call(this, item, indexes)
276
- // item extender
277
- if (item.base) {
278
- for (const ns of this.app.getAllNs()) {
279
- const plugin = this.app[ns]
280
- const glob = `${plugin.dir.pkg}/extend/dobo/extend/${item.ns}/model/${item.base}.*`
281
- const files = await fastGlob(glob)
282
- for (const file of files) {
283
- const extender = await readConfig(file, { ns: plugin.ns, ignoreError: true })
284
- if (!isPlainObject(extender)) this.plugin.fatal('invalidModelExtender%s%s', ns, item.name)
285
- await findAllProps.call(this, item, extender.properties ?? [], indexes, true)
286
- await findAllFeats.call(this, item, extender.features ?? [], indexes, true)
287
- await findAllIndexes.call(this, item, extender.indexes ?? [], indexes, true)
288
- }
289
- }
290
- }
291
287
  for (const key of ['properties', 'indexes']) {
292
288
  item[key] = mergeObjectsByKey(item[key], 'name')
293
289
  }
@@ -322,10 +318,11 @@ async function collectModels () {
322
318
 
323
319
  const base = path.basename(file, path.extname(file))
324
320
  const defName = pascalCase(`${this.alias} ${base}`)
325
- const item = await readConfig(file, { ns: this.ns, ignoreError: true })
321
+ const item = await readConfig(file, { ns: this.ns, baseNs: me.ns })
326
322
  if (isEmpty(item)) return undefined
327
323
  if (!isPlainObject(item)) me.fatal('invalidModel%s', defName)
328
324
  item.name = item.name ?? defName
325
+ me.log.trace('- %s', item.name)
329
326
  item.collName = item.collName ?? item.name
330
327
  item.file = file
331
328
  item.ns = this.ns
@@ -342,9 +339,19 @@ async function collectModels () {
342
339
  const model = new DoboModel(plugin, schema)
343
340
  me.models.push(model)
344
341
  }
342
+ // last sanitizing & checking
345
343
  for (const model of me.models) {
346
344
  await sanitizeRef.call(this, model, me.models)
347
- me.log.trace('- %s', model.name)
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
+ }
348
355
  }
349
356
  this.log.debug('collected%s%d', this.t('model'), this.models.length)
350
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.0",
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,22 @@
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
+
16
+ ## 2026-04-23
17
+
18
+ - [2.20.1] Bug fix in ```collect-models.js```, now with deep merge in advance
19
+
3
20
  ## 2026-04-21
4
21
 
5
22
  - [2.19.1] Bug fix in ```collect-models.js```, now values are sanitized with ```parseObject()```