dobo 2.24.0 → 2.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/extend/bajo/intl/en-US.json +4 -0
- package/extend/bajo/intl/id.json +4 -0
- package/extend/dobo/feature/immutable.js +1 -1
- package/index.js +2 -2
- package/lib/collect-models.js +26 -24
- package/lib/factory/driver.js +26 -26
- package/lib/factory/model/_util.js +2 -2
- package/lib/factory/model/load-fixtures.js +8 -5
- package/lib/factory/model/validate.js +7 -2
- package/lib/factory/model.js +6 -1
- package/package.json +1 -1
- package/wiki/CHANGES.md +14 -0
|
@@ -151,6 +151,7 @@
|
|
|
151
151
|
"duplicateRefKeys%s%s": "Duplicate reference keys found in '%s' (%s)",
|
|
152
152
|
"sanitizeBodyError": "Error sanitizing body",
|
|
153
153
|
"virtualFieldIn%s%s%s": "Virtual field '%s' can't be used in '%s' on %s",
|
|
154
|
+
"noFunctionAllowed%s%s": "Use name handler instead of function in '%s.%s'",
|
|
154
155
|
"field": {
|
|
155
156
|
"id": "ID",
|
|
156
157
|
"code": "Kode",
|
|
@@ -225,6 +226,9 @@
|
|
|
225
226
|
"required": "Required",
|
|
226
227
|
"only": "Must match with %(ref)s"
|
|
227
228
|
},
|
|
229
|
+
"array": {
|
|
230
|
+
"base": "Required"
|
|
231
|
+
},
|
|
228
232
|
"string": {
|
|
229
233
|
"alphanum": "Must only contain alpha-numeric characters",
|
|
230
234
|
"base": "Must be a string",
|
package/extend/bajo/intl/id.json
CHANGED
|
@@ -150,6 +150,7 @@
|
|
|
150
150
|
"duplicateRefKeys%s%s": "Ditemukan kunci referensi duplikat di '%s' (%s)",
|
|
151
151
|
"sanitizeBodyError": "Kesalahan saat sanitasi body",
|
|
152
152
|
"virtualFieldIn%s%s%s": "Kolom virtual '%s' tidak bisa digunakan di '%s' pada %s",
|
|
153
|
+
"noFunctionAllowed%s%s": "Gunakan nama handler, bukan fungsi di '%s.%s'",
|
|
153
154
|
"field": {
|
|
154
155
|
"id": "ID",
|
|
155
156
|
"code": "Kode",
|
|
@@ -224,6 +225,9 @@
|
|
|
224
225
|
"required": "Harus diisi/dipilih",
|
|
225
226
|
"only": "Harus sesuai dengan %(ref)s"
|
|
226
227
|
},
|
|
228
|
+
"array": {
|
|
229
|
+
"base": "Required"
|
|
230
|
+
},
|
|
227
231
|
"string": {
|
|
228
232
|
"alphanum": "Harus berupa alfa numerik karakter saja",
|
|
229
233
|
"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.
|
|
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 })
|
package/index.js
CHANGED
package/lib/collect-models.js
CHANGED
|
@@ -290,7 +290,6 @@ async function createSchema (item) {
|
|
|
290
290
|
}
|
|
291
291
|
item.hooks = orderBy(item.hooks, ['name', 'level'])
|
|
292
292
|
delete item.features
|
|
293
|
-
delete item.base
|
|
294
293
|
await sanitizeAll.call(this, item)
|
|
295
294
|
return item
|
|
296
295
|
}
|
|
@@ -304,8 +303,8 @@ async function createSchema (item) {
|
|
|
304
303
|
* @see Dobo#init
|
|
305
304
|
*/
|
|
306
305
|
async function collectModels () {
|
|
307
|
-
const { eachPlugins } = this.app.bajo
|
|
308
|
-
const { orderBy, has,
|
|
306
|
+
const { eachPlugins, callHandler } = this.app.bajo
|
|
307
|
+
const { orderBy, has, omit } = this.app.lib._
|
|
309
308
|
await actionFactory.call(this)
|
|
310
309
|
await modelFactory.call(this)
|
|
311
310
|
|
|
@@ -313,28 +312,31 @@ async function collectModels () {
|
|
|
313
312
|
const me = this
|
|
314
313
|
let schemas = []
|
|
315
314
|
await eachPlugins(async function ({ file }) {
|
|
316
|
-
const { readConfig } = this.app.bajo
|
|
315
|
+
const { readConfig, callHandler } = this.app.bajo
|
|
317
316
|
const { pascalCase } = this.app.lib.aneka
|
|
318
|
-
const { isPlainObject, isEmpty } = this.app.lib._
|
|
317
|
+
const { isPlainObject, isEmpty, isArray } = this.app.lib._
|
|
319
318
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
319
|
+
let items = await readConfig(file, { ns: this.ns, baseNs: me.ns, merge: true })
|
|
320
|
+
if (isEmpty(items)) return undefined
|
|
321
|
+
if (isPlainObject(items)) {
|
|
322
|
+
items.baseName = items.baseName ?? path.basename(file, path.extname(file))
|
|
323
|
+
items.name = items.name ?? pascalCase(`${this.alias} ${items.baseName}`)
|
|
324
|
+
}
|
|
325
|
+
if (!isArray(items)) items = [items]
|
|
326
|
+
for (const item of items) {
|
|
327
|
+
if (!item.baseName) me.fatal('missing%s%s', 'baseName', file)
|
|
328
|
+
item.name = item.name ?? pascalCase(`${this.alias} ${item.baseName}`)
|
|
329
|
+
me.log.trace('- %s', item.name)
|
|
330
|
+
item.collName = item.collName ?? item.name
|
|
331
|
+
item.options = item.options ?? {}
|
|
332
|
+
item.options.attachment = item.options.attachment ?? true
|
|
333
|
+
item.options.persistence = item.options.persistence ?? true
|
|
334
|
+
item.ns = this.ns
|
|
335
|
+
if (item.buildStart) await callHandler(this, item.buildStart, item)
|
|
336
|
+
const schema = await createSchema.call(me, item)
|
|
337
|
+
schemas.push(schema)
|
|
338
|
+
}
|
|
339
|
+
}, { glob: ['model/*.*', 'model.*'], prefix: this.ns })
|
|
338
340
|
schemas = orderBy(schemas, ['buildLevel', 'name'])
|
|
339
341
|
for (const schema of schemas) {
|
|
340
342
|
const plugin = this.app[schema.ns]
|
|
@@ -360,7 +362,7 @@ async function collectModels () {
|
|
|
360
362
|
const prop = model.properties.find(p => p.name === field)
|
|
361
363
|
if (!prop || (prop && prop.virtual)) throw this.error('virtualFieldIn%s%s%s', field, 'scanable', model.name)
|
|
362
364
|
}
|
|
363
|
-
if (
|
|
365
|
+
if (schema.buildEnd) await callHandler(this.app[schema.ns], schema.buildEnd, model)
|
|
364
366
|
}
|
|
365
367
|
schemas = []
|
|
366
368
|
this.log.debug('collected%s%d', this.t('model'), this.models.length)
|
package/lib/factory/driver.js
CHANGED
|
@@ -130,8 +130,8 @@ async function driverFactory () {
|
|
|
130
130
|
const { ns } = this.app.dobo
|
|
131
131
|
const options = last(args)
|
|
132
132
|
if (!options.noDriverHook) {
|
|
133
|
-
await runHook(`${ns}:${name}`, model, ...args)
|
|
134
|
-
await runHook(`${ns}.${camelCase(model.name)}:${name}`, model, ...args)
|
|
133
|
+
await runHook(`${ns}.driver:${name}`, model, ...args)
|
|
134
|
+
await runHook(`${ns}.driver.${camelCase(model.name)}:${name}`, model, ...args)
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
137
|
|
|
@@ -257,9 +257,9 @@ async function driverFactory () {
|
|
|
257
257
|
}
|
|
258
258
|
body = this.sanitizeBody(model, body)
|
|
259
259
|
|
|
260
|
-
await this._attachHook('
|
|
260
|
+
await this._attachHook('beforeCreateRecord', model, body, options)
|
|
261
261
|
const result = await this.createRecord(model, body, options)
|
|
262
|
-
await this._attachHook('
|
|
262
|
+
await this._attachHook('afterCreateRecord', model, body, result, options)
|
|
263
263
|
|
|
264
264
|
if (options.noResult) return
|
|
265
265
|
result.data = this.sanitizeRecord(model, result.data)
|
|
@@ -277,18 +277,18 @@ async function driverFactory () {
|
|
|
277
277
|
bodies[idx] = this.sanitizeBody(model, body)
|
|
278
278
|
}
|
|
279
279
|
|
|
280
|
-
await this._attachHook('
|
|
280
|
+
await this._attachHook('beforeBulkCreateRecord', model, bodies, options)
|
|
281
281
|
const items = chunk(bodies, chunkSize)
|
|
282
282
|
for (const item of items) {
|
|
283
283
|
await this.bulkCreateRecord(model, item, options)
|
|
284
284
|
}
|
|
285
|
-
await this._attachHook('
|
|
285
|
+
await this._attachHook('afterBulkCreateRecord', model, bodies, [], options)
|
|
286
286
|
}
|
|
287
287
|
|
|
288
288
|
async _getRecord (model, id, options = {}) {
|
|
289
|
-
await this._attachHook('
|
|
289
|
+
await this._attachHook('beforeGetRecord', model, id, options)
|
|
290
290
|
const result = await this.getRecord(model, id, options)
|
|
291
|
-
await this._attachHook('
|
|
291
|
+
await this._attachHook('afterGetRecord', model, id, result, options)
|
|
292
292
|
|
|
293
293
|
if (isEmpty(result.data) && options.throwNotFound) throw this.plugin.error('recordNotFound%s%s', id, model.name)
|
|
294
294
|
result.data = this.sanitizeRecord(model, result.data)
|
|
@@ -309,9 +309,9 @@ async function driverFactory () {
|
|
|
309
309
|
body = this.sanitizeBody(model, body, true)
|
|
310
310
|
delete body.id
|
|
311
311
|
|
|
312
|
-
await this._attachHook('
|
|
312
|
+
await this._attachHook('beforeUpdateRecord', model, id, body, options)
|
|
313
313
|
const result = await this.updateRecord(model, id, body, options)
|
|
314
|
-
await this._attachHook('
|
|
314
|
+
await this._attachHook('afterUpdateRecord', model, id, body, result, options)
|
|
315
315
|
|
|
316
316
|
if (options.noResult) return
|
|
317
317
|
result.oldData = this.sanitizeRecord(model, result.oldData)
|
|
@@ -334,9 +334,9 @@ async function driverFactory () {
|
|
|
334
334
|
}
|
|
335
335
|
body = this.sanitizeBody(model, body)
|
|
336
336
|
|
|
337
|
-
await this._attachHook('
|
|
337
|
+
await this._attachHook('beforeUpsertRecord', model, body, options)
|
|
338
338
|
const result = await this.upsertRecord(model, body, options)
|
|
339
|
-
await this._attachHook('
|
|
339
|
+
await this._attachHook('afterUpsertRecord', model, body, result, options)
|
|
340
340
|
|
|
341
341
|
if (options.noResult) return
|
|
342
342
|
if (result.oldData) result.oldData = this.sanitizeRecord(model, result.oldData)
|
|
@@ -352,9 +352,9 @@ async function driverFactory () {
|
|
|
352
352
|
options._data = resp.data
|
|
353
353
|
}
|
|
354
354
|
|
|
355
|
-
await this._attachHook('
|
|
355
|
+
await this._attachHook('beforeRemoveRecord', model, id, options)
|
|
356
356
|
const result = await this.removeRecord(model, id, options)
|
|
357
|
-
await this._attachHook('
|
|
357
|
+
await this._attachHook('afterRemoveRecord', model, id, result, options)
|
|
358
358
|
|
|
359
359
|
if (options.noResult) return
|
|
360
360
|
result.oldData = this.sanitizeRecord(model, result.oldData)
|
|
@@ -363,9 +363,9 @@ async function driverFactory () {
|
|
|
363
363
|
}
|
|
364
364
|
|
|
365
365
|
async _clearRecord (model, options = {}) {
|
|
366
|
-
await this._attachHook('
|
|
366
|
+
await this._attachHook('beforeClearRecord', model, options)
|
|
367
367
|
const result = await this.clearRecord(model, options)
|
|
368
|
-
await this._attachHook('
|
|
368
|
+
await this._attachHook('afterClearRecord', model, result, options)
|
|
369
369
|
|
|
370
370
|
this._injectMeta(result, options)
|
|
371
371
|
return result
|
|
@@ -374,9 +374,9 @@ async function driverFactory () {
|
|
|
374
374
|
async _findRecord (model, filter = {}, options = {}) {
|
|
375
375
|
let result
|
|
376
376
|
try {
|
|
377
|
-
await this._attachHook('
|
|
377
|
+
await this._attachHook('beforeFindRecord', model, filter, options)
|
|
378
378
|
result = await this.findRecord(model, filter, options)
|
|
379
|
-
await this._attachHook('
|
|
379
|
+
await this._attachHook('afterFindRecord', model, filter, result, options)
|
|
380
380
|
} catch (err) {
|
|
381
381
|
if (err.message !== '_emptyColumnQuery') throw err
|
|
382
382
|
result = {
|
|
@@ -396,9 +396,9 @@ async function driverFactory () {
|
|
|
396
396
|
async _findAllRecord (model, filter = {}, options = {}) {
|
|
397
397
|
let result
|
|
398
398
|
try {
|
|
399
|
-
await this._attachHook('
|
|
399
|
+
await this._attachHook('beforeFindAllRecord', model, filter, options)
|
|
400
400
|
result = await this.findAllRecord(model, filter, options)
|
|
401
|
-
await this._attachHook('
|
|
401
|
+
await this._attachHook('afterFindAllRecord', model, filter, result, options)
|
|
402
402
|
} catch (err) {
|
|
403
403
|
if (err.message !== '_emptyColumnQuery') throw err
|
|
404
404
|
result = {
|
|
@@ -418,9 +418,9 @@ async function driverFactory () {
|
|
|
418
418
|
async _countRecord (model, filter = {}, options = {}) {
|
|
419
419
|
let result
|
|
420
420
|
try {
|
|
421
|
-
await this._attachHook('
|
|
421
|
+
await this._attachHook('beforeCountRecord', model, filter, options)
|
|
422
422
|
result = await this.countRecord(model, filter, options)
|
|
423
|
-
await this._attachHook('
|
|
423
|
+
await this._attachHook('afterCountRecord', model, filter, result, options)
|
|
424
424
|
} catch (err) {
|
|
425
425
|
if (err.message !== '_emptyColumnQuery') throw err
|
|
426
426
|
result = { data: 0 }
|
|
@@ -445,9 +445,9 @@ async function driverFactory () {
|
|
|
445
445
|
|
|
446
446
|
let result
|
|
447
447
|
try {
|
|
448
|
-
await this._attachHook('
|
|
448
|
+
await this._attachHook('beforeCreateAggregate', model, filter, params, options)
|
|
449
449
|
result = await this.createAggregate(model, filter, params, options)
|
|
450
|
-
await this._attachHook('
|
|
450
|
+
await this._attachHook('afterCreateAggregate', model, filter, params, result, options)
|
|
451
451
|
} catch (err) {
|
|
452
452
|
if (err.message !== '_emptyColumnQuery') throw err
|
|
453
453
|
result = { data: [] }
|
|
@@ -475,9 +475,9 @@ async function driverFactory () {
|
|
|
475
475
|
|
|
476
476
|
let result
|
|
477
477
|
try {
|
|
478
|
-
await this._attachHook('
|
|
478
|
+
await this._attachHook('beforeCreateHistogram', model, filter, params, options)
|
|
479
479
|
result = await this.createHistogram(model, filter, params, options)
|
|
480
|
-
await this._attachHook('
|
|
480
|
+
await this._attachHook('afterCreateHistogram', model, filter, params, result, options)
|
|
481
481
|
} catch (err) {
|
|
482
482
|
if (err.message !== '_emptyColumnQuery') throw err
|
|
483
483
|
result = { data: [] }
|
|
@@ -188,7 +188,7 @@ export async function getRefs (records = [], options = {}) {
|
|
|
188
188
|
if (!rModel) return
|
|
189
189
|
let matches = []
|
|
190
190
|
for (const rec of records) {
|
|
191
|
-
const items = isValues ? [...rec[prop.name]] : [rec[prop.name]]
|
|
191
|
+
const items = isValues ? [...(rec[prop.name] ?? [])] : (rec[prop.name] ? [rec[prop.name]] : [])
|
|
192
192
|
matches.push(...items.map(item => prop.name === 'id' ? rModel.sanitizeId(item) : item))
|
|
193
193
|
}
|
|
194
194
|
matches = uniq(without(matches, undefined, null, NaN)).map(i => i + '')
|
|
@@ -211,7 +211,7 @@ export async function getRefs (records = [], options = {}) {
|
|
|
211
211
|
for (const i in records) {
|
|
212
212
|
records[i]._ref = records[i]._ref ?? {}
|
|
213
213
|
const rec = records[i]
|
|
214
|
-
let items = isValues ? [...rec[prop.name]] : [rec[prop.name]]
|
|
214
|
+
let items = isValues ? [...(rec[prop.name] ?? [])] : (rec[prop.name] ? [rec[prop.name]] : [])
|
|
215
215
|
items = items.map(item => item + '')
|
|
216
216
|
const res = results.filter(r => items.includes(r[ref.field] + ''))
|
|
217
217
|
if (res.length === 0) records[i]._ref[key] = isValues ? [] : {}
|
|
@@ -26,21 +26,21 @@ 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 {
|
|
30
|
-
const { isEmpty, isString, isArray } = this.app.lib._
|
|
29
|
+
const { 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
|
|
34
34
|
}
|
|
35
35
|
const result = { success: 0, failed: 0 }
|
|
36
|
-
const
|
|
37
|
-
const pattern = resolvePath(`${path.dirname(this.options.file)}/../fixture/${base}.*`)
|
|
38
|
-
const items = await readConfig(pattern, { ns: this.plugin.ns, baseNs: 'dobo', checkOverride: true, defValue: [] })
|
|
36
|
+
const items = await readConfig(`${this.plugin.ns}:/extend/dobo/fixture/${this.baseName}.*`, { ns: this.plugin.ns, baseNs: 'dobo', checkOverride: true, defValue: [] })
|
|
39
37
|
const opts = { ...options, noMagic: true }
|
|
40
38
|
for (const item of items) {
|
|
41
39
|
const lv = {}
|
|
40
|
+
const deleted = {}
|
|
42
41
|
for (const key in item) {
|
|
43
42
|
const val = item[key]
|
|
43
|
+
deleted[key] = deleted[key] ?? []
|
|
44
44
|
if (!noLookup) {
|
|
45
45
|
if (isString(val) && val.slice(0, 2) === '?:') {
|
|
46
46
|
item[key] = await this._simpleLookup(val.slice(2), lv, opts)
|
|
@@ -50,11 +50,14 @@ async function loadFixtures ({ spinner, ignoreError = true, collectItems = false
|
|
|
50
50
|
if (isString(val[idx]) && val[idx].slice(0, 2) === '?:') {
|
|
51
51
|
item[key][idx] = await this._simpleLookup(val[idx].slice(2), lv, opts)
|
|
52
52
|
if (isSet(item[key][idx])) item[key][idx] += ''
|
|
53
|
+
else deleted[key].push(idx)
|
|
53
54
|
lv[`${key}.${idx}`] = item[key][idx]
|
|
54
55
|
}
|
|
55
56
|
}
|
|
57
|
+
if (deleted[key].length > 0) pullAt(item[key], deleted[key])
|
|
56
58
|
}
|
|
57
59
|
}
|
|
60
|
+
delete deleted[key]
|
|
58
61
|
if (val === null) item[key] = undefined
|
|
59
62
|
else {
|
|
60
63
|
const prop = this.properties.find(item => item.name === key)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import joi from 'joi'
|
|
2
2
|
|
|
3
|
-
const excludedTypes = ['object'
|
|
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
|
-
|
|
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, '')
|
package/lib/factory/model.js
CHANGED
|
@@ -96,11 +96,16 @@ async function modelFactory () {
|
|
|
96
96
|
return namesOnly ? items.map(item => item.name) : items
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
getVirtualProperties = (
|
|
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
|
}
|
package/package.json
CHANGED
package/wiki/CHANGES.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changes
|
|
2
2
|
|
|
3
|
+
## 2026-05-26
|
|
4
|
+
|
|
5
|
+
- [2.26.0] Add loading multiple model schema as in one ```model.js``` file
|
|
6
|
+
- [2.26.0] Remove caching feature of model schema
|
|
7
|
+
- [2.26.0] Change driver hook name with this syntax: ```dobo.driver:<action>```
|
|
8
|
+
|
|
9
|
+
## 2026-05-22
|
|
10
|
+
|
|
11
|
+
- [2.25.0] Add ```array``` & ```object``` validator handling
|
|
12
|
+
- [2.25.0] Change ```interSite``` definition to ```xSite```
|
|
13
|
+
- [2.25.0] Add ```model.getNonVirtualProperties()```
|
|
14
|
+
- [2.25.0] Bug fix in ```model.loadFixtures()```
|
|
15
|
+
- [2.25.0] Handle ```array``` validation schema
|
|
16
|
+
|
|
3
17
|
## 2026-05-16
|
|
4
18
|
|
|
5
19
|
- [2.24.0] Change ```dobo:immutable``` feature, field no longer hidden
|