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.
- package/extend/bajo/intl/en-US.json +3 -0
- package/extend/bajo/intl/id.json +3 -0
- package/index.js +2 -1
- package/lib/collect-models.js +7 -1
- package/lib/factory/driver.js +15 -0
- package/lib/factory/model/_util.js +11 -9
- package/lib/factory/model/find-all-record.js +3 -1
- package/lib/factory/model/validate.js +27 -11
- package/package.json +1 -1
- package/wiki/CHANGES.md +11 -0
|
@@ -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",
|
package/extend/bajo/intl/id.json
CHANGED
|
@@ -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
|
},
|
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 } = 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(':')
|
package/lib/factory/driver.js
CHANGED
|
@@ -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
|
|
57
|
-
const nFilter = cloneDeep(omit(filter,
|
|
58
|
-
const nOptions = cloneDeep(omit(options,
|
|
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
|
|
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
|
|
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)
|
|
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
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
|