dobo 2.28.0 → 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',
@@ -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
@@ -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
@@ -86,7 +86,8 @@ 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 = {}) {
@@ -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'
@@ -193,6 +194,7 @@ async function modelFactory () {
193
194
  sanitizeRecord = sanitizeRecord
194
195
  sanitizeBody = sanitizeBody
195
196
  sanitizeId = sanitizeId
197
+ sanitizeFixture = sanitizeFixture
196
198
  validate = validate
197
199
 
198
200
  // aliases
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dobo",
3
- "version": "2.28.0",
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,12 @@
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
+
3
10
  ## 2026-06-03
4
11
 
5
12
  - [2.28.0] Property ```values``` can now accept a function that will be called dynamically upon used