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.
- package/extend/dobo/feature/immutable.js +17 -3
- package/lib/collect-models.js +13 -20
- package/lib/factory/model/_util.js +0 -15
- package/lib/factory/model/load-fixtures.js +14 -42
- package/lib/factory/model/sanitize-fixture.js +43 -0
- package/lib/factory/model/sanitize-record.js +1 -3
- package/lib/factory/model/validate.js +5 -8
- package/lib/factory/model.js +16 -0
- package/package.json +1 -1
- package/wiki/CHANGES.md +14 -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',
|
package/lib/collect-models.js
CHANGED
|
@@ -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
|
|
348
|
-
|
|
349
|
-
|
|
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 =
|
|
358
|
-
if (!prop || (prop && prop.virtual)) throw this.error('virtualFieldIn%s%s%s', field, 'index',
|
|
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
|
|
362
|
-
const prop =
|
|
363
|
-
if (!prop || (prop && prop.virtual)) throw this.error('virtualFieldIn%s%s%s', field, 'scanable',
|
|
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(
|
|
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 ({
|
|
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
|
}
|
|
@@ -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
|
|
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
|
-
|
|
140
|
-
|
|
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)
|
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'
|
|
@@ -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
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()```
|