dobo 2.6.6 → 2.8.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.
@@ -145,6 +145,9 @@
145
145
  "inMemoryDb%s%s": "'%s' is an in-memory database, %s is not allowed",
146
146
  "buildOp": "'build' operation",
147
147
  "dropOp": "'drop' operation",
148
+ "maxLimitWarning%s%s": "Records per page (%s rows) above the allowed threshold (%s rows)",
149
+ "hardLimitWarning%s%s": "Max records returned (%s rows) above the allowed threshold (%s rows)",
150
+ "maxPageError%s%s": "Page number (%s) above the allowed threshold (%s)",
148
151
  "field": {
149
152
  "id": "ID",
150
153
  "code": "Kode",
@@ -143,6 +143,9 @@
143
143
  "inMemoryDb%s%s": "'%s' adalah database in-memory, %s tidak diizinkan",
144
144
  "buildOp": "operasi 'build'",
145
145
  "dropOp": "operasi 'drop'",
146
+ "maxLimitWarning%s": "Data per halaman (%s baris) melampaui batas yang diijinkan (%s baris)",
147
+ "hardLimitWarning%s": "Maksimum data yang dihasilkan (%s baris) melampaui batas yang diijinkan (%s baris)",
148
+ "maxPageError%s%s": "Nomor halaman (%s) melampaui batas yang diijinkan (%s)",
146
149
  "field": {
147
150
  "id": "ID",
148
151
  "code": "Kode",
package/index.js CHANGED
@@ -81,7 +81,7 @@ const propertyType = {
81
81
  }
82
82
  }
83
83
 
84
- const commonPropertyTypes = ['name', 'type', 'required', 'rules', 'validator', 'ref', 'default']
84
+ const commonPropertyTypes = ['name', 'type', 'required', 'rules', 'validator', 'ref', 'default', 'values', 'rulesMsg']
85
85
 
86
86
  /**
87
87
  * Plugin factory
@@ -151,6 +151,7 @@ async function factory (pkgName) {
151
151
  limit: 25,
152
152
  maxLimit: 200,
153
153
  hardLimit: 10000,
154
+ maxPage: 50,
154
155
  sort: ['dt:-1', 'updatedAt:-1', 'updated_at:-1', 'createdAt:-1', 'createdAt:-1', 'ts:-1', 'username', 'name']
155
156
  }
156
157
  },
@@ -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 } = this.app.lib._
13
+ const { isEmpty, isString, keys, pick, isArray, isPlainObject, camelCase } = this.app.lib._
14
14
  const allPropKeys = this.getAllPropertyKeys(model.connection.driver)
15
15
  const propType = this.constructor.propertyType
16
16
  if (isString(prop)) {
@@ -22,6 +22,12 @@ async function sanitizeProp (model, prop, indexes) {
22
22
  prop.required = required === 'true'
23
23
  }
24
24
  prop.type = prop.type ?? 'string'
25
+ if (isArray(prop.values)) {
26
+ prop.values = prop.values.map(item => {
27
+ if (isPlainObject(item)) return pick(item, ['value', 'text'])
28
+ return { value: item, text: camelCase(item) }
29
+ })
30
+ } else if (!isString(prop.values)) delete prop.values
25
31
  if (prop.index) {
26
32
  if (prop.index === true || prop.index === 'true') prop.index = 'index'
27
33
  const [idx, idxName] = prop.index.split(':')
@@ -216,6 +216,11 @@ async function driverFactory () {
216
216
  }
217
217
  }
218
218
 
219
+ _injectMeta (result = {}, options = {}) {
220
+ result.warnings = result.warnings ?? []
221
+ result.warnings.push(...(options.warnings ?? []))
222
+ }
223
+
219
224
  async _createRecord (model, body = {}, options = {}) {
220
225
  const { isSet } = this.app.lib.aneka
221
226
  await this._prepBodyForCreate(model, body, options)
@@ -231,6 +236,7 @@ async function driverFactory () {
231
236
  const result = await this.createRecord(model, input, options)
232
237
  if (options.noResult) return
233
238
  result.data = this.sanitizeRecord(model, result.data)
239
+ this._injectMeta(result, options)
234
240
  return result
235
241
  }
236
242
 
@@ -254,6 +260,7 @@ async function driverFactory () {
254
260
  const result = await this.getRecord(model, id, options)
255
261
  if (isEmpty(result.data) && options.throwNotFound) throw this.plugin.error('recordNotFound%s%s', id, model.name)
256
262
  result.data = this.sanitizeRecord(model, result.data)
263
+ this._injectMeta(result, options)
257
264
  return result
258
265
  }
259
266
 
@@ -272,6 +279,7 @@ async function driverFactory () {
272
279
  if (options.noResult) return
273
280
  result.oldData = this.sanitizeRecord(model, result.oldData)
274
281
  result.data = this.sanitizeRecord(model, result.data)
282
+ this._injectMeta(result, options)
275
283
  return result
276
284
  }
277
285
 
@@ -291,6 +299,7 @@ async function driverFactory () {
291
299
  if (options.noResult) return
292
300
  if (result.oldData) result.oldData = this.sanitizeRecord(model, result.oldData)
293
301
  result.data = this.sanitizeRecord(model, result.data)
302
+ this._injectMeta(result, options)
294
303
  return result
295
304
  }
296
305
 
@@ -303,11 +312,13 @@ async function driverFactory () {
303
312
  const result = await this.removeRecord(model, id, options)
304
313
  if (options.noResult) return
305
314
  result.oldData = this.sanitizeRecord(model, result.oldData)
315
+ this._injectMeta(result, options)
306
316
  return result
307
317
  }
308
318
 
309
319
  async _clearRecord (model, options = {}) {
310
320
  const result = await this.clearRecord(model, options)
321
+ this._injectMeta(result, options)
311
322
  return result
312
323
  }
313
324
 
@@ -316,6 +327,7 @@ async function driverFactory () {
316
327
  for (const idx in result.data) {
317
328
  result.data[idx] = this.sanitizeRecord(model, result.data[idx])
318
329
  }
330
+ this._injectMeta(result, options)
319
331
  return result
320
332
  }
321
333
 
@@ -324,6 +336,7 @@ async function driverFactory () {
324
336
  for (const idx in result.data) {
325
337
  result.data[idx] = this.sanitizeRecord(model, result.data[idx])
326
338
  }
339
+ this._injectMeta(result, options)
327
340
  return result
328
341
  }
329
342
 
@@ -349,6 +362,7 @@ async function driverFactory () {
349
362
  for (const idx in result.data) {
350
363
  result.data[idx] = this.sanitizeRecord(model, result.data[idx])
351
364
  }
365
+ this._injectMeta(result, options)
352
366
  return result
353
367
  }
354
368
 
@@ -369,6 +383,7 @@ async function driverFactory () {
369
383
  for (const idx in result.data) {
370
384
  result.data[idx] = this.sanitizeRecord(model, result.data[idx])
371
385
  }
386
+ this._injectMeta(result, options)
372
387
  return result
373
388
  }
374
389
 
@@ -53,14 +53,14 @@ export async function execValidation (body, options = {}) {
53
53
  export async function getFilterAndOptions (filter = {}, options = {}, action) {
54
54
  const { cloneDeep, omit } = this.app.lib._
55
55
  const { runModelHook } = this.app.dobo
56
- const keys = ['req', 'reply']
57
- const nFilter = cloneDeep(omit(filter, keys))
58
- const nOptions = cloneDeep(omit(options, keys))
56
+ const omittedKeys = ['req', 'reply']
57
+ const nFilter = cloneDeep(omit(filter, omittedKeys))
58
+ const nOptions = cloneDeep(omit(options, omittedKeys))
59
59
  nOptions.action = action
60
60
  nOptions.dataOnly = false
61
61
  nOptions.truncateString = nOptions.truncateString ?? false
62
62
  nOptions.throwNotFound = nOptions.throwNotFound ?? true
63
- for (const key of keys) {
63
+ for (const key of omittedKeys) {
64
64
  nOptions[key] = options[key]
65
65
  }
66
66
  nFilter.orgQuery = nFilter.query
@@ -234,10 +234,7 @@ export function buildFilterQuery (filter = {}) {
234
234
  query = trim(query)
235
235
  if (query.startsWith('{')) q = JSON.parse(query) // JSON formatted query
236
236
  else if (query.includes(':')) q = nql(query).parse() // NQL
237
- else if (this.driver.support.search) {
238
- filter.search = q
239
- return {} //
240
- } else {
237
+ else {
241
238
  let scanables = [...this.scanables]
242
239
  if (scanables.length === 0) scanables = [...this.sortables]
243
240
  const fields = scanables.filter(f => {
@@ -380,10 +377,15 @@ export function preparePagination (filter = {}, options = {}) {
380
377
  const buildPageSkipLimit = (filter) => {
381
378
  let limit = parseInt(filter.limit) || config.default.filter.limit
382
379
  if (limit === -1) limit = config.default.filter.maxLimit
383
- if (limit > config.default.filter.maxLimit) limit = config.default.filter.maxLimit
380
+ if (limit > config.default.filter.maxLimit) {
381
+ options.warnings = options.warnings ?? []
382
+ options.warnings.push(options.req ? options.req.t('maxLimitWarning%s%s', limit, config.default.filter.maxLimit) : this.plugin.t('maxLimitWarning%s', limit, config.default.filter.maxLimit))
383
+ limit = config.default.filter.maxLimit // TODO: notify as warning in response object
384
+ }
384
385
  if (limit < 1) limit = 1
385
386
  let page = parseInt(filter.page) || 1
386
387
  if (page < 1) page = 1
388
+ if (page > config.default.filter.maxPage) throw this.plugin.error('maxPageError%s%s', page, config.default.filter.maxPage)
387
389
  let skip = (page - 1) * limit
388
390
  if (filter.skip) {
389
391
  skip = parseInt(filter.skip) || skip
@@ -43,12 +43,14 @@ async function loop (params, opts, dataOnly) {
43
43
  nFilter.limit = maxLimit
44
44
  nFilter.page = 1
45
45
  let count = 0
46
+ const warnings = options.warnings ?? []
46
47
  const data = []
47
48
  for (;;) {
48
49
  const result = await this.findRecord(nFilter, nOptions)
49
50
  if (result.data.length === 0) break
50
51
  if (count + result.data.length > hardLimit) {
51
52
  const sliced = result.data.slice(0, hardLimit - count)
53
+ warnings.push(options.req ? options.req.t('hardLimitWarning%s%s', result.data.length, hardLimit) : this.plugin.t('hardLimitWarning%s%s', result.data.length, hardLimit))
52
54
  data.push(...sliced)
53
55
  break
54
56
  }
@@ -56,7 +58,7 @@ async function loop (params, opts, dataOnly) {
56
58
  count = count + result.data.length
57
59
  nFilter.page++
58
60
  }
59
- return dataOnly ? data : { data, count }
61
+ return dataOnly ? data : { data, count, warnings }
60
62
  }
61
63
 
62
64
  async function findAllRecord (...args) {
@@ -89,8 +89,9 @@ const validator = {
89
89
  timestamp: ['timestamp']
90
90
  }
91
91
 
92
- function buildFromDbModel (opts = {}) {
92
+ async function buildFromDbModel (opts = {}) {
93
93
  const { isPlainObject, get, isEmpty, isString, keys, find, has, without } = this.app.lib._
94
+ const { callHandler } = this.app.bajo
94
95
  const { fields = [], rule = {}, extFields = [] } = opts
95
96
  const obj = {}
96
97
  const { propertyType: propType } = this.app.baseClass.Dobo
@@ -110,7 +111,7 @@ function buildFromDbModel (opts = {}) {
110
111
  return { key, value, columns }
111
112
  }
112
113
 
113
- function applyFieldRules (prop, obj) {
114
+ async function applyFieldRules (prop, obj) {
114
115
  const minMax = { min: false, max: false }
115
116
  const rules = get(rule, prop.name, prop.rules ?? [])
116
117
  if (!Array.isArray(rules)) return rules
@@ -133,8 +134,23 @@ function buildFromDbModel (opts = {}) {
133
134
  if (has(prop, `${k}Length`)) obj = obj[k](prop[`${k}Length`])
134
135
  }
135
136
  }
136
- if (Array.isArray(prop.values)) obj = obj.valid(...prop.values)
137
137
  if (!['id'].includes(prop.name) && prop.required) obj = obj.required()
138
+ if (prop.values) {
139
+ let items = []
140
+ if (Array.isArray(prop.values)) items = prop.values.map(item => item.value)
141
+ else if (typeof prop.values === 'string') {
142
+ const resp = await callHandler(prop.values)
143
+ items = resp.map(item => item.value)
144
+ }
145
+ obj = obj.valid(...items)
146
+ }
147
+ if (prop.rulesMsg) {
148
+ const msgs = {}
149
+ for (const k in prop.rulesMsg) {
150
+ msgs[k] = '~' + prop.rulesMsg[k] // note: to tell bajo error formatter that this is a custom error message
151
+ }
152
+ obj = obj.messages(msgs)
153
+ }
138
154
  return obj
139
155
  }
140
156
 
@@ -147,28 +163,28 @@ function buildFromDbModel (opts = {}) {
147
163
  switch (p.type) {
148
164
  case 'text':
149
165
  case 'string': {
150
- item = applyFieldRules(p, joi.string())
166
+ item = await applyFieldRules(p, joi.string())
151
167
  break
152
168
  }
153
169
  case 'smallint':
154
170
  case 'integer':
155
- item = applyFieldRules(p, joi.number().integer())
171
+ item = await applyFieldRules(p, joi.number().integer())
156
172
  break
157
173
  case 'float':
158
174
  case 'double':
159
- if (p.precision) item = applyFieldRules(p, joi.number().precision(p.precision))
160
- else item = applyFieldRules(p, joi.number())
175
+ if (p.precision) item = await applyFieldRules(p, joi.number().precision(p.precision))
176
+ else item = await applyFieldRules(p, joi.number())
161
177
  break
162
178
  case 'time':
163
179
  case 'date':
164
180
  case 'datetime':
165
- item = applyFieldRules(p, joi.date())
181
+ item = await applyFieldRules(p, joi.date())
166
182
  break
167
183
  case 'timestamp':
168
- item = applyFieldRules(p, joi.number().integer())
184
+ item = await applyFieldRules(p, joi.number().integer())
169
185
  break
170
186
  case 'boolean':
171
- item = applyFieldRules(p, joi.boolean())
187
+ item = await applyFieldRules(p, joi.boolean())
172
188
  break
173
189
  }
174
190
  if (item) {
@@ -218,7 +234,7 @@ async function validate (body, joiModel, opts = {}) {
218
234
  params = defaultsDeep(params, this.app.dobo.config.validationParams)
219
235
  const { rule = {} } = params
220
236
  delete params.rule
221
- if (isEmpty(joiModel)) joiModel = buildFromDbModel.call(this, { fields, rule, extFields, partial })
237
+ if (isEmpty(joiModel)) joiModel = await buildFromDbModel.call(this, { fields, rule, extFields, partial })
222
238
  if (!joiModel) return { value: body }
223
239
  try {
224
240
  return await joiModel.validateAsync(body, params)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dobo",
3
- "version": "2.6.6",
3
+ "version": "2.8.0",
4
4
  "description": "DBMS for Bajo Framework",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/wiki/CHANGES.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changes
2
2
 
3
+ ## 2026-02-22
4
+
5
+ - [2.8.0] Add ```warnings``` to response object
6
+ - [2.8.0] Throw error if ```page``` above the allowed threshold
7
+
8
+ ## 2026-02-20
9
+
10
+ - [2.7.0] Add ```prop.values``` support
11
+ - [2.7.0] Add ```prop.rulesMsg``` support
12
+ - [2.7.0] Change ```buildFromDbModel()``` on validation to async function
13
+
3
14
  ## 2026-02-17
4
15
 
5
16
  - [2.6.5] Bug fix on model extender