dobo 2.27.2 → 2.29.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.
@@ -3,7 +3,20 @@ async function beforeRemoveRecord (id, opts, options) {
3
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
- if (immutable) throw this.plugin.error('recordImmutable%s%s', id, this.name, { statusCode: 423 })
6
+ if (immutable.length === 1 && immutable[0] === '*') throw this.plugin.error('recordImmutable%s%s', id, this.name, { statusCode: 423 })
7
+ }
8
+
9
+ async function beforeUpdateRecord (id, body, opts, options) {
10
+ const { get } = this.app.lib._
11
+ if (get(options, 'req.user.isXSiteAdmin')) return
12
+ const record = await this.driver.getRecord(this, id)
13
+ const immutable = get(record.data, opts.field)
14
+ if (immutable.length === 0) return
15
+ const fields = []
16
+ if (immutable.length === 1 && immutable[0] === '*') fields.push(...this.getNonVirtualProperties(true))
17
+ for (const field of fields) {
18
+ delete body[field]
19
+ }
7
20
  }
8
21
 
9
22
  async function immutable (opts = {}) {
@@ -11,12 +24,13 @@ async function immutable (opts = {}) {
11
24
  return {
12
25
  properties: {
13
26
  name: opts.field,
14
- type: 'boolean'
27
+ type: 'array',
28
+ default: []
15
29
  },
16
30
  hooks: [{
17
31
  name: 'beforeUpdateRecord',
18
32
  handler: async function (id, body, options) {
19
- await beforeRemoveRecord.call(this, id, opts, options)
33
+ await beforeUpdateRecord.call(this, id, body, opts, options)
20
34
  }
21
35
  }, {
22
36
  name: 'beforeRemoveRecord',
@@ -10,7 +10,7 @@ import actionFactory from './factory/action.js'
10
10
  * @param {Array} [indexes] - Container array to fill up found index
11
11
  */
12
12
  async function sanitizeProp (model, prop, indexes) {
13
- const { isEmpty, isString, keys, pick, isArray, isPlainObject, omit } = this.app.lib._
13
+ const { isEmpty, isString, keys, pick, isArray, isPlainObject, omit, isFunction } = this.app.lib._
14
14
  const allPropKeys = this.getAllPropertyKeys(model.connection.driver)
15
15
  const propType = this.constructor.propertyType
16
16
  if (isString(prop)) {
@@ -27,7 +27,7 @@ async function sanitizeProp (model, prop, indexes) {
27
27
  if (isPlainObject(item)) return pick(item, ['value', 'text'])
28
28
  return { value: item, text: item }
29
29
  })
30
- } else if (!isString(prop.values)) delete prop.values
30
+ } else if (!(isString(prop.values) || isFunction(prop.values))) delete prop.values
31
31
  if (prop.hidden) model.hidden.push(prop.name)
32
32
  if (prop.scanable) model.scanables.push(prop.scanable)
33
33
  if (prop.virtual) {
@@ -149,7 +149,6 @@ async function findAllIndexes (model, inputs = [], indexes = []) {
149
149
  */
150
150
  export async function sanitizeRef (model, models) {
151
151
  const { find, isString, pullAt } = this.app.lib._
152
- if (!models) models = this.models
153
152
  const _refKeys = []
154
153
  for (const prop of model.properties) {
155
154
  const ignored = []
@@ -339,30 +338,24 @@ async function collectModels () {
339
338
  }, { glob: ['model/*.*', 'model.*'], prefix: this.ns })
340
339
  schemas = orderBy(schemas, ['buildLevel', 'name'])
341
340
  for (const schema of schemas) {
342
- const plugin = this.app[schema.ns]
343
- delete schema.ns
344
341
  const idProp = schema.properties.find(p => p.name === 'id')
345
342
  if (!this.constructor.idTypes.includes(idProp.type)) this.fatal('invalidIdType%s%s', schema.name, this.constructor.idTypes.join(', '))
346
343
  if (idProp.type === 'string' && !has(idProp, 'maxLength')) idProp.maxLength = 50
347
- const model = new this.app.baseClass.DoboModel(plugin, omit(schema, ['beforeCreate', 'afterCreate']))
348
- schema.model = model
349
- me.models.push(model)
350
- }
351
- // last sanitizing & checking
352
- for (const schema of schemas) {
353
- const model = schema.model
354
- await sanitizeRef.call(this, model, me.models)
355
- for (const item of model.indexes) {
344
+ const plugin = this.app[schema.ns]
345
+ await sanitizeRef.call(this, schema, schemas)
346
+ for (const item of schema.indexes) {
356
347
  for (const field of item.fields) {
357
- const prop = model.properties.find(p => p.name === field)
358
- if (!prop || (prop && prop.virtual)) throw this.error('virtualFieldIn%s%s%s', field, 'index', model.name)
348
+ const prop = schema.properties.find(p => p.name === field)
349
+ if (!prop || (prop && prop.virtual)) throw this.error('virtualFieldIn%s%s%s', field, 'index', schema.name)
359
350
  }
360
351
  }
361
- for (const field of model.scanables) {
362
- const prop = model.properties.find(p => p.name === field)
363
- if (!prop || (prop && prop.virtual)) throw this.error('virtualFieldIn%s%s%s', field, 'scanable', model.name)
352
+ for (const field of schema.scanables) {
353
+ const prop = schema.properties.find(p => p.name === field)
354
+ if (!prop || (prop && prop.virtual)) throw this.error('virtualFieldIn%s%s%s', field, 'scanable', schema.name)
364
355
  }
365
- if (schema.buildEnd) await callHandler(model.plugin, schema.buildEnd, model)
356
+ if (schema.buildEnd) await callHandler(plugin, schema.buildEnd, schema)
357
+ const model = new this.app.baseClass.DoboModel(plugin, omit(schema, ['beforeCreate', 'afterCreate']))
358
+ me.models.push(model)
366
359
  }
367
360
  schemas = []
368
361
  this.log.debug('collected%s%d', this.t('model'), this.models.length)
@@ -72,7 +72,6 @@ export async function getFilterAndOptions (filter = {}, options = {}, action) {
72
72
  const nFilter = cloneDeep(filter || {})
73
73
  const nOptions = cloneOptions.call(this, options)
74
74
  if (options.noMagic) {
75
- nOptions.noModelHook = true
76
75
  nOptions.noHook = true
77
76
  nOptions.noDynHook = true
78
77
  nOptions.noValidation = true
@@ -417,17 +416,3 @@ export async function clearCache (id) {
417
416
  await clear({ key: `dobo|${this.name}|findAllRecord` })
418
417
  await clear({ key: `dobo|${this.name}|findOneRecord` })
419
418
  }
420
-
421
- export async function buildPropValues (prop, opts) {
422
- const { isString, camelCase } = this.app.lib._
423
- const { callHandler } = this.app.bajo
424
- const values = (isString(prop.values) ? await callHandler(prop.values) : [...prop.values]).map(v => {
425
- if (isString(v)) v = { value: v, text: v }
426
- if (opts.req) {
427
- const key = camelCase(`${prop.name} ${v.text}`)
428
- if (opts.req.te(key)) v.text = opts.req.t(key)
429
- }
430
- return v
431
- })
432
- return values
433
- }
@@ -1,14 +1,14 @@
1
1
  import path from 'path'
2
2
 
3
- async function exec ({ item, spinner, options, result, items } = {}) {
3
+ async function exec ({ body, spinner, options, result, bodies } = {}) {
4
4
  const { isArray, isString } = this.app.lib._
5
5
  const { getPluginDataDir } = this.app.bajo
6
6
  const { fs } = this.app.lib
7
7
 
8
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) {
9
+ const resp = await this.createRecord(body, { ...options, trx })
10
+ if (isArray(body._attachments) && body._attachments.length > 0) {
11
+ for (let att of body._attachments) {
12
12
  if (isString(att)) att = { field: 'file', file: att }
13
13
  const fname = path.basename(att.file)
14
14
  if (fs.existsSync(att.file)) {
@@ -21,19 +21,18 @@ async function exec ({ item, spinner, options, result, items } = {}) {
21
21
  }
22
22
  })
23
23
  result.success++
24
- if (spinner) spinner.setText('recordsAdded%s%d%d', this.name, result.success, items.length)
24
+ if (spinner) spinner.setText('recordsAdded%s%d%d', this.name, result.success, bodies.length)
25
25
  }
26
26
 
27
27
  async function loadFixtures ({ spinner, ignoreError = true, collectItems = false, noLookup = false } = {}, options = {}) {
28
28
  const { readConfig } = this.app.bajo
29
- const { isSet } = this.app.lib.aneka
30
- const { isEmpty, isString, isArray, pullAt } = this.app.lib._
29
+ const { isEmpty } = this.app.lib._
31
30
  if (this.connection.proxy) {
32
31
  this.log.warn('proxiedConnBound%s', this.name)
33
32
  return
34
33
  }
35
34
  const result = { success: 0, failed: 0 }
36
- const items = await readConfig(`${this.plugin.ns}:/extend/dobo/fixture/${this.baseName}.*`, { ns: this.plugin.ns, baseNs: 'dobo', checkOverride: true, defValue: [] })
35
+ const bodies = await readConfig(`${this.plugin.ns}:/extend/dobo/fixture/${this.baseName}.*`, { ns: this.plugin.ns, baseNs: 'dobo', checkOverride: true, defValue: [] })
37
36
  const opts = {
38
37
  ...options,
39
38
  noModelHook: false,
@@ -42,49 +41,22 @@ async function loadFixtures ({ spinner, ignoreError = true, collectItems = false
42
41
  noValidation: false,
43
42
  noCache: true
44
43
  }
45
- for (const item of items) {
46
- const lv = {}
47
- const deleted = {}
48
- for (const key in item) {
49
- const val = item[key]
50
- deleted[key] = deleted[key] ?? []
51
- if (!noLookup) {
52
- if (isString(val) && val.slice(0, 2) === '?:') {
53
- item[key] = await this._simpleLookup(val.slice(2), lv, opts)
54
- lv[key] = item[key]
55
- } else if (isArray(val)) {
56
- for (const idx in val) {
57
- if (isString(val[idx]) && val[idx].slice(0, 2) === '?:') {
58
- item[key][idx] = await this._simpleLookup(val[idx].slice(2), lv, opts)
59
- if (isSet(item[key][idx])) item[key][idx] += ''
60
- else deleted[key].push(idx)
61
- lv[`${key}.${idx}`] = item[key][idx]
62
- }
63
- }
64
- if (deleted[key].length > 0) pullAt(item[key], deleted[key])
65
- }
66
- }
67
- delete deleted[key]
68
- if (val === null) item[key] = undefined
69
- else {
70
- const prop = this.properties.find(item => item.name === key)
71
- if (prop && ['string', 'text'].includes(prop.type)) item[key] += ''
72
- }
73
- }
44
+ for (const body of bodies) {
45
+ await this.sanitizeFixture({ body, noLookup }, options)
74
46
  }
75
- if (collectItems) return items
76
- if (isEmpty(items)) return result
77
- for (const item of items) {
47
+ if (collectItems) return bodies
48
+ if (isEmpty(bodies)) return result
49
+ for (const body of bodies) {
78
50
  if (ignoreError) {
79
51
  try {
80
- await exec.call(this, { item, spinner, options: opts, result, items })
52
+ await exec.call(this, { body, spinner, options: opts, result, bodies })
81
53
  } catch (err) {
82
54
  if (this.app.bajo.config.log.applet) console.error(err)
83
55
  err.model = this.name
84
56
  if (this.app.applet) this.plugin.print.fail(this.app.dobo.validationErrorMessage(err))
85
57
  result.failed++
86
58
  }
87
- } else await exec.call(this, { item, spinner, options: opts, result, items })
59
+ } else await exec.call(this, { body, spinner, options: opts, result, bodies })
88
60
  }
89
61
  return result
90
62
  }
@@ -0,0 +1,43 @@
1
+ async function sanitizeFixture ({ body = {}, lookupValue = {}, noLookup } = {}, options = {}) {
2
+ const { isString, isArray, pullAt, cloneDeep } = this.app.lib._
3
+ const { isSet } = this.app.lib.aneka
4
+ const lv = cloneDeep(lookupValue)
5
+ const deleted = {}
6
+ const opts = {
7
+ ...options,
8
+ noModelHook: false,
9
+ noHook: true,
10
+ noDynHook: true,
11
+ noValidation: false,
12
+ noCache: true
13
+ }
14
+ for (const key in body) {
15
+ const val = body[key]
16
+ deleted[key] = deleted[key] ?? []
17
+ if (!noLookup) {
18
+ if (isString(val) && val.slice(0, 2) === '?:') {
19
+ body[key] = await this._simpleLookup(val.slice(2), lv, opts)
20
+ lv[key] = body[key]
21
+ } else if (isArray(val)) {
22
+ for (const idx in val) {
23
+ if (isString(val[idx]) && val[idx].slice(0, 2) === '?:') {
24
+ body[key][idx] = await this._simpleLookup(val[idx].slice(2), lv, opts)
25
+ if (isSet(body[key][idx])) body[key][idx] += ''
26
+ else deleted[key].push(idx)
27
+ lv[`${key}.${idx}`] = body[key][idx]
28
+ }
29
+ }
30
+ if (deleted[key].length > 0) pullAt(body[key], deleted[key])
31
+ }
32
+ }
33
+ delete deleted[key]
34
+ if (val === null) body[key] = undefined
35
+ else {
36
+ const prop = this.properties.find(item => item.name === key)
37
+ if (prop && ['string', 'text'].includes(prop.type)) body[key] += ''
38
+ }
39
+ }
40
+ return body
41
+ }
42
+
43
+ export default sanitizeFixture
@@ -1,5 +1,3 @@
1
- import { buildPropValues } from './_util.js'
2
-
3
1
  /**
4
2
  * Sanitize record to conform with the model's definition
5
3
  *
@@ -41,7 +39,7 @@ async function sanitizeRecord (record = {}, opts = {}) {
41
39
  if (!prop) continue
42
40
  let value = ['object', 'array'].includes(prop.type) ? cloneDeep(newRecord[key]) : newRecord[key]
43
41
  if (prop.values) {
44
- const values = await buildPropValues.call(this, prop, opts)
42
+ const values = await this.buildPropValues(prop, opts)
45
43
  value = (values.find(v => v.value === value) ?? {}).text ?? value
46
44
  }
47
45
  if (prop.format === false) newRecord._fmt[key] = value + ''
@@ -86,16 +86,17 @@ const validator = {
86
86
  'sign', 'unsafe'],
87
87
  boolean: ['falsy', 'sensitive', 'truthy'],
88
88
  date: ['greater', 'iso', 'less', 'max', 'min'],
89
- timestamp: ['timestamp']
89
+ timestamp: ['timestamp'],
90
+ array: ['length', 'max', 'min']
90
91
  }
91
92
 
92
93
  async function buildFromDbModel (opts = {}) {
93
94
  const { isPlainObject, get, isEmpty, isString, keys, find, has, without } = this.app.lib._
94
- const { callHandler } = this.app.bajo
95
95
  const { fields = [], rule = {}, extFields = [] } = opts
96
96
  const obj = {}
97
97
  const { propertyType: propType } = this.app.baseClass.Dobo
98
98
  const refs = []
99
+ const me = this
99
100
 
100
101
  function getRuleKv (kvRule) {
101
102
  let key
@@ -136,12 +137,8 @@ async function buildFromDbModel (opts = {}) {
136
137
  }
137
138
  if (!['id'].includes(prop.name) && prop.required) obj = obj.required()
138
139
  if (prop.values) {
139
- let items = []
140
- if (Array.isArray(prop.values)) items = prop.values.map(item => item.value)
141
- else if (typeof prop.values === 'string') {
142
- const resp = await callHandler(prop.values)
143
- items = resp.map(item => item.value)
144
- }
140
+ const values = await me.buildPropValues(prop, opts)
141
+ const items = values.map(item => item.value)
145
142
  if (prop.type === 'array') {
146
143
  obj = obj.items(joi.string().valid(...items))
147
144
  } else obj = obj.valid(...items)
@@ -22,6 +22,7 @@ import findAttachment from './model/find-attachment.js'
22
22
  import sanitizeBody from './model/sanitize-body.js'
23
23
  import sanitizeRecord from './model/sanitize-record.js'
24
24
  import sanitizeId from './model/sanitize-id.js'
25
+ import sanitizeFixture from './model/sanitize-fixture.js'
25
26
  import upsertRecord from './model/upsert-record.js'
26
27
  import bulkCreateRecord from './model/bulk-create-record.js'
27
28
  import listAttachment from './model/list-attachment.js'
@@ -147,6 +148,20 @@ async function modelFactory () {
147
148
  return get(rec, field, null)
148
149
  }
149
150
 
151
+ buildPropValues = async (prop, opts) => {
152
+ const { isString, camelCase, isFunction } = this.app.lib._
153
+ const { callHandler } = this.app.bajo
154
+ const values = isString(prop.values) || isFunction(prop.values) ? await callHandler(this, prop.values, opts) : [...prop.values]
155
+ return values.map(v => {
156
+ if (isString(v)) v = { value: v, text: v }
157
+ if (opts.req) {
158
+ const key = camelCase(`${prop.name} ${v.text}`)
159
+ if (opts.req.te(key)) v.text = opts.req.t(key)
160
+ }
161
+ return v
162
+ })
163
+ }
164
+
150
165
  build = build
151
166
  exists = exists
152
167
  drop = drop
@@ -179,6 +194,7 @@ async function modelFactory () {
179
194
  sanitizeRecord = sanitizeRecord
180
195
  sanitizeBody = sanitizeBody
181
196
  sanitizeId = sanitizeId
197
+ sanitizeFixture = sanitizeFixture
182
198
  validate = validate
183
199
 
184
200
  // aliases
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dobo",
3
- "version": "2.27.2",
3
+ "version": "2.29.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-06-10
4
+
5
+ - [2.29.0] Feature ```dobo:immutable``` now using array instead of boolean
6
+ - [2.29.0] Add ```model.sanitizeFixture()```
7
+ - [2.29.0] Options ```noMagic``` no without touching ```noModelHook```
8
+ - [2.29.0] Add ```array``` validator type
9
+
10
+ ## 2026-06-03
11
+
12
+ - [2.28.0] Property ```values``` can now accept a function that will be called dynamically upon used
13
+ - [2.28.0] ```_util.buildPropValues()``` now become a new model method
14
+ - [2.28.0] Bug fix in ```model.sanitizeRecord()```
15
+ - [2.28.0] Bug fix in ```model.validate()```
16
+
3
17
  ## 2026-05-30
4
18
 
5
19
  - [2.27.0] Bug fix in ```model.transaction()```