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.
@@ -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",
@@ -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.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 })
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
  }
@@ -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, isFunction, omit } = this.app.lib._
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
- const base = path.basename(file, path.extname(file))
321
- const defName = pascalCase(`${this.alias} ${base}`)
322
- const item = await readConfig(file, { ns: this.ns, baseNs: me.ns, merge: true })
323
- if (isEmpty(item)) return undefined
324
- if (!isPlainObject(item)) me.fatal('invalidModel%s', defName)
325
- item.name = item.name ?? defName
326
- me.log.trace('- %s', item.name)
327
- item.collName = item.collName ?? item.name
328
- item.options = item.options ?? {}
329
- item.options.attachment = item.options.attachment ?? true
330
- item.options.persistence = item.options.persistence ?? true
331
- item.options.file = file
332
- item.options.base = item.options.base ?? path.basename(file, path.extname(file))
333
- item.ns = this.ns
334
- if (isFunction(item.buildStart)) await item.buildStart.call(me, item)
335
- const schema = await createSchema.call(me, item)
336
- schemas.push(schema)
337
- }, { glob: 'model/*.*', prefix: this.ns })
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 (isFunction(schema.buildEnd)) await schema.buildEnd.call(me, model)
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)
@@ -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('beforeDriverCreateRecord', model, body, options)
260
+ await this._attachHook('beforeCreateRecord', model, body, options)
261
261
  const result = await this.createRecord(model, body, options)
262
- await this._attachHook('afterDriverCreateRecord', model, body, result, options)
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('beforeDriverBulkCreateRecord', model, bodies, options)
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('afterDriverBulkCreateRecord', model, bodies, [], options)
285
+ await this._attachHook('afterBulkCreateRecord', model, bodies, [], options)
286
286
  }
287
287
 
288
288
  async _getRecord (model, id, options = {}) {
289
- await this._attachHook('beforeDriverGetRecord', model, id, options)
289
+ await this._attachHook('beforeGetRecord', model, id, options)
290
290
  const result = await this.getRecord(model, id, options)
291
- await this._attachHook('afterDriverGetRecord', model, id, result, options)
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('beforeDriverUpdateRecord', model, id, body, options)
312
+ await this._attachHook('beforeUpdateRecord', model, id, body, options)
313
313
  const result = await this.updateRecord(model, id, body, options)
314
- await this._attachHook('afterDriverUpdateRecord', model, id, body, result, options)
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('beforeDriverUpsertRecord', model, body, options)
337
+ await this._attachHook('beforeUpsertRecord', model, body, options)
338
338
  const result = await this.upsertRecord(model, body, options)
339
- await this._attachHook('afterDriverUpsertRecord', model, body, result, options)
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('beforeDriverRemoveRecord', model, id, options)
355
+ await this._attachHook('beforeRemoveRecord', model, id, options)
356
356
  const result = await this.removeRecord(model, id, options)
357
- await this._attachHook('afterDriverRemoveRecord', model, id, result, options)
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('beforeDriverClearRecord', model, options)
366
+ await this._attachHook('beforeClearRecord', model, options)
367
367
  const result = await this.clearRecord(model, options)
368
- await this._attachHook('afterDriverClearRecord', model, result, options)
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('beforeDriverFindRecord', model, filter, options)
377
+ await this._attachHook('beforeFindRecord', model, filter, options)
378
378
  result = await this.findRecord(model, filter, options)
379
- await this._attachHook('afterDriverFindRecord', model, filter, result, options)
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('beforeDriverFindAllRecord', model, filter, options)
399
+ await this._attachHook('beforeFindAllRecord', model, filter, options)
400
400
  result = await this.findAllRecord(model, filter, options)
401
- await this._attachHook('afterDriverFindAllRecord', model, filter, result, options)
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('beforeDriverCountRecord', model, filter, options)
421
+ await this._attachHook('beforeCountRecord', model, filter, options)
422
422
  result = await this.countRecord(model, filter, options)
423
- await this._attachHook('afterDriverCountRecord', model, filter, result, options)
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('beforeDriverCreateAggregate', model, filter, params, options)
448
+ await this._attachHook('beforeCreateAggregate', model, filter, params, options)
449
449
  result = await this.createAggregate(model, filter, params, options)
450
- await this._attachHook('afterDriverCreateAggregate', model, filter, params, result, options)
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('beforeDriverCreateHistogram', model, filter, params, options)
478
+ await this._attachHook('beforeCreateHistogram', model, filter, params, options)
479
479
  result = await this.createHistogram(model, filter, params, options)
480
- await this._attachHook('afterDriverCreateHistogram', model, filter, params, result, options)
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 { resolvePath, isSet } = this.app.lib.aneka
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 base = path.basename(this.options.file, path.extname(this.options.file))
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', '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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dobo",
3
- "version": "2.24.0",
3
+ "version": "2.26.0",
4
4
  "description": "DBMS for Bajo Framework",
5
5
  "main": "index.js",
6
6
  "scripts": {
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