dobo 2.15.0 → 2.17.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/bajo/intl/en-US.json +1 -0
- package/extend/bajo/intl/id.json +1 -0
- package/extend/dobo/feature/immutable.js +4 -3
- package/index.js +12 -11
- package/lib/collect-models.js +8 -3
- package/lib/factory/model/_util.js +26 -21
- package/lib/factory/model/sanitize-body.js +1 -1
- package/lib/factory/model/sanitize-record.js +21 -1
- package/lib/factory/model/update-record.js +1 -0
- package/package.json +1 -1
- package/wiki/CHANGES.md +14 -0
|
@@ -148,6 +148,7 @@
|
|
|
148
148
|
"maxLimitWarning%s%s": "Records per page (%s rows) above the allowed threshold (%s rows)",
|
|
149
149
|
"hardCapWarning%s%s": "Max records returned (%s rows) above the allowed threshold (%s rows)",
|
|
150
150
|
"maxPageError%s%s": "Page number (%s) above the allowed threshold (%s)",
|
|
151
|
+
"duplicateRefKeys%s%s": "Duplicate reference keys found in '%s' (%s)",
|
|
151
152
|
"field": {
|
|
152
153
|
"id": "ID",
|
|
153
154
|
"code": "Kode",
|
package/extend/bajo/intl/id.json
CHANGED
|
@@ -146,6 +146,7 @@
|
|
|
146
146
|
"maxLimitWarning%s%s": "Data per halaman (%s baris) melampaui batas yang diijinkan (%s baris)",
|
|
147
147
|
"hardCapWarning%s%s": "Maksimum data yang dihasilkan (%s baris) melampaui batas yang diijinkan (%s baris)",
|
|
148
148
|
"maxPageError%s%s": "Nomor halaman (%s) melampaui batas yang diijinkan (%s)",
|
|
149
|
+
"duplicateRefKeys%s%s": "Ditemukan kunci referensi duplikat di '%s' (%s)",
|
|
149
150
|
"field": {
|
|
150
151
|
"id": "ID",
|
|
151
152
|
"code": "Kode",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
async function beforeRemoveRecord (id, opts) {
|
|
1
|
+
async function beforeRemoveRecord (id, opts, options) {
|
|
2
2
|
const { get } = this.app.lib._
|
|
3
|
+
if (get(options, 'req.user.interSiteAdmin')) return
|
|
3
4
|
const record = await this.driver.getRecord(this, id)
|
|
4
5
|
const immutable = get(record.data, opts.field)
|
|
5
6
|
if (immutable) throw this.plugin.error('recordImmutable%s%s', id, this.name, { statusCode: 423 })
|
|
@@ -16,12 +17,12 @@ async function immutable (opts = {}) {
|
|
|
16
17
|
hooks: [{
|
|
17
18
|
name: 'beforeUpdateRecord',
|
|
18
19
|
handler: async function (id, body, options) {
|
|
19
|
-
await beforeRemoveRecord.call(this, id, opts)
|
|
20
|
+
await beforeRemoveRecord.call(this, id, opts, options)
|
|
20
21
|
}
|
|
21
22
|
}, {
|
|
22
23
|
name: 'beforeRemoveRecord',
|
|
23
24
|
handler: async function (id, options) {
|
|
24
|
-
await beforeRemoveRecord.call(this, id, opts)
|
|
25
|
+
await beforeRemoveRecord.call(this, id, opts, options)
|
|
25
26
|
}
|
|
26
27
|
}]
|
|
27
28
|
}
|
package/index.js
CHANGED
|
@@ -82,7 +82,7 @@ const propertyType = {
|
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
const commonPropertyTypes = ['name', 'type', 'required', 'rules', 'validator', 'ref', 'default', 'values', 'rulesMsg', 'immutable', 'feature']
|
|
85
|
+
const commonPropertyTypes = ['name', 'type', 'required', 'rules', 'validator', 'ref', 'default', 'values', 'rulesMsg', 'immutable', 'feature', 'format']
|
|
86
86
|
|
|
87
87
|
/**
|
|
88
88
|
* Plugin factory
|
|
@@ -390,17 +390,21 @@ async function factory (pkgName) {
|
|
|
390
390
|
return parseInt(value) || null
|
|
391
391
|
}
|
|
392
392
|
|
|
393
|
-
sanitizeObject = (value) => {
|
|
393
|
+
sanitizeObject = (value, { strict = false } = {}) => {
|
|
394
394
|
const { isString } = this.app.lib._
|
|
395
395
|
let result = null
|
|
396
396
|
if (isString(value)) {
|
|
397
397
|
try {
|
|
398
398
|
result = JSON.parse(value)
|
|
399
|
-
} catch (err) {
|
|
399
|
+
} catch (err) {
|
|
400
|
+
if (strict) throw err
|
|
401
|
+
}
|
|
400
402
|
} else {
|
|
401
403
|
try {
|
|
402
404
|
result = JSON.parse(JSON.stringify(value))
|
|
403
|
-
} catch (err) {
|
|
405
|
+
} catch (err) {
|
|
406
|
+
if (strict) throw err
|
|
407
|
+
}
|
|
404
408
|
}
|
|
405
409
|
return result
|
|
406
410
|
}
|
|
@@ -497,13 +501,10 @@ async function factory (pkgName) {
|
|
|
497
501
|
}
|
|
498
502
|
|
|
499
503
|
getDefaultValues = (options = {}) => {
|
|
500
|
-
const
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
const
|
|
504
|
-
const maxPage = get(options, 'req.site.setting.dobo.default.filter.maxPage', config.default.filter.maxPage)
|
|
505
|
-
const hardCap = get(options, 'req.site.setting.dobo.default.hardCap', config.default.hardCap)
|
|
506
|
-
const warnings = get(options, 'req.site.setting.dobo.default.warnings', config.default.warnings)
|
|
504
|
+
const key = 'default.filter'
|
|
505
|
+
let config = this.app.dobo.getConfig(key)
|
|
506
|
+
if (options.req) config = options.req.getSetting(`dobo:${key}`, config)
|
|
507
|
+
const { limit, maxLimit, maxPage, hardCap, warnings } = config
|
|
507
508
|
const t = options.req ? options.req.t : this.t
|
|
508
509
|
return { limit, maxLimit, hardCap, maxPage, warnings, t }
|
|
509
510
|
}
|
package/lib/collect-models.js
CHANGED
|
@@ -39,7 +39,7 @@ async function sanitizeProp (model, prop, indexes) {
|
|
|
39
39
|
else {
|
|
40
40
|
const feature = this.getFeature(prop.type)
|
|
41
41
|
if (!feature) this.fatal('unknownPropType%s%s', prop.type, model.name)
|
|
42
|
-
await applyFeature.call(this, model, feature, {
|
|
42
|
+
await applyFeature.call(this, model, feature, { field: prop.name }, indexes)
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
|
|
@@ -80,6 +80,7 @@ async function applyFeature (model, feature, options, indexes, isExtender) {
|
|
|
80
80
|
}
|
|
81
81
|
const item = await feature.handler(options)
|
|
82
82
|
if (item.rules) model.rules.push(...item.rules)
|
|
83
|
+
if (item.scanables) model.scanables.push(...item.scanables)
|
|
83
84
|
if (!isArray(item.properties)) item.properties = [item.properties]
|
|
84
85
|
for (const prop of item.properties) {
|
|
85
86
|
prop.feature = `${feature.plugin.ns}:${feature.name}`
|
|
@@ -137,16 +138,18 @@ async function findAllIndexes (model, inputs = [], indexes = []) {
|
|
|
137
138
|
export async function sanitizeRef (model, models) {
|
|
138
139
|
const { find, isString, pullAt } = this.app.lib._
|
|
139
140
|
if (!models) models = this.models
|
|
141
|
+
const _refKeys = []
|
|
140
142
|
for (const prop of model.properties) {
|
|
141
143
|
const ignored = []
|
|
142
144
|
for (const key in prop.ref ?? {}) {
|
|
145
|
+
_refKeys.push(key)
|
|
143
146
|
let ref = prop.ref[key]
|
|
144
147
|
if (isString(ref)) {
|
|
145
148
|
ref = { field: ref }
|
|
146
149
|
}
|
|
147
150
|
ref.field = ref.field ?? 'id'
|
|
148
151
|
ref.type = ref.type ?? '1:1'
|
|
149
|
-
ref.searchField = ref.searchField ?? model.scanables[0] ??
|
|
152
|
+
ref.searchField = ref.searchField ?? model.scanables[0] ?? ref.field
|
|
150
153
|
ref.labelField = ref.labelField ?? ref.searchField
|
|
151
154
|
const rModel = find(models, { name: ref.model })
|
|
152
155
|
if (!rModel) {
|
|
@@ -175,6 +178,8 @@ export async function sanitizeRef (model, models) {
|
|
|
175
178
|
delete prop.ref[key]
|
|
176
179
|
}
|
|
177
180
|
}
|
|
181
|
+
const dupes = _refKeys.filter((item, index) => _refKeys.indexOf(item) !== index)
|
|
182
|
+
if (dupes.length > 0) throw this.error('duplicateRefKeys%s%s', model.name, dupes.join(', '))
|
|
178
183
|
}
|
|
179
184
|
|
|
180
185
|
/**
|
|
@@ -336,7 +341,7 @@ async function collectModels () {
|
|
|
336
341
|
me.models.push(model)
|
|
337
342
|
}
|
|
338
343
|
for (const model of me.models) {
|
|
339
|
-
await sanitizeRef.call(this, model, me.models
|
|
344
|
+
await sanitizeRef.call(this, model, me.models)
|
|
340
345
|
me.log.trace('- %s', model.name)
|
|
341
346
|
}
|
|
342
347
|
this.log.debug('collected%s%d', this.t('model'), this.models.length)
|
|
@@ -161,10 +161,21 @@ export async function handleAttachmentUpload (id, trigger, options = {}) {
|
|
|
161
161
|
return copyAttachment.call(this, id, { req, mimeType, stats, setFile, setField })
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
+
async function _getRef ({ ref, rModel, prop, key, options, filter } = {}) {
|
|
165
|
+
if (!((typeof options.refs === 'string' && ['*', 'all'].includes(options.refs)) || options.refs.includes(key))) return
|
|
166
|
+
if (ref.fields.length === 0) return
|
|
167
|
+
const { formatValue, retainOriginalValue } = options
|
|
168
|
+
const fields = [...ref.fields]
|
|
169
|
+
if (!fields.includes(prop.name)) fields.push(prop.name)
|
|
170
|
+
const rOptions = { dataOnly: true, refs: [], formatValue, retainOriginalValue, fields }
|
|
171
|
+
const results = await rModel.findRecord(filter, rOptions)
|
|
172
|
+
return { rOptions, results }
|
|
173
|
+
}
|
|
174
|
+
|
|
164
175
|
export async function getSingleRef (record = {}, options = {}) {
|
|
165
176
|
const { isSet } = this.app.lib.aneka
|
|
166
|
-
const { get } = this.app.lib._
|
|
167
177
|
const { parseQuery } = this.app.dobo
|
|
178
|
+
const { get } = this.app.lib._
|
|
168
179
|
const props = this.properties.filter(p => isSet(p.ref) && !(options.hidden ?? []).includes(p.name))
|
|
169
180
|
const refs = {}
|
|
170
181
|
options.refs = options.refs ?? []
|
|
@@ -172,23 +183,21 @@ export async function getSingleRef (record = {}, options = {}) {
|
|
|
172
183
|
for (const prop of props) {
|
|
173
184
|
for (const key in prop.ref) {
|
|
174
185
|
try {
|
|
175
|
-
if (!((typeof options.refs === 'string' && ['*', 'all'].includes(options.refs)) || options.refs.includes(key))) continue
|
|
176
|
-
const ref = prop.ref[key]
|
|
177
|
-
if (ref.fields.length === 0) continue
|
|
178
186
|
if (get(record, `_ref.${key}`)) continue
|
|
187
|
+
const ref = prop.ref[key]
|
|
179
188
|
const rModel = this.app.dobo.getModel(ref.model, true)
|
|
180
|
-
if (!rModel)
|
|
189
|
+
if (!rModel) return
|
|
181
190
|
let query = {}
|
|
182
191
|
query[ref.field] = record[prop.name]
|
|
183
192
|
if (ref.field === 'id') query[ref.field] = this.sanitizeId(query[ref.field])
|
|
184
193
|
if (ref.query) query = { $and: [query, parseQuery(ref.query)] }
|
|
185
|
-
const
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
const
|
|
194
|
+
const filter = { query }
|
|
195
|
+
const resp = await _getRef.call(this, { ref, rModel, prop, key, options, filter })
|
|
196
|
+
if (!resp) continue
|
|
197
|
+
const { rOptions, results } = resp
|
|
189
198
|
const data = []
|
|
190
199
|
for (const res of results) {
|
|
191
|
-
data.push(await rModel.sanitizeRecord(res,
|
|
200
|
+
data.push(await rModel.sanitizeRecord(res, rOptions))
|
|
192
201
|
}
|
|
193
202
|
refs[key] = ['1:1'].includes(ref.type) ? data[0] : data
|
|
194
203
|
} catch (err) {}
|
|
@@ -208,13 +217,10 @@ export async function getMultiRefs (records = [], options = {}) {
|
|
|
208
217
|
for (const prop of props) {
|
|
209
218
|
for (const key in prop.ref) {
|
|
210
219
|
try {
|
|
211
|
-
if (!((typeof options.refs === 'string' && ['*', 'all'].includes(options.refs)) || options.refs.includes(key))) continue
|
|
212
|
-
const ref = prop.ref[key]
|
|
213
|
-
if (ref.fields.length === 0) continue
|
|
214
|
-
if (ref.type !== '1:1') continue
|
|
215
220
|
if (get(records, `0._ref.${key}`)) continue
|
|
221
|
+
const ref = prop.ref[key]
|
|
216
222
|
const rModel = this.app.dobo.getModel(ref.model, true)
|
|
217
|
-
if (!rModel)
|
|
223
|
+
if (!rModel) return
|
|
218
224
|
let matches = []
|
|
219
225
|
for (const r of records) {
|
|
220
226
|
matches.push(rModel.sanitizeId(r[prop.name]))
|
|
@@ -223,16 +229,15 @@ export async function getMultiRefs (records = [], options = {}) {
|
|
|
223
229
|
let query = {}
|
|
224
230
|
query[ref.field] = { $in: matches }
|
|
225
231
|
if (ref.query) query = { $and: [query, parseQuery(ref.query)] }
|
|
226
|
-
const
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
const
|
|
230
|
-
if (!fields.includes(prop.name)) fields.push(prop.name)
|
|
232
|
+
const filter = { query, limit: matches.length }
|
|
233
|
+
const resp = await _getRef.call(this, { ref, rModel, prop, key, options, filter })
|
|
234
|
+
if (!resp) continue
|
|
235
|
+
const { rOptions, results } = resp
|
|
231
236
|
for (const i in records) {
|
|
232
237
|
records[i]._ref = records[i]._ref ?? {}
|
|
233
238
|
const rec = records[i]
|
|
234
239
|
const res = results.find(res => (res[ref.field] + '') === rec[prop.name] + '')
|
|
235
|
-
if (res) records[i]._ref[key] = await rModel.sanitizeRecord(res,
|
|
240
|
+
if (res) records[i]._ref[key] = await rModel.sanitizeRecord(res, rOptions)
|
|
236
241
|
else records[i]._ref[key] = {}
|
|
237
242
|
}
|
|
238
243
|
} catch (err) {}
|
|
@@ -21,7 +21,7 @@ async function sanitizeBody ({ body = {}, partial, strict, extFields = [], noDef
|
|
|
21
21
|
|
|
22
22
|
const sanitize = (name, type) => {
|
|
23
23
|
if (onlyTypes.length > 0 && !onlyTypes.includes(type)) return
|
|
24
|
-
if (['object', 'array'].includes(type)) result[name] = sanitizeObject(result[name])
|
|
24
|
+
if (['object', 'array'].includes(type)) result[name] = sanitizeObject(result[name], { strict })
|
|
25
25
|
else if (type === 'boolean') result[name] = sanitizeBoolean(result[name])
|
|
26
26
|
else if (['float', 'double'].includes(type)) result[name] = sanitizeFloat(result[name], { strict })
|
|
27
27
|
else if (['integer', 'smallint'].includes(type)) result[name] = sanitizeInt(result[name], { strict })
|
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
*/
|
|
12
12
|
async function sanitizeRecord (record = {}, opts = {}) {
|
|
13
13
|
const { fields = [], hidden = [], forceNoHidden } = opts
|
|
14
|
-
const { isEmpty, map, without, isArray } = this.app.lib._
|
|
14
|
+
const { isEmpty, map, without, isArray, isFunction, isString, get, cloneDeep } = this.app.lib._
|
|
15
15
|
const { fillObject } = this.app.lib.aneka
|
|
16
|
+
const { callHandler, format } = this.app.bajo
|
|
16
17
|
let allHidden = without([...this.hidden, ...hidden], 'id')
|
|
17
18
|
if (forceNoHidden === true) allHidden = []
|
|
18
19
|
else if (isArray(forceNoHidden)) allHidden = without(allHidden, ...forceNoHidden)
|
|
@@ -22,6 +23,25 @@ async function sanitizeRecord (record = {}, opts = {}) {
|
|
|
22
23
|
newFields = without(newFields, ...allHidden)
|
|
23
24
|
const body = fillObject(record, newFields, null)
|
|
24
25
|
const newRecord = await this.sanitizeBody({ body, noDefault: true })
|
|
26
|
+
if (opts.formatValue) {
|
|
27
|
+
if (opts.retainOriginalValue) newRecord._orig = cloneDeep(newRecord)
|
|
28
|
+
for (const key in newRecord) {
|
|
29
|
+
const prop = this.getProperty(key)
|
|
30
|
+
if (!prop) continue
|
|
31
|
+
const value = ['object', 'array'].includes(prop.type) ? cloneDeep(newRecord[key]) : newRecord[key]
|
|
32
|
+
if (prop.format) {
|
|
33
|
+
if (isFunction(prop.format)) newRecord[key] = await prop.format.call(this, value, newRecord, opts)
|
|
34
|
+
else if (isString(prop.format)) newRecord[key] = await callHandler(this.plugin, this, value, newRecord, opts)
|
|
35
|
+
} else {
|
|
36
|
+
const options = {
|
|
37
|
+
lang: get(opts, 'req.lang'),
|
|
38
|
+
latitude: ['lat', 'latitude'].includes(key),
|
|
39
|
+
longitude: ['lon', 'lng', 'longitude'].includes(key)
|
|
40
|
+
}
|
|
41
|
+
newRecord[key] = format(value, prop.type, options)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
25
45
|
if (record._ref) newRecord._ref = record._ref
|
|
26
46
|
return newRecord
|
|
27
47
|
}
|
|
@@ -57,6 +57,7 @@ async function updateRecord (...args) {
|
|
|
57
57
|
const { truncateString, noResult, noBodySanitizer, noResultSanitizer, noValidation, partial } = options
|
|
58
58
|
const extFields = get(options, 'validation.extFields', [])
|
|
59
59
|
id = this.sanitizeId(id)
|
|
60
|
+
|
|
60
61
|
let input = noBodySanitizer ? cloneDeep(body) : await this.sanitizeBody({ body, extFields, strict: true, truncateString, partial, onlyTypes })
|
|
61
62
|
const immutables = this.properties.filter(prop => prop.immutable)
|
|
62
63
|
if (immutables.length > 0) input = omit(input, immutables)
|
package/package.json
CHANGED
package/wiki/CHANGES.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changes
|
|
2
2
|
|
|
3
|
+
## 2026-04-13
|
|
4
|
+
|
|
5
|
+
- [2.17.0] Add ```{ strict }``` as parameter to ```sanitizeObject()```
|
|
6
|
+
|
|
7
|
+
## 2026-04-11
|
|
8
|
+
|
|
9
|
+
- [2.16.0] Add ```format``` as new model's property key
|
|
10
|
+
- [2.16.0] Rewrite ```getDefaultValues()``` to base on ```req.getSetting()```
|
|
11
|
+
- [2.16.0] All inter site admins are now exempts from ```immutable``` row
|
|
12
|
+
- [2.16.0] Bug fix in ```collect-models.js```
|
|
13
|
+
- [2.16.0] Bug fix in ```getSingleRef()``` and ```getMultiRefs()```
|
|
14
|
+
- [2.16.0] Add feature to return formatted row(s) with ```options.formatValue```
|
|
15
|
+
- [2.16.0] If row is formatted, add feature to save original row in ```_orig``` with ```options.retainOriginalValue```
|
|
16
|
+
|
|
3
17
|
## 2026-04-07
|
|
4
18
|
|
|
5
19
|
- [2.15.0] Add ```parseNql()```
|