dobo 2.28.0 → 2.29.1
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/dobo/feature/immutable.js +17 -3
- package/lib/factory/model/_util.js +0 -1
- package/lib/factory/model/load-fixtures.js +14 -42
- package/lib/factory/model/sanitize-body.js +2 -2
- package/lib/factory/model/sanitize-fixture.js +43 -0
- package/lib/factory/model/validate.js +2 -1
- package/lib/factory/model.js +2 -0
- package/package.json +1 -1
- package/wiki/CHANGES.md +11 -0
|
@@ -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: '
|
|
27
|
+
type: 'array',
|
|
28
|
+
default: []
|
|
15
29
|
},
|
|
16
30
|
hooks: [{
|
|
17
31
|
name: 'beforeUpdateRecord',
|
|
18
32
|
handler: async function (id, body, options) {
|
|
19
|
-
await
|
|
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 ({
|
|
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(
|
|
10
|
-
if (isArray(
|
|
11
|
-
for (let att of
|
|
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,
|
|
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 {
|
|
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
|
|
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
|
|
46
|
-
|
|
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
|
|
76
|
-
if (isEmpty(
|
|
77
|
-
for (const
|
|
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, {
|
|
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, {
|
|
59
|
+
} else await exec.call(this, { body, spinner, options: opts, result, bodies })
|
|
88
60
|
}
|
|
89
61
|
return result
|
|
90
62
|
}
|
|
@@ -33,11 +33,11 @@ async function sanitizeBody ({ body = {}, partial, strict, extFields = [], noDef
|
|
|
33
33
|
|
|
34
34
|
const omitted = []
|
|
35
35
|
const details = []
|
|
36
|
-
const properties = [...this.
|
|
36
|
+
const properties = [...this.getNonVirtualProperties(), ...extFields]
|
|
37
37
|
for (const prop of properties) {
|
|
38
38
|
try {
|
|
39
39
|
if (partial && !has(body, prop.name)) {
|
|
40
|
-
if (prop.type === 'array') result[prop.name] = null
|
|
40
|
+
// if (prop.type === 'array') result[prop.name] = null
|
|
41
41
|
continue
|
|
42
42
|
}
|
|
43
43
|
result[prop.name] = body[prop.name]
|
|
@@ -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 = {}) {
|
package/lib/factory/model.js
CHANGED
|
@@ -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
package/wiki/CHANGES.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changes
|
|
2
2
|
|
|
3
|
+
## 2026-06-11
|
|
4
|
+
|
|
5
|
+
- [2.29.1] Bug fix in ```model.sanitizeBody()```
|
|
6
|
+
|
|
7
|
+
## 2026-06-10
|
|
8
|
+
|
|
9
|
+
- [2.29.0] Feature ```dobo:immutable``` now using array instead of boolean
|
|
10
|
+
- [2.29.0] Add ```model.sanitizeFixture()```
|
|
11
|
+
- [2.29.0] Options ```noMagic``` no without touching ```noModelHook```
|
|
12
|
+
- [2.29.0] Add ```array``` validator type
|
|
13
|
+
|
|
3
14
|
## 2026-06-03
|
|
4
15
|
|
|
5
16
|
- [2.28.0] Property ```values``` can now accept a function that will be called dynamically upon used
|