dobo 2.12.0 → 2.14.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 -1
- package/extend/bajo/intl/id.json +2 -2
- package/index.js +27 -4
- package/lib/factory/driver.js +1 -2
- package/lib/factory/model/_util.js +25 -8
- package/lib/factory/model/count-record.js +8 -0
- package/lib/factory/model/find-all-record.js +5 -4
- package/lib/factory/model/find-record.js +15 -2
- package/package.json +1 -1
- package/wiki/CHANGES.md +5 -0
- package/wiki/CONFIG.md +1 -1
|
@@ -146,7 +146,7 @@
|
|
|
146
146
|
"buildOp": "'build' operation",
|
|
147
147
|
"dropOp": "'drop' operation",
|
|
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
151
|
"field": {
|
|
152
152
|
"id": "ID",
|
package/extend/bajo/intl/id.json
CHANGED
|
@@ -143,8 +143,8 @@
|
|
|
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
|
-
"
|
|
146
|
+
"maxLimitWarning%s%s": "Data per halaman (%s baris) melampaui batas yang diijinkan (%s baris)",
|
|
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
149
|
"field": {
|
|
150
150
|
"id": "ID",
|
package/index.js
CHANGED
|
@@ -148,10 +148,10 @@ async function factory (pkgName) {
|
|
|
148
148
|
},
|
|
149
149
|
default: {
|
|
150
150
|
filter: {
|
|
151
|
-
limit: 25,
|
|
152
|
-
maxLimit: 200,
|
|
153
|
-
|
|
154
|
-
maxPage: 50,
|
|
151
|
+
limit: 25, // num of records per page
|
|
152
|
+
maxLimit: 200, // max num of records per page
|
|
153
|
+
hardCap: 10000, // max returned records
|
|
154
|
+
maxPage: 50, // max allowed page number
|
|
155
155
|
sort: ['dt:-1', 'updatedAt:-1', 'updated_at:-1', 'createdAt:-1', 'createdAt:-1', 'ts:-1', 'username', 'name']
|
|
156
156
|
},
|
|
157
157
|
cache: {
|
|
@@ -493,6 +493,29 @@ async function factory (pkgName) {
|
|
|
493
493
|
else await hook.handler.call(model, ...args)
|
|
494
494
|
}
|
|
495
495
|
}
|
|
496
|
+
|
|
497
|
+
getDefaultValues = (options = {}) => {
|
|
498
|
+
const { get } = this.app.lib._
|
|
499
|
+
const config = this.app.dobo.config
|
|
500
|
+
const limit = get(options, 'req.site.setting.dobo.default.limit', config.default.filter.limit)
|
|
501
|
+
const maxLimit = get(options, 'req.site.setting.dobo.default.maxLimit', config.default.filter.maxLimit)
|
|
502
|
+
const hardCap = get(options, 'req.site.setting.dobo.default.hardCap', config.default.filter.hardCap)
|
|
503
|
+
const maxPage = get(options, 'req.site.setting.dobo.default.maxPage', config.default.filter.maxPage)
|
|
504
|
+
const t = options.req ? options.req.t : this.t
|
|
505
|
+
return { limit, maxLimit, hardCap, maxPage, t }
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
handleLastPage = (params = {}, options = {}) => {
|
|
509
|
+
const { count, limit, page } = params
|
|
510
|
+
const { hardCap } = this.getDefaultValues(options)
|
|
511
|
+
if (count > hardCap) {
|
|
512
|
+
const lastPage = Math.floor(hardCap / limit)
|
|
513
|
+
if (page > lastPage) {
|
|
514
|
+
const warnings = [this.t('hardCapWarning%s%s', count, hardCap)]
|
|
515
|
+
return { data: [], count: hardCap, warnings }
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
496
519
|
}
|
|
497
520
|
|
|
498
521
|
return Dobo
|
package/lib/factory/driver.js
CHANGED
|
@@ -135,8 +135,7 @@ async function driverFactory () {
|
|
|
135
135
|
if (isSet(body[field])) query[field] = body[field]
|
|
136
136
|
}
|
|
137
137
|
if (isEmpty(query)) continue
|
|
138
|
-
const
|
|
139
|
-
const data = resp.data[0]
|
|
138
|
+
const data = await this.findOneRecord(model, { query }, options)
|
|
140
139
|
if (!isEmpty(data)) {
|
|
141
140
|
if (['updateRecord', 'upsertRecord'].includes(options.action)) {
|
|
142
141
|
let eq = true
|
|
@@ -236,6 +236,22 @@ export async function getMultiRefs (records = [], options = {}) {
|
|
|
236
236
|
}
|
|
237
237
|
}
|
|
238
238
|
|
|
239
|
+
export function parseNql (text) {
|
|
240
|
+
const sanitized = text.split('+').map(item => {
|
|
241
|
+
const [key, ...rest] = item.split(':').map(i => i.trim())
|
|
242
|
+
let value = rest.join(':')
|
|
243
|
+
const neg = value[1] === '-' ? '-' : ''
|
|
244
|
+
if ((value[0] === '{' || value[1] === '{') && value[value.length - 1] === '}') {
|
|
245
|
+
if (value[0] === '-') value = value.slice(1)
|
|
246
|
+
const items = value.slice(1, -1).split(',')
|
|
247
|
+
value = `${neg}>=${items[0]}+${key}:${neg}<=${items[1]}`
|
|
248
|
+
}
|
|
249
|
+
return `${key}:${value}`
|
|
250
|
+
}).join('+')
|
|
251
|
+
|
|
252
|
+
return nql(sanitized).parse()
|
|
253
|
+
}
|
|
254
|
+
|
|
239
255
|
export function buildFilterQuery (filter = {}) {
|
|
240
256
|
const { trim, find, isString, isPlainObject } = this.app.lib._
|
|
241
257
|
let query = filter.query ?? {}
|
|
@@ -244,7 +260,7 @@ export function buildFilterQuery (filter = {}) {
|
|
|
244
260
|
try {
|
|
245
261
|
query = trim(query)
|
|
246
262
|
if (query.startsWith('{')) q = JSON.parse(query) // JSON formatted query
|
|
247
|
-
else if (query.includes(':')) q =
|
|
263
|
+
else if (query.includes(':')) q = parseNql.call(this, query) // NQL
|
|
248
264
|
else {
|
|
249
265
|
let scanables = [...this.scanables]
|
|
250
266
|
if (scanables.length === 0) scanables = [...this.sortables]
|
|
@@ -383,20 +399,21 @@ export function replaceIdInQuerySearch (filter) {
|
|
|
383
399
|
*/
|
|
384
400
|
export function preparePagination (filter = {}, options = {}) {
|
|
385
401
|
const { isEmpty, map, each, isPlainObject, isString, trim, keys } = this.app.lib._
|
|
386
|
-
const config = this.app.dobo
|
|
402
|
+
const { getDefaultValues, config } = this.app.dobo
|
|
403
|
+
const { limit: defLimit, maxLimit: defMaxLimit, maxPage: defMaxPage } = getDefaultValues(options)
|
|
387
404
|
|
|
388
405
|
const buildPageSkipLimit = (filter) => {
|
|
389
|
-
let limit = parseInt(filter.limit) ||
|
|
390
|
-
if (limit === -1) limit =
|
|
391
|
-
if (limit >
|
|
406
|
+
let limit = parseInt(filter.limit) || defLimit
|
|
407
|
+
if (limit === -1) limit = defMaxLimit
|
|
408
|
+
if (limit > defMaxLimit) {
|
|
392
409
|
options.warnings = options.warnings ?? []
|
|
393
|
-
options.warnings.push(options.req ? options.req.t('maxLimitWarning%s%s', limit,
|
|
394
|
-
limit =
|
|
410
|
+
options.warnings.push(options.req ? options.req.t('maxLimitWarning%s%s', limit, defMaxLimit) : this.plugin.t('maxLimitWarning%s', limit, defMaxLimit))
|
|
411
|
+
limit = defMaxLimit // TODO: notify as warning in response object
|
|
395
412
|
}
|
|
396
413
|
if (limit < 1) limit = 1
|
|
397
414
|
let page = parseInt(filter.page) || 1
|
|
398
415
|
if (page < 1) page = 1
|
|
399
|
-
if (page >
|
|
416
|
+
if (page > defMaxPage) throw this.plugin.error('maxPageError%s%s', page, defMaxPage)
|
|
400
417
|
let skip = (page - 1) * limit
|
|
401
418
|
if (filter.skip) {
|
|
402
419
|
skip = parseInt(filter.skip) || skip
|
|
@@ -2,14 +2,22 @@ import { getFilterAndOptions, execHook, execModelHook, execDynHook } from './_ut
|
|
|
2
2
|
const action = 'countRecord'
|
|
3
3
|
|
|
4
4
|
async function countRecord (...args) {
|
|
5
|
+
const { getDefaultValues } = this.app.dobo
|
|
5
6
|
if (args.length === 0) return this.action(action, ...args)
|
|
6
7
|
const [params = {}, opts = {}] = args
|
|
7
8
|
const { dataOnly = true } = opts
|
|
8
9
|
const { filter, options } = await getFilterAndOptions.call(this, params, opts, action)
|
|
10
|
+
const { hardCap, t } = getDefaultValues(options)
|
|
9
11
|
await execHook.call(this, 'beforeCountRecord', options)
|
|
10
12
|
await execModelHook.call(this, 'beforeCountRecord', filter, options)
|
|
11
13
|
await execDynHook.call(this, 'beforeCountRecord', filter, options)
|
|
12
14
|
const result = (await this.driver._countRecord(this, filter, options)) ?? {}
|
|
15
|
+
if (result.data > hardCap) {
|
|
16
|
+
result.warnings = result.warnings ?? []
|
|
17
|
+
result.warnings.push(t('hardCapWarning%s%s', result.data, hardCap))
|
|
18
|
+
result.orgCount = result.data
|
|
19
|
+
result.data = hardCap
|
|
20
|
+
}
|
|
13
21
|
await execDynHook.call(this, 'afterCountRecord', filter, result, options)
|
|
14
22
|
await execModelHook.call(this, 'afterCountRecord', filter, result, options)
|
|
15
23
|
await execHook.call(this, 'afterCountRecord', filter, result, options)
|
|
@@ -44,10 +44,11 @@ async function native (...args) {
|
|
|
44
44
|
|
|
45
45
|
async function loop (...args) {
|
|
46
46
|
const { cloneDeep } = this.app.lib._
|
|
47
|
+
const { getDefaultValues } = this.app.dobo
|
|
47
48
|
const [params = {}, opts = {}] = args
|
|
48
49
|
const { dataOnly = true } = opts
|
|
49
50
|
const { filter, options } = await getFilterAndOptions.call(this, params, opts, action)
|
|
50
|
-
const { maxLimit,
|
|
51
|
+
const { maxLimit, hardCap } = getDefaultValues(options)
|
|
51
52
|
const nFilter = cloneDeep(filter || {})
|
|
52
53
|
const nOptions = cloneOptions.call(this, options)
|
|
53
54
|
nOptions.count = false
|
|
@@ -60,9 +61,9 @@ async function loop (...args) {
|
|
|
60
61
|
for (;;) {
|
|
61
62
|
const result = await this.findRecord(nFilter, nOptions)
|
|
62
63
|
if (result.data.length === 0) break
|
|
63
|
-
if (count + result.data.length >
|
|
64
|
-
const sliced = result.data.slice(0,
|
|
65
|
-
warnings.push(options.req ? options.req.t('
|
|
64
|
+
if (count + result.data.length > hardCap) {
|
|
65
|
+
const sliced = result.data.slice(0, hardCap - count)
|
|
66
|
+
warnings.push(options.req ? options.req.t('hardCapWarning%s%s', result.data.length, hardCap) : this.plugin.t('hardCapWarning%s%s', result.data.length, hardCap))
|
|
66
67
|
data.push(...sliced)
|
|
67
68
|
break
|
|
68
69
|
}
|
|
@@ -66,12 +66,14 @@ const action = 'findRecord'
|
|
|
66
66
|
*/
|
|
67
67
|
async function findRecord (...args) {
|
|
68
68
|
if (args.length === 0) return this.action(action, ...args)
|
|
69
|
+
const { getDefaultValues, t } = this.app.dobo
|
|
69
70
|
const [params = {}, opts = {}] = args
|
|
70
71
|
const { isSet } = this.app.lib.aneka
|
|
71
|
-
const { cloneDeep } = this.app.lib._
|
|
72
|
+
const { cloneDeep, pick, omit } = this.app.lib._
|
|
72
73
|
const { dataOnly = true } = opts
|
|
73
74
|
const { get: getCache, set: setCache } = this.app.bajoCache ?? {}
|
|
74
75
|
const { filter, options } = await getFilterAndOptions.call(this, params, opts, action)
|
|
76
|
+
const { hardCap } = getDefaultValues(options)
|
|
75
77
|
if (dataOnly) options.count = false
|
|
76
78
|
let { noResultSanitizer, noCache } = options
|
|
77
79
|
await execHook.call(this, 'beforeFindRecord', filter, options)
|
|
@@ -89,7 +91,18 @@ async function findRecord (...args) {
|
|
|
89
91
|
return dataOnly ? result.data : result
|
|
90
92
|
}
|
|
91
93
|
}
|
|
92
|
-
|
|
94
|
+
let result = options.record ?? (await this.driver._findRecord(this, filter, options)) ?? {}
|
|
95
|
+
result.page = filter.page
|
|
96
|
+
result.limit = filter.limit
|
|
97
|
+
result.filter = pick(filter, ['query', 'match', 'sort'])
|
|
98
|
+
result.warnings = result.warnings ?? []
|
|
99
|
+
if (!options.count) result = omit(result, ['count', 'pages'])
|
|
100
|
+
else if (options.count && result.count > hardCap) {
|
|
101
|
+
result.warnings.push(t('hardCapWarning%s%s', result.count, hardCap))
|
|
102
|
+
result.count = hardCap
|
|
103
|
+
}
|
|
104
|
+
result.pages = options.count ? Math.ceil(result.count / filter.limit) : undefined
|
|
105
|
+
|
|
93
106
|
if (!noResultSanitizer) {
|
|
94
107
|
for (const idx in result.data) {
|
|
95
108
|
result.data[idx] = await this.sanitizeRecord(result.data[idx], options)
|
package/package.json
CHANGED
package/wiki/CHANGES.md
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
# Changes
|
|
2
2
|
|
|
3
|
+
## 2026-04-01
|
|
4
|
+
|
|
5
|
+
- [2.14.0] Add ```between``` as custom query, since it doesn't exists in NQL
|
|
6
|
+
|
|
3
7
|
## 2026-03-30
|
|
4
8
|
|
|
5
9
|
- [2.12.0] Add ```.bootorder``` level 10
|
|
6
10
|
- [2.12.0] Bug fix in ```model._simpleLookup()```
|
|
7
11
|
- [2.12.0] Add model name in validation error's payload
|
|
12
|
+
- [2.13.0] Add ```hardCap``` support
|
|
8
13
|
|
|
9
14
|
## 2026-03-26
|
|
10
15
|
|
package/wiki/CONFIG.md
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
| ```filter``` | ```object``` | | |
|
|
17
17
|
| ```limit``` | ```number``` | ```25``` | Rows returned in one page |
|
|
18
18
|
| ```maxLimit``` | ```number``` | ```200``` | Max rows returned in one page |
|
|
19
|
-
| ```
|
|
19
|
+
| ```hardCap``` | ```number``` | ```10000``` | Max rows returned on dataset export |
|
|
20
20
|
| ```sort``` | ```array``` | | |
|
|
21
21
|
| ```idField``` | ```object``` | | |
|
|
22
22
|
| ```memDb``` | ```object``` | | |
|