dobo 2.15.0 → 2.16.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.
@@ -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",
@@ -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
@@ -497,13 +497,10 @@ async function factory (pkgName) {
497
497
  }
498
498
 
499
499
  getDefaultValues = (options = {}) => {
500
- const { get } = this.app.lib._
501
- const config = this.app.dobo.config
502
- const limit = get(options, 'req.site.setting.dobo.default.filter.limit', config.default.filter.limit)
503
- const maxLimit = get(options, 'req.site.setting.dobo.default.filter.maxLimit', config.default.filter.maxLimit)
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)
500
+ const key = 'default.filter'
501
+ let config = this.app.dobo.getConfig(key)
502
+ if (options.req) config = options.req.getSetting(`dobo:${key}`, config)
503
+ const { limit, maxLimit, maxPage, hardCap, warnings } = config
507
504
  const t = options.req ? options.req.t : this.t
508
505
  return { limit, maxLimit, hardCap, maxPage, warnings, t }
509
506
  }
@@ -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, { name: prop.name }, indexes)
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] ?? 'id'
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, true)
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) continue
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 rFilter = { query }
186
- const rOptions = { dataOnly: true, refs: [] }
187
- const results = await rModel.findRecord(rFilter, rOptions)
188
- const fields = [...ref.fields]
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, { fields }))
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) continue
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 rFilter = { query, limit: matches.length }
227
- const rOptions = { dataOnly: true, refs: [] }
228
- const results = await rModel.findRecord(rFilter, rOptions)
229
- const fields = [...ref.fields]
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, { fields })
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) {}
@@ -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,24 @@ 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
+ if (prop.format) {
32
+ if (isFunction(prop.format)) newRecord[key] = await prop.format.call(this, newRecord[key], newRecord, opts)
33
+ else if (isString(prop.format)) newRecord[key] = await callHandler(this.plugin, this, newRecord[key], newRecord, opts)
34
+ } else {
35
+ const options = {
36
+ lang: get(opts, 'req.lang'),
37
+ latitude: ['lat', 'latitude'].includes(key),
38
+ longitude: ['lon', 'lng', 'longitude'].includes(key)
39
+ }
40
+ newRecord[key] = format(newRecord[key], prop.type, options)
41
+ }
42
+ }
43
+ }
25
44
  if (record._ref) newRecord._ref = record._ref
26
45
  return newRecord
27
46
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dobo",
3
- "version": "2.15.0",
3
+ "version": "2.16.0",
4
4
  "description": "DBMS for Bajo Framework",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/wiki/CHANGES.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changes
2
2
 
3
+ ## 2026-04-11
4
+
5
+ - [2.16.0] Add ```format``` as new model's property key
6
+ - [2.16.0] Rewrite ```getDefaultValues()``` to base on ```req.getSetting()```
7
+ - [2.16.0] All inter site admins are now exempts from ```immutable``` row
8
+ - [2.16.0] Bug fix in ```collect-models.js```
9
+ - [2.16.0] Bug fix in ```getSingleRef()``` and ```getMultiRefs()```
10
+ - [2.16.0] Add feature to return formatted row(s) with ```options.formatValue```
11
+ - [2.16.0] If row is formatted, add feature to save original row in ```_orig``` with ```options.retainOriginalValue```
12
+
3
13
  ## 2026-04-07
4
14
 
5
15
  - [2.15.0] Add ```parseNql()```