dobo 2.14.0 → 2.15.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/dobo/feature/created-at.js +4 -4
- package/extend/dobo/feature/dt.js +2 -2
- package/extend/dobo/feature/immutable.js +3 -3
- package/extend/dobo/feature/removed-at.js +6 -6
- package/extend/dobo/feature/unique.js +4 -4
- package/extend/dobo/feature/updated-at.js +6 -6
- package/extend/waibuMpa/route/attachment/@model/@id/@field/@file.js +1 -1
- package/index.js +69 -9
- package/lib/collect-models.js +9 -6
- package/lib/factory/action.js +6 -6
- package/lib/factory/connection.js +2 -2
- package/lib/factory/feature.js +2 -2
- package/lib/factory/model/_util.js +26 -42
- package/lib/factory/model/count-record.js +3 -0
- package/lib/factory/model/create-aggregate.js +3 -0
- package/lib/factory/model/create-attachment.js +5 -5
- package/lib/factory/model/create-histogram.js +3 -0
- package/lib/factory/model/create-record.js +3 -0
- package/lib/factory/model/find-all-record.js +20 -4
- package/lib/factory/model/find-attachment.js +4 -4
- package/lib/factory/model/find-one-record.js +7 -3
- package/lib/factory/model/find-record.js +3 -1
- package/lib/factory/model/get-attachment.js +3 -3
- package/lib/factory/model/get-record.js +3 -0
- package/lib/factory/model/list-attachment.js +5 -5
- package/lib/factory/model/remove-attachment.js +2 -2
- package/lib/factory/model/remove-record.js +3 -0
- package/lib/factory/model/sanitize-record.js +4 -2
- package/lib/factory/model/update-record.js +3 -0
- package/lib/factory/model/upsert-record.js +3 -0
- package/lib/factory/model.js +2 -2
- package/package.json +1 -1
- package/wiki/CHANGES.md +17 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
async function createdAt (opts = {}) {
|
|
2
|
-
opts.
|
|
2
|
+
opts.field = opts.field ?? 'createdAt'
|
|
3
3
|
opts.noOverwrite = opts.noOverwrite ?? false
|
|
4
4
|
return {
|
|
5
5
|
properties: [{
|
|
6
|
-
name: opts.
|
|
6
|
+
name: opts.field,
|
|
7
7
|
type: 'datetime',
|
|
8
8
|
index: true
|
|
9
9
|
}],
|
|
@@ -11,8 +11,8 @@ async function createdAt (opts = {}) {
|
|
|
11
11
|
name: 'beforeCreateRecord',
|
|
12
12
|
handler: async function (body, options) {
|
|
13
13
|
const { isSet } = this.app.lib.aneka
|
|
14
|
-
if (opts.noOverwrite) body[opts.
|
|
15
|
-
else if (!isSet(body[opts.
|
|
14
|
+
if (opts.noOverwrite) body[opts.field] = new Date()
|
|
15
|
+
else if (!isSet(body[opts.field])) body[opts.field] = new Date()
|
|
16
16
|
}
|
|
17
17
|
}]
|
|
18
18
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
async function dt (opts = {}) {
|
|
2
|
-
opts.
|
|
2
|
+
opts.field = opts.field ?? 'dt'
|
|
3
3
|
return {
|
|
4
4
|
properties: [{
|
|
5
|
-
name: opts.
|
|
5
|
+
name: opts.field ?? 'dt',
|
|
6
6
|
type: 'datetime',
|
|
7
7
|
required: opts.required ?? true,
|
|
8
8
|
index: opts.index ?? true
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
async function beforeRemoveRecord (id, opts) {
|
|
2
2
|
const { get } = this.app.lib._
|
|
3
3
|
const record = await this.driver.getRecord(this, id)
|
|
4
|
-
const immutable = get(record.data, opts.
|
|
4
|
+
const immutable = get(record.data, opts.field)
|
|
5
5
|
if (immutable) throw this.plugin.error('recordImmutable%s%s', id, this.name, { statusCode: 423 })
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
async function immutable (opts = {}) {
|
|
9
|
-
opts.
|
|
9
|
+
opts.field = opts.field ?? '_immutable'
|
|
10
10
|
return {
|
|
11
11
|
properties: {
|
|
12
|
-
name: opts.
|
|
12
|
+
name: opts.field,
|
|
13
13
|
type: 'boolean',
|
|
14
14
|
hidden: true
|
|
15
15
|
},
|
|
@@ -6,24 +6,24 @@ async function beforeFindRecord ({ filter = {} }, opts) {
|
|
|
6
6
|
if (filter.query.$and) q.$and.push(...filter.query.$and)
|
|
7
7
|
else q.$and.push(filter.query)
|
|
8
8
|
}
|
|
9
|
-
q.$and.push(set({}, opts.
|
|
9
|
+
q.$and.push(set({}, opts.field, null))
|
|
10
10
|
filter.query = q
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
async function afterGetRecord ({ id, record = {} }, opts) {
|
|
14
14
|
const { isEmpty } = this.app.lib._
|
|
15
|
-
if (!isEmpty(record.data[opts.
|
|
15
|
+
if (!isEmpty(record.data[opts.field])) throw this.error('recordNotFound%s%s', id, this.name, { statusCode: 404 })
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
async function beforeCreateRecord ({ body = {} }, opts) {
|
|
19
|
-
delete body[opts.
|
|
19
|
+
delete body[opts.field]
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
async function removedAt (opts = {}) {
|
|
23
|
-
opts.
|
|
23
|
+
opts.field = opts.field ?? '_removedAt'
|
|
24
24
|
return {
|
|
25
25
|
properties: {
|
|
26
|
-
name: opts.
|
|
26
|
+
name: opts.field,
|
|
27
27
|
type: 'datetime',
|
|
28
28
|
index: true,
|
|
29
29
|
hidden: true
|
|
@@ -52,7 +52,7 @@ async function removedAt (opts = {}) {
|
|
|
52
52
|
name: 'beforeRemoveRecord',
|
|
53
53
|
handler: async function (id, options) {
|
|
54
54
|
const { set } = this.app.lib._
|
|
55
|
-
const body = set({}, opts.
|
|
55
|
+
const body = set({}, opts.field, new Date())
|
|
56
56
|
const record = await this.driver.recordUpdate(this, id, body, { noResult: false })
|
|
57
57
|
options.record = { oldData: record.oldData }
|
|
58
58
|
}
|
|
@@ -2,11 +2,11 @@ import crypto from 'crypto'
|
|
|
2
2
|
|
|
3
3
|
async function unique (opts = {}) {
|
|
4
4
|
const { omit } = this.app.lib._
|
|
5
|
-
opts.
|
|
5
|
+
opts.field = opts.field ?? 'id'
|
|
6
6
|
opts.fields = opts.fields ?? []
|
|
7
7
|
return {
|
|
8
8
|
properties: [{
|
|
9
|
-
name: opts.
|
|
9
|
+
name: opts.field,
|
|
10
10
|
type: 'string',
|
|
11
11
|
maxLength: 32,
|
|
12
12
|
required: true,
|
|
@@ -16,12 +16,12 @@ async function unique (opts = {}) {
|
|
|
16
16
|
name: 'beforeCreateRecord',
|
|
17
17
|
level: 1000,
|
|
18
18
|
handler: async function (body, options) {
|
|
19
|
-
if (opts.fields.length === 0) opts.fields = omit(this.properties.map(prop => prop.name), [opts.
|
|
19
|
+
if (opts.fields.length === 0) opts.fields = omit(this.properties.map(prop => prop.name), [opts.field])
|
|
20
20
|
const item = {}
|
|
21
21
|
for (const f of opts.fields) {
|
|
22
22
|
item[f] = body[f]
|
|
23
23
|
}
|
|
24
|
-
body[opts.
|
|
24
|
+
body[opts.field] = crypto.createHash('md5').update(JSON.stringify(item)).digest('hex')
|
|
25
25
|
}
|
|
26
26
|
}]
|
|
27
27
|
}
|
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
async function updatedAt (opts = {}) {
|
|
2
2
|
const { isSet } = this.app.lib.aneka
|
|
3
|
-
opts.
|
|
3
|
+
opts.field = opts.field ?? 'updatedAt'
|
|
4
4
|
opts.noOverwrite = opts.noOverwrite ?? false
|
|
5
5
|
return {
|
|
6
6
|
properties: {
|
|
7
|
-
name: opts.
|
|
7
|
+
name: opts.field,
|
|
8
8
|
type: 'datetime',
|
|
9
9
|
index: true
|
|
10
10
|
},
|
|
11
11
|
hooks: [{
|
|
12
12
|
name: 'beforeCreateRecord',
|
|
13
13
|
handler: async function (body, options) {
|
|
14
|
-
if (opts.noOverwrite) body[opts.
|
|
15
|
-
else if (!isSet(body[opts.
|
|
14
|
+
if (opts.noOverwrite) body[opts.field] = new Date()
|
|
15
|
+
else if (!isSet(body[opts.field])) body[opts.field] = new Date()
|
|
16
16
|
}
|
|
17
17
|
}, {
|
|
18
18
|
name: 'beforeUpdateRecord',
|
|
19
19
|
handler: async function (id, body, options) {
|
|
20
|
-
if (opts.noOverwrite) body[opts.
|
|
21
|
-
else if (!isSet(body[opts.
|
|
20
|
+
if (opts.noOverwrite) body[opts.field] = new Date()
|
|
21
|
+
else if (!isSet(body[opts.field])) body[opts.field] = new Date()
|
|
22
22
|
}
|
|
23
23
|
}]
|
|
24
24
|
}
|
|
@@ -7,7 +7,7 @@ async function attachment (req, reply) {
|
|
|
7
7
|
const { fs } = this.app.lib
|
|
8
8
|
const mdl = this.app.dobo.getModel(req.params.model)
|
|
9
9
|
const type = req.query.type
|
|
10
|
-
const items = (await mdl.listAttachments({ id: req.params.id,
|
|
10
|
+
const items = (await mdl.listAttachments({ id: req.params.id, field: req.params.field, file: '*', type })) ?? []
|
|
11
11
|
let item
|
|
12
12
|
if (req.params.file === '_first') item = items[0]
|
|
13
13
|
else if (req.params.file === '_last') item = last(items)
|
package/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import nql from '@tryghost/nql'
|
|
1
2
|
import collectConnections from './lib/collect-connections.js'
|
|
2
3
|
import collectDrivers from './lib/collect-drivers.js'
|
|
3
4
|
import collectFeatures from './lib/collect-features.js'
|
|
@@ -150,13 +151,14 @@ async function factory (pkgName) {
|
|
|
150
151
|
filter: {
|
|
151
152
|
limit: 25, // num of records per page
|
|
152
153
|
maxLimit: 200, // max num of records per page
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
sort: ['dt:-1', 'updatedAt:-1', 'updated_at:-1', 'createdAt:-1', 'createdAt:-1', 'ts:-1', 'username', 'name']
|
|
154
|
+
maxPage: 400, // max allowed page number
|
|
155
|
+
sort: ['dt:-1', 'updatedAt:-1', 'createdAt:-1', 'ts:-1', 'username', 'name']
|
|
156
156
|
},
|
|
157
157
|
cache: {
|
|
158
158
|
ttlDur: '10s'
|
|
159
|
-
}
|
|
159
|
+
},
|
|
160
|
+
hardCap: 10000, // max returned records
|
|
161
|
+
warnings: true
|
|
160
162
|
},
|
|
161
163
|
memDb: {
|
|
162
164
|
autoSaveDur: '1s'
|
|
@@ -497,12 +499,13 @@ async function factory (pkgName) {
|
|
|
497
499
|
getDefaultValues = (options = {}) => {
|
|
498
500
|
const { get } = this.app.lib._
|
|
499
501
|
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
|
|
503
|
-
const
|
|
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)
|
|
504
507
|
const t = options.req ? options.req.t : this.t
|
|
505
|
-
return { limit, maxLimit, hardCap, maxPage, t }
|
|
508
|
+
return { limit, maxLimit, hardCap, maxPage, warnings, t }
|
|
506
509
|
}
|
|
507
510
|
|
|
508
511
|
handleLastPage = (params = {}, options = {}) => {
|
|
@@ -516,6 +519,63 @@ async function factory (pkgName) {
|
|
|
516
519
|
}
|
|
517
520
|
}
|
|
518
521
|
}
|
|
522
|
+
|
|
523
|
+
parseNql = (text) => {
|
|
524
|
+
const sanitized = text.split('+').map(item => {
|
|
525
|
+
const [key, ...rest] = item.split(':').map(i => i.trim())
|
|
526
|
+
let value = rest.join(':')
|
|
527
|
+
const neg = value[1] === '-' ? '-' : ''
|
|
528
|
+
if ((value[0] === '{' || value[1] === '{') && value[value.length - 1] === '}') {
|
|
529
|
+
if (value[0] === '-') value = value.slice(1)
|
|
530
|
+
const items = value.slice(1, -1).split(',')
|
|
531
|
+
value = `${neg}>=${items[0]}+${key}:${neg}<=${items[1]}`
|
|
532
|
+
}
|
|
533
|
+
return `${key}:${value}`
|
|
534
|
+
}).join('+')
|
|
535
|
+
return nql(sanitized).parse()
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
parseQuery = (query, silent = true) => {
|
|
539
|
+
const { isPlainObject, trim } = this.app.lib._
|
|
540
|
+
let result = {}
|
|
541
|
+
if (isPlainObject(query)) result = query
|
|
542
|
+
else {
|
|
543
|
+
query = trim(query)
|
|
544
|
+
try {
|
|
545
|
+
if (query.startsWith('{')) result = JSON.parse(query)
|
|
546
|
+
else result = this.parseNql(query)
|
|
547
|
+
} catch (err) {
|
|
548
|
+
if (silent) return {}
|
|
549
|
+
throw err
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
try {
|
|
553
|
+
const text = JSON.stringify(result)
|
|
554
|
+
if (text.includes('["__REGEXP__",')) result = this.reviveRegexInJson(text)
|
|
555
|
+
} catch (err) {}
|
|
556
|
+
return result
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
replaceRegexInJson = (input = {}, returnString = true) => {
|
|
560
|
+
const { isString } = this.app.lib._
|
|
561
|
+
if (isString(input)) input = JSON.parse(input) ?? {}
|
|
562
|
+
const result = JSON.stringify(input, (key, value) => {
|
|
563
|
+
if (value instanceof RegExp) return ['__REGEXP__', value.source, value.flags]
|
|
564
|
+
return value
|
|
565
|
+
})
|
|
566
|
+
if (returnString) return result
|
|
567
|
+
return JSON.parse(result)
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
reviveRegexInJson = (input, returnObject = true) => {
|
|
571
|
+
const { isPlainObject } = this.app.lib._
|
|
572
|
+
if (isPlainObject(input)) input = JSON.stringify(input)
|
|
573
|
+
const result = JSON.parse(input, (key, value) => {
|
|
574
|
+
if (Array.isArray(value) && value[0] === '__REGEXP__') return { $regex: new RegExp(value[1], value[2]) }
|
|
575
|
+
return value
|
|
576
|
+
})
|
|
577
|
+
return returnObject ? result : JSON.stringify(result)
|
|
578
|
+
}
|
|
519
579
|
}
|
|
520
580
|
|
|
521
581
|
return Dobo
|
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, isArray, isPlainObject
|
|
13
|
+
const { isEmpty, isString, keys, pick, isArray, isPlainObject } = this.app.lib._
|
|
14
14
|
const allPropKeys = this.getAllPropertyKeys(model.connection.driver)
|
|
15
15
|
const propType = this.constructor.propertyType
|
|
16
16
|
if (isString(prop)) {
|
|
@@ -25,7 +25,7 @@ async function sanitizeProp (model, prop, indexes) {
|
|
|
25
25
|
if (isArray(prop.values)) {
|
|
26
26
|
prop.values = prop.values.map(item => {
|
|
27
27
|
if (isPlainObject(item)) return pick(item, ['value', 'text'])
|
|
28
|
-
return { value: item, text:
|
|
28
|
+
return { value: item, text: item }
|
|
29
29
|
})
|
|
30
30
|
} else if (!isString(prop.values)) delete prop.values
|
|
31
31
|
if (prop.index) {
|
|
@@ -74,7 +74,7 @@ async function findAllProps (model, inputs = [], indexes = [], isExtender) {
|
|
|
74
74
|
*/
|
|
75
75
|
async function applyFeature (model, feature, options, indexes, isExtender) {
|
|
76
76
|
const { isArray, findIndex } = this.app.lib._
|
|
77
|
-
if (feature.name === 'dobo:unique' && options.
|
|
77
|
+
if (feature.name === 'dobo:unique' && options.field === 'id') {
|
|
78
78
|
const idx = findIndex(model.properties, { name: 'id' })
|
|
79
79
|
if (idx > -1) model.properties.pullAt(idx)
|
|
80
80
|
}
|
|
@@ -142,19 +142,22 @@ export async function sanitizeRef (model, models) {
|
|
|
142
142
|
for (const key in prop.ref ?? {}) {
|
|
143
143
|
let ref = prop.ref[key]
|
|
144
144
|
if (isString(ref)) {
|
|
145
|
-
ref = {
|
|
145
|
+
ref = { field: ref }
|
|
146
146
|
}
|
|
147
|
+
ref.field = ref.field ?? 'id'
|
|
147
148
|
ref.type = ref.type ?? '1:1'
|
|
149
|
+
ref.searchField = ref.searchField ?? model.scanables[0] ?? 'id'
|
|
150
|
+
ref.labelField = ref.labelField ?? ref.searchField
|
|
148
151
|
const rModel = find(models, { name: ref.model })
|
|
149
152
|
if (!rModel) {
|
|
150
153
|
ignored.push(key)
|
|
151
154
|
this.log.warn('notFound%s%s', this.t('model'), ref.model)
|
|
152
155
|
continue
|
|
153
156
|
}
|
|
154
|
-
const rProp = find(rModel.properties, { name: ref.
|
|
157
|
+
const rProp = find(rModel.properties, { name: ref.field })
|
|
155
158
|
if (!rProp) {
|
|
156
159
|
ignored.push(key)
|
|
157
|
-
this.log.warn('notFound%s%s', this.t('property'), `${ref.
|
|
160
|
+
this.log.warn('notFound%s%s', this.t('property'), `${ref.field}@${ref.model}`)
|
|
158
161
|
continue
|
|
159
162
|
}
|
|
160
163
|
ref.fields = ref.fields ?? '*'
|
package/lib/factory/action.js
CHANGED
|
@@ -13,9 +13,9 @@ const methods = {
|
|
|
13
13
|
createHistogram: ['params'],
|
|
14
14
|
createAttachment: ['id'],
|
|
15
15
|
findAttachment: ['id'],
|
|
16
|
-
getAttachment: ['id', '
|
|
16
|
+
getAttachment: ['id', 'field', 'file'],
|
|
17
17
|
listAttachment: ['params'],
|
|
18
|
-
removeAttachment: ['id', '
|
|
18
|
+
removeAttachment: ['id', 'field', 'file'],
|
|
19
19
|
updateAttachment: ['id']
|
|
20
20
|
}
|
|
21
21
|
|
|
@@ -123,8 +123,8 @@ async function actionFactory () {
|
|
|
123
123
|
return this
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
|
|
127
|
-
this.
|
|
126
|
+
field = value => {
|
|
127
|
+
this._field = value
|
|
128
128
|
return this
|
|
129
129
|
}
|
|
130
130
|
|
|
@@ -147,8 +147,8 @@ async function actionFactory () {
|
|
|
147
147
|
return await this.model[this.name](...args)
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
-
dispose () {
|
|
151
|
-
super.dispose()
|
|
150
|
+
dispose = async () => {
|
|
151
|
+
await super.dispose()
|
|
152
152
|
this.model = null
|
|
153
153
|
this._options = null
|
|
154
154
|
}
|
package/lib/factory/feature.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import path from 'path'
|
|
2
|
-
import nql from '@tryghost/nql'
|
|
3
2
|
|
|
4
3
|
export const omittedOptionsKeys = ['req', 'reply', 'trx']
|
|
5
4
|
|
|
@@ -115,12 +114,12 @@ export async function mergeAttachmentInfo (rec, source, options = {}) {
|
|
|
115
114
|
}
|
|
116
115
|
}
|
|
117
116
|
|
|
118
|
-
export async function getAttachmentPath (id,
|
|
117
|
+
export async function getAttachmentPath (id, field, file, options = {}) {
|
|
119
118
|
const { getPluginDataDir } = this.app.bajo
|
|
120
119
|
const { fs } = this.app.lib
|
|
121
120
|
const dir = `${getPluginDataDir(this.app.dobo.ns)}/attachment/${this.name}/${id}`
|
|
122
121
|
if (options.dirOnly) return dir
|
|
123
|
-
const path =
|
|
122
|
+
const path = field ? `${dir}/${field}/${file}` : `${dir}/${file}`
|
|
124
123
|
if (!fs.existsSync(path)) throw this.app.dobo.error('notFound')
|
|
125
124
|
return path
|
|
126
125
|
}
|
|
@@ -134,11 +133,11 @@ export async function copyAttachment (id, options = {}) {
|
|
|
134
133
|
const result = []
|
|
135
134
|
if (files.length === 0) return result
|
|
136
135
|
for (const f of files) {
|
|
137
|
-
let [
|
|
136
|
+
let [field, ...parts] = path.basename(f).split('@')
|
|
138
137
|
if (parts.length === 0) continue
|
|
139
|
-
|
|
138
|
+
field = setField ?? field
|
|
140
139
|
const file = setFile ?? parts.join('@')
|
|
141
|
-
const opts = { source: f,
|
|
140
|
+
const opts = { source: f, field, file, mimeType, stats, req }
|
|
142
141
|
const rec = await this.createAttachment(id, opts)
|
|
143
142
|
if (!rec) continue
|
|
144
143
|
delete rec.dir
|
|
@@ -165,6 +164,7 @@ export async function handleAttachmentUpload (id, trigger, options = {}) {
|
|
|
165
164
|
export async function getSingleRef (record = {}, options = {}) {
|
|
166
165
|
const { isSet } = this.app.lib.aneka
|
|
167
166
|
const { get } = this.app.lib._
|
|
167
|
+
const { parseQuery } = this.app.dobo
|
|
168
168
|
const props = this.properties.filter(p => isSet(p.ref) && !(options.hidden ?? []).includes(p.name))
|
|
169
169
|
const refs = {}
|
|
170
170
|
options.refs = options.refs ?? []
|
|
@@ -176,10 +176,12 @@ export async function getSingleRef (record = {}, options = {}) {
|
|
|
176
176
|
const ref = prop.ref[key]
|
|
177
177
|
if (ref.fields.length === 0) continue
|
|
178
178
|
if (get(record, `_ref.${key}`)) continue
|
|
179
|
-
const rModel = this.app.dobo.getModel(ref.model)
|
|
180
|
-
|
|
181
|
-
query
|
|
182
|
-
|
|
179
|
+
const rModel = this.app.dobo.getModel(ref.model, true)
|
|
180
|
+
if (!rModel) continue
|
|
181
|
+
let query = {}
|
|
182
|
+
query[ref.field] = record[prop.name]
|
|
183
|
+
if (ref.field === 'id') query[ref.field] = this.sanitizeId(query[ref.field])
|
|
184
|
+
if (ref.query) query = { $and: [query, parseQuery(ref.query)] }
|
|
183
185
|
const rFilter = { query }
|
|
184
186
|
const rOptions = { dataOnly: true, refs: [] }
|
|
185
187
|
const results = await rModel.findRecord(rFilter, rOptions)
|
|
@@ -199,6 +201,7 @@ export async function getSingleRef (record = {}, options = {}) {
|
|
|
199
201
|
export async function getMultiRefs (records = [], options = {}) {
|
|
200
202
|
const { isSet } = this.app.lib.aneka
|
|
201
203
|
const { uniq, without, get } = this.app.lib._
|
|
204
|
+
const { parseQuery } = this.app.dobo
|
|
202
205
|
const props = this.properties.filter(p => isSet(p.ref) && !(options.hidden ?? []).includes(p.name))
|
|
203
206
|
options.refs = options.refs ?? []
|
|
204
207
|
if (props.length > 0) {
|
|
@@ -210,14 +213,16 @@ export async function getMultiRefs (records = [], options = {}) {
|
|
|
210
213
|
if (ref.fields.length === 0) continue
|
|
211
214
|
if (ref.type !== '1:1') continue
|
|
212
215
|
if (get(records, `0._ref.${key}`)) continue
|
|
213
|
-
const rModel = this.app.dobo.getModel(ref.model)
|
|
216
|
+
const rModel = this.app.dobo.getModel(ref.model, true)
|
|
217
|
+
if (!rModel) continue
|
|
214
218
|
let matches = []
|
|
215
219
|
for (const r of records) {
|
|
216
220
|
matches.push(rModel.sanitizeId(r[prop.name]))
|
|
217
221
|
}
|
|
218
222
|
matches = uniq(without(matches, undefined, null, NaN))
|
|
219
|
-
|
|
220
|
-
query[ref.
|
|
223
|
+
let query = {}
|
|
224
|
+
query[ref.field] = { $in: matches }
|
|
225
|
+
if (ref.query) query = { $and: [query, parseQuery(ref.query)] }
|
|
221
226
|
const rFilter = { query, limit: matches.length }
|
|
222
227
|
const rOptions = { dataOnly: true, refs: [] }
|
|
223
228
|
const results = await rModel.findRecord(rFilter, rOptions)
|
|
@@ -226,7 +231,7 @@ export async function getMultiRefs (records = [], options = {}) {
|
|
|
226
231
|
for (const i in records) {
|
|
227
232
|
records[i]._ref = records[i]._ref ?? {}
|
|
228
233
|
const rec = records[i]
|
|
229
|
-
const res = results.find(res => (res[ref.
|
|
234
|
+
const res = results.find(res => (res[ref.field] + '') === rec[prop.name] + '')
|
|
230
235
|
if (res) records[i]._ref[key] = await rModel.sanitizeRecord(res, { fields })
|
|
231
236
|
else records[i]._ref[key] = {}
|
|
232
237
|
}
|
|
@@ -236,31 +241,16 @@ export async function getMultiRefs (records = [], options = {}) {
|
|
|
236
241
|
}
|
|
237
242
|
}
|
|
238
243
|
|
|
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
|
-
|
|
255
244
|
export function buildFilterQuery (filter = {}) {
|
|
256
245
|
const { trim, find, isString, isPlainObject } = this.app.lib._
|
|
246
|
+
const { parseNql } = this.app.dobo
|
|
257
247
|
let query = filter.query ?? {}
|
|
258
248
|
let q = {}
|
|
259
249
|
if (isString(query)) {
|
|
260
250
|
try {
|
|
261
251
|
query = trim(query)
|
|
262
252
|
if (query.startsWith('{')) q = JSON.parse(query) // JSON formatted query
|
|
263
|
-
else if (query.includes(':')) q = parseNql
|
|
253
|
+
else if (query.includes(':')) q = parseNql(query) // NQL
|
|
264
254
|
else {
|
|
265
255
|
let scanables = [...this.scanables]
|
|
266
256
|
if (scanables.length === 0) scanables = [...this.sortables]
|
|
@@ -273,8 +263,8 @@ export function buildFilterQuery (filter = {}) {
|
|
|
273
263
|
if (query[query.length - 1] === '*') return `${f}:~^'${query.replaceAll('*', '')}'`
|
|
274
264
|
return `${f}:~'${query.replaceAll('*', '')}'`
|
|
275
265
|
})
|
|
276
|
-
if (parts.length === 1) q =
|
|
277
|
-
else if (parts.length > 1) q =
|
|
266
|
+
if (parts.length === 1) q = parseNql(parts[0])
|
|
267
|
+
else if (parts.length > 1) q = parseNql(parts.join(','))
|
|
278
268
|
}
|
|
279
269
|
} catch (err) {
|
|
280
270
|
this.plugin.error('invalidQuery', { orgMessage: err.message })
|
|
@@ -366,16 +356,10 @@ export function buildFilterSearch (filter = {}) {
|
|
|
366
356
|
}
|
|
367
357
|
|
|
368
358
|
export function replaceIdInQuerySearch (filter) {
|
|
369
|
-
|
|
370
|
-
const query =
|
|
371
|
-
if (value instanceof RegExp) return ['__REGEXP__', value.source, value.flags]
|
|
372
|
-
return value
|
|
373
|
-
}).replaceAll('"id"', `"${this.driver.idField.name}"`)
|
|
359
|
+
const { replaceRegexInJson, reviveRegexInJson } = this.app.dobo
|
|
360
|
+
const query = replaceRegexInJson(filter.query).replaceAll('"id"', `"${this.driver.idField.name}"`)
|
|
374
361
|
try {
|
|
375
|
-
filter.query =
|
|
376
|
-
if (Array.isArray(value) && value[0] === '__REGEXP__') return new RegExp(value[1], value[2])
|
|
377
|
-
return value
|
|
378
|
-
})
|
|
362
|
+
filter.query = reviveRegexInJson(query)
|
|
379
363
|
} catch (err) {}
|
|
380
364
|
// search
|
|
381
365
|
const search = JSON.stringify(filter.search ?? {}).replaceAll('"id"', `"${this.driver.idField.name}"`)
|
|
@@ -16,8 +16,11 @@ async function countRecord (...args) {
|
|
|
16
16
|
result.warnings = result.warnings ?? []
|
|
17
17
|
result.warnings.push(t('hardCapWarning%s%s', result.data, hardCap))
|
|
18
18
|
result.orgCount = result.data
|
|
19
|
+
result.hardCapped = true
|
|
19
20
|
result.data = hardCap
|
|
20
21
|
}
|
|
22
|
+
const { warnings } = getDefaultValues(options)
|
|
23
|
+
if (!warnings) delete result.warnings
|
|
21
24
|
await execDynHook.call(this, 'afterCountRecord', filter, result, options)
|
|
22
25
|
await execModelHook.call(this, 'afterCountRecord', filter, result, options)
|
|
23
26
|
await execHook.call(this, 'afterCountRecord', filter, result, options)
|
|
@@ -6,10 +6,13 @@ async function createAggregate (...args) {
|
|
|
6
6
|
const [_filter = {}, params = {}, opts = {}] = args
|
|
7
7
|
const { dataOnly = true } = opts
|
|
8
8
|
const { filter, options } = await getFilterAndOptions.call(this, _filter, opts, action)
|
|
9
|
+
const { getDefaultValues } = this.app.dobo
|
|
9
10
|
await execHook.call(this, 'beforeCreateAggregate', filter, params, options)
|
|
10
11
|
await execModelHook.call(this, 'beforeCreateAggregate', filter, params, options)
|
|
11
12
|
await execDynHook.call(this, 'beforeCreateAggregate', filter, params, options)
|
|
12
13
|
const result = (await this.driver._createAggregate(this, filter, params, options)) ?? {}
|
|
14
|
+
const { warnings } = getDefaultValues(options)
|
|
15
|
+
if (!warnings) delete result.warnings
|
|
13
16
|
await execDynHook.call(this, 'afterCreateAggregate', filter, params, result, options)
|
|
14
17
|
await execModelHook.call(this, 'afterCreateAggregate', filter, params, result, options)
|
|
15
18
|
await execHook.call(this, 'afterCreateAggregate', filter, params, result, options)
|
|
@@ -7,17 +7,17 @@ async function createAttachment (...args) {
|
|
|
7
7
|
const [id, opts = {}] = args
|
|
8
8
|
const { fs } = this.app.lib
|
|
9
9
|
const { isEmpty } = this.app.lib._
|
|
10
|
-
const { source,
|
|
10
|
+
const { source, field = 'file', file, fullPath, stats, mimeType, req } = opts
|
|
11
11
|
if (isEmpty(file)) return
|
|
12
12
|
if (!source) throw this.plugin.error('isMissing%s', this.plugin.t('field.source'))
|
|
13
|
-
const baseDir = await getAttachmentPath.call(this, id,
|
|
14
|
-
let dir = `${baseDir}/${
|
|
15
|
-
if ((
|
|
13
|
+
const baseDir = await getAttachmentPath.call(this, id, field, file, { dirOnly: true })
|
|
14
|
+
let dir = `${baseDir}/${field}`
|
|
15
|
+
if ((field || '').endsWith('[]')) dir = `${baseDir}/${field.replace('[]', '')}`
|
|
16
16
|
const dest = `${dir}/${file}`.replaceAll('//', '/')
|
|
17
17
|
await fs.ensureDir(dir)
|
|
18
18
|
await fs.copy(source, dest)
|
|
19
19
|
const rec = {
|
|
20
|
-
field:
|
|
20
|
+
field: field === '' ? undefined : field,
|
|
21
21
|
dir,
|
|
22
22
|
file
|
|
23
23
|
}
|
|
@@ -6,10 +6,13 @@ async function createHistogram (...args) {
|
|
|
6
6
|
const [_filter = {}, params = {}, opts = {}] = args
|
|
7
7
|
const { dataOnly = true } = opts
|
|
8
8
|
const { filter, options } = await getFilterAndOptions.call(this, _filter, opts, action)
|
|
9
|
+
const { getDefaultValues } = this.app.dobo
|
|
9
10
|
await execHook.call(this, 'beforeCreateHistogram', filter, params, options)
|
|
10
11
|
await execModelHook.call(this, 'beforeCreateHistogram', filter, params, options)
|
|
11
12
|
await execDynHook.call(this, 'beforeCreateHistogram', filter, params, options)
|
|
12
13
|
const result = (await this.driver._createHistogram(this, filter, params, options)) ?? {}
|
|
14
|
+
const { warnings } = getDefaultValues(options)
|
|
15
|
+
if (!warnings) delete result.warnings
|
|
13
16
|
await execDynHook.call(this, 'afterCreateHistogram', filter, params, result, options)
|
|
14
17
|
await execModelHook.call(this, 'afterCreateHistogram', filter, params, result, options)
|
|
15
18
|
await execHook.call(this, 'afterCreateHistogram', filter, params, result, options)
|
|
@@ -6,6 +6,7 @@ const action = 'createRecord'
|
|
|
6
6
|
async function createRecord (...args) {
|
|
7
7
|
if (args.length === 0) return this.action(action, ...args)
|
|
8
8
|
const [body = {}, opts = {}] = args
|
|
9
|
+
const { getDefaultValues } = this.app.dobo
|
|
9
10
|
const { isSet } = this.app.lib.aneka
|
|
10
11
|
const { runHook } = this.app.bajo
|
|
11
12
|
const { cloneDeep, get } = this.app.lib._
|
|
@@ -24,6 +25,8 @@ async function createRecord (...args) {
|
|
|
24
25
|
return
|
|
25
26
|
}
|
|
26
27
|
result = result ?? {}
|
|
28
|
+
const { warnings } = getDefaultValues(options)
|
|
29
|
+
if (!warnings) delete result.warnings
|
|
27
30
|
if (!noResultSanitizer) result.data = await this.sanitizeRecord(result.data, options)
|
|
28
31
|
if (isSet(options.refs)) await getSingleRef.call(this, result.data, options)
|
|
29
32
|
await handleReq.call(this, result.data.id, 'created', options)
|
|
@@ -3,11 +3,13 @@ const action = 'findAllRecord'
|
|
|
3
3
|
|
|
4
4
|
async function native (...args) {
|
|
5
5
|
const { isSet } = this.app.lib.aneka
|
|
6
|
-
const {
|
|
6
|
+
const { getDefaultValues, t } = this.app.dobo
|
|
7
|
+
const { cloneDeep, pick, omit } = this.app.lib._
|
|
7
8
|
const [params = {}, opts = {}] = args
|
|
8
9
|
const { dataOnly = true } = opts
|
|
9
10
|
const { get: getCache, set: setCache } = this.app.bajoCache ?? {}
|
|
10
11
|
const { filter, options } = await getFilterAndOptions.call(this, params, opts, action)
|
|
12
|
+
const { hardCap, warnings } = getDefaultValues(options)
|
|
11
13
|
if (dataOnly) options.count = false
|
|
12
14
|
let { noResultSanitizer, noCache } = options
|
|
13
15
|
await execHook.call(this, 'beforeFindRecord', filter, options)
|
|
@@ -25,7 +27,19 @@ async function native (...args) {
|
|
|
25
27
|
return dataOnly ? result.data : result
|
|
26
28
|
}
|
|
27
29
|
}
|
|
28
|
-
|
|
30
|
+
let result = options.record ?? (await this.driver._findAllRecord(this, filter, options)) ?? {}
|
|
31
|
+
result.limit = filter.limit
|
|
32
|
+
result.filter = pick(filter, ['query', 'match', 'sort'])
|
|
33
|
+
result.warnings = result.warnings ?? []
|
|
34
|
+
if (!options.count) result = omit(result, ['count', 'pages'])
|
|
35
|
+
else if (options.count && result.count > hardCap) {
|
|
36
|
+
result.warnings.push(t('hardCapWarning%s%s', result.count, hardCap))
|
|
37
|
+
result.count = hardCap
|
|
38
|
+
result.hardCapped = true
|
|
39
|
+
}
|
|
40
|
+
result.pages = options.count ? Math.ceil(result.count / filter.limit) : undefined
|
|
41
|
+
if (!warnings) delete result.warnings
|
|
42
|
+
|
|
29
43
|
if (!noResultSanitizer) {
|
|
30
44
|
for (const idx in result.data) {
|
|
31
45
|
result.data[idx] = await this.sanitizeRecord(result.data[idx], options)
|
|
@@ -48,7 +62,7 @@ async function loop (...args) {
|
|
|
48
62
|
const [params = {}, opts = {}] = args
|
|
49
63
|
const { dataOnly = true } = opts
|
|
50
64
|
const { filter, options } = await getFilterAndOptions.call(this, params, opts, action)
|
|
51
|
-
const { maxLimit, hardCap } = getDefaultValues(options)
|
|
65
|
+
const { maxLimit, hardCap, warnings: showWarnings } = getDefaultValues(options)
|
|
52
66
|
const nFilter = cloneDeep(filter || {})
|
|
53
67
|
const nOptions = cloneOptions.call(this, options)
|
|
54
68
|
nOptions.count = false
|
|
@@ -71,7 +85,9 @@ async function loop (...args) {
|
|
|
71
85
|
count = count + result.data.length
|
|
72
86
|
nFilter.page++
|
|
73
87
|
}
|
|
74
|
-
|
|
88
|
+
const result = { data, count, warnings }
|
|
89
|
+
if (!showWarnings) delete result.warnings
|
|
90
|
+
return dataOnly ? data : result
|
|
75
91
|
}
|
|
76
92
|
|
|
77
93
|
async function findAllRecord (...args) {
|
|
@@ -14,12 +14,12 @@ async function findAttachment (...args) {
|
|
|
14
14
|
const recs = []
|
|
15
15
|
for (const f of files) {
|
|
16
16
|
const item = f.replace(dir, '')
|
|
17
|
-
let [,
|
|
17
|
+
let [, field, file] = item.split('/')
|
|
18
18
|
if (!file) {
|
|
19
|
-
file =
|
|
20
|
-
|
|
19
|
+
file = field
|
|
20
|
+
field = null
|
|
21
21
|
}
|
|
22
|
-
const rec = {
|
|
22
|
+
const rec = { field, file }
|
|
23
23
|
await mergeAttachmentInfo.call(this, rec, f, { mimeType, fullPath, stats })
|
|
24
24
|
recs.push(rec)
|
|
25
25
|
}
|
|
@@ -4,6 +4,7 @@ const action = 'findOneRecord'
|
|
|
4
4
|
|
|
5
5
|
async function findOneRecord (...args) {
|
|
6
6
|
if (args.length === 0) return this.action(action, ...args)
|
|
7
|
+
const { getDefaultValues } = this.app.dobo
|
|
7
8
|
const [params = {}, opts = {}] = args
|
|
8
9
|
const { cloneDeep } = this.app.lib._
|
|
9
10
|
const { dataOnly = true } = opts
|
|
@@ -13,9 +14,12 @@ async function findOneRecord (...args) {
|
|
|
13
14
|
nOptions.count = false
|
|
14
15
|
nOptions.dataOnly = false
|
|
15
16
|
nFilter.limit = 1
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
|
|
17
|
+
const { warnings } = getDefaultValues(nOptions)
|
|
18
|
+
const resp = await this.findRecord(nFilter, nOptions)
|
|
19
|
+
const data = resp.data[0]
|
|
20
|
+
const result = { data, cached: resp.cached, warnings: resp.warnings }
|
|
21
|
+
if (!warnings) delete result.warnings
|
|
22
|
+
return dataOnly ? data : result
|
|
19
23
|
}
|
|
20
24
|
|
|
21
25
|
export default findOneRecord
|
|
@@ -73,7 +73,7 @@ async function findRecord (...args) {
|
|
|
73
73
|
const { dataOnly = true } = opts
|
|
74
74
|
const { get: getCache, set: setCache } = this.app.bajoCache ?? {}
|
|
75
75
|
const { filter, options } = await getFilterAndOptions.call(this, params, opts, action)
|
|
76
|
-
const { hardCap } = getDefaultValues(options)
|
|
76
|
+
const { hardCap, warnings } = getDefaultValues(options)
|
|
77
77
|
if (dataOnly) options.count = false
|
|
78
78
|
let { noResultSanitizer, noCache } = options
|
|
79
79
|
await execHook.call(this, 'beforeFindRecord', filter, options)
|
|
@@ -100,8 +100,10 @@ async function findRecord (...args) {
|
|
|
100
100
|
else if (options.count && result.count > hardCap) {
|
|
101
101
|
result.warnings.push(t('hardCapWarning%s%s', result.count, hardCap))
|
|
102
102
|
result.count = hardCap
|
|
103
|
+
result.hardCapped = true
|
|
103
104
|
}
|
|
104
105
|
result.pages = options.count ? Math.ceil(result.count / filter.limit) : undefined
|
|
106
|
+
if (!warnings) delete result.warnings
|
|
105
107
|
|
|
106
108
|
if (!noResultSanitizer) {
|
|
107
109
|
for (const idx in result.data) {
|
|
@@ -3,11 +3,11 @@ const action = 'getAttachment'
|
|
|
3
3
|
async function getAttachment (...args) {
|
|
4
4
|
if (!this.attachment) return
|
|
5
5
|
if (args.length === 0) return this.action(action, ...args)
|
|
6
|
-
let [id,
|
|
6
|
+
let [id, field, file, opts = {}] = args
|
|
7
7
|
const { find } = this.app.lib._
|
|
8
8
|
const all = await this.findAttachment(id, opts)
|
|
9
|
-
if (
|
|
10
|
-
const data = find(all, {
|
|
9
|
+
if (field === 'null') field = null
|
|
10
|
+
const data = find(all, { field, file })
|
|
11
11
|
if (!data) throw this.error('notFound', { statusCode: 404 })
|
|
12
12
|
return data
|
|
13
13
|
}
|
|
@@ -45,6 +45,7 @@ const action = 'getRecord'
|
|
|
45
45
|
async function getRecord (...args) {
|
|
46
46
|
if (args.length === 0) return this.action(action, ...args)
|
|
47
47
|
let [id, opts = {}] = args
|
|
48
|
+
const { getDefaultValues } = this.app.dobo
|
|
48
49
|
const { isEmpty } = this.app.lib._
|
|
49
50
|
const { isSet } = this.app.lib.aneka
|
|
50
51
|
const { get: getCache, set: setCache } = this.app.bajoCache ?? {}
|
|
@@ -66,6 +67,8 @@ async function getRecord (...args) {
|
|
|
66
67
|
}
|
|
67
68
|
}
|
|
68
69
|
const result = options.record ?? (await this.driver._getRecord(this, id, options)) ?? {}
|
|
70
|
+
const { warnings } = getDefaultValues(options)
|
|
71
|
+
if (!warnings) delete result.warnings
|
|
69
72
|
if (isEmpty(result.data) && !options.throwNotFound) return dataOnly ? undefined : { data: undefined }
|
|
70
73
|
if (!noResultSanitizer) result.data = await this.sanitizeRecord(result.data, options)
|
|
71
74
|
if (isSet(options.refs)) await getSingleRef.call(this, result.data, options)
|
|
@@ -10,10 +10,10 @@ async function listAttachment (...args) {
|
|
|
10
10
|
const mime = await importPkg('waibu:mime')
|
|
11
11
|
const { fastGlob } = this.app.lib
|
|
12
12
|
|
|
13
|
-
const { id = '*',
|
|
13
|
+
const { id = '*', field = '*', file = '*', type } = params
|
|
14
14
|
const { uriEncoded = true } = opts
|
|
15
15
|
const root = `${getPluginDataDir('dobo')}/attachment`
|
|
16
|
-
let pattern = `${root}/${this.name}/${id}/${
|
|
16
|
+
let pattern = `${root}/${this.name}/${id}/${field}/${file}`
|
|
17
17
|
if (type === 'image') pattern += '.{jpg,jpeg,gif,png,webp,avif,svg}'
|
|
18
18
|
else if (type === 'video') pattern += '.{mp4,m4v,webm,mov,qt,mkv,ogg,ogv}'
|
|
19
19
|
else if (type) pattern += `.${type}`
|
|
@@ -26,12 +26,12 @@ async function listAttachment (...args) {
|
|
|
26
26
|
fileName: path.basename(fullPath),
|
|
27
27
|
fullPath,
|
|
28
28
|
mimeType,
|
|
29
|
-
params: { model: this.name, id,
|
|
29
|
+
params: { model: this.name, id, field, file }
|
|
30
30
|
}
|
|
31
31
|
if (this.app.waibuMpa) {
|
|
32
32
|
const { routePath } = this.app.waibu
|
|
33
|
-
const [, _model, _id,
|
|
34
|
-
row.url = routePath(`dobo:/attachment/${kebabCase(_model)}/${_id}/${
|
|
33
|
+
const [, _model, _id, _field, _file] = fullPath.split('/')
|
|
34
|
+
row.url = routePath(`dobo:/attachment/${kebabCase(_model)}/${_id}/${_field}/${_file}`)
|
|
35
35
|
}
|
|
36
36
|
return row
|
|
37
37
|
})
|
|
@@ -4,9 +4,9 @@ const action = 'removeAttachment'
|
|
|
4
4
|
async function removeAttachment (...args) {
|
|
5
5
|
if (!this.attachment) return
|
|
6
6
|
if (args.length === 0) return this.action(action, ...args)
|
|
7
|
-
const [id,
|
|
7
|
+
const [id, field, file, opts = {}] = args
|
|
8
8
|
const { fs } = this.app.lib
|
|
9
|
-
const path = await getAttachmentPath.call(this, id,
|
|
9
|
+
const path = await getAttachmentPath.call(this, id, field, file)
|
|
10
10
|
const { req } = opts
|
|
11
11
|
await fs.remove(path)
|
|
12
12
|
if (!opts.noFlash && req && req.flash) req.flash('notify', req.t('attachmentRemoved'))
|
|
@@ -34,6 +34,7 @@ const action = 'removeRecord'
|
|
|
34
34
|
async function removeRecord (...args) {
|
|
35
35
|
if (args.length === 0) return this.action(action, ...args)
|
|
36
36
|
let [id, opts = {}] = args
|
|
37
|
+
const { getDefaultValues } = this.app.dobo
|
|
37
38
|
const { isSet } = this.app.lib.aneka
|
|
38
39
|
const { runHook } = this.app.bajo
|
|
39
40
|
const { dataOnly = true } = opts
|
|
@@ -48,6 +49,8 @@ async function removeRecord (...args) {
|
|
|
48
49
|
await runHook('cache:clear', this, 'remove', id)
|
|
49
50
|
return
|
|
50
51
|
}
|
|
52
|
+
const { warnings } = getDefaultValues(options)
|
|
53
|
+
if (!warnings) delete result.warnings
|
|
51
54
|
if (!noResultSanitizer) result.oldData = await this.sanitizeRecord(result.oldData, options)
|
|
52
55
|
if (isSet(options.refs)) await getSingleRef.call(this, result.data, options)
|
|
53
56
|
await handleReq.call(this, result.oldData.id, 'removed', options)
|
|
@@ -11,9 +11,11 @@
|
|
|
11
11
|
*/
|
|
12
12
|
async function sanitizeRecord (record = {}, opts = {}) {
|
|
13
13
|
const { fields = [], hidden = [], forceNoHidden } = opts
|
|
14
|
-
const { isEmpty, map, without } = this.app.lib._
|
|
14
|
+
const { isEmpty, map, without, isArray } = this.app.lib._
|
|
15
15
|
const { fillObject } = this.app.lib.aneka
|
|
16
|
-
|
|
16
|
+
let allHidden = without([...this.hidden, ...hidden], 'id')
|
|
17
|
+
if (forceNoHidden === true) allHidden = []
|
|
18
|
+
else if (isArray(forceNoHidden)) allHidden = without(allHidden, ...forceNoHidden)
|
|
17
19
|
let newFields = [...fields]
|
|
18
20
|
if (isEmpty(newFields)) newFields = map(this.properties, prop => prop.name)
|
|
19
21
|
if (!newFields.includes('id')) newFields.unshift('id')
|
|
@@ -47,6 +47,7 @@ const action = 'updateRecord'
|
|
|
47
47
|
async function updateRecord (...args) {
|
|
48
48
|
if (args.length === 0) return this.action(action, ...args)
|
|
49
49
|
let [id, body = {}, opts = {}] = args
|
|
50
|
+
const { getDefaultValues } = this.app.dobo
|
|
50
51
|
const { isSet } = this.app.lib.aneka
|
|
51
52
|
const { runHook } = this.app.bajo
|
|
52
53
|
const { cloneDeep, get, omit } = this.app.lib._
|
|
@@ -69,6 +70,8 @@ async function updateRecord (...args) {
|
|
|
69
70
|
await runHook('cache:clear', this, 'update', id, body)
|
|
70
71
|
return
|
|
71
72
|
}
|
|
73
|
+
const { warnings } = getDefaultValues(options)
|
|
74
|
+
if (!warnings) delete result.warnings
|
|
72
75
|
if (!noResultSanitizer) {
|
|
73
76
|
result.data = await this.sanitizeRecord(result.data, options)
|
|
74
77
|
result.oldData = await this.sanitizeRecord(result.oldData, options)
|
|
@@ -3,6 +3,7 @@ const action = 'upsertRecord'
|
|
|
3
3
|
|
|
4
4
|
async function native (body = {}, opts = {}) {
|
|
5
5
|
const { isSet } = this.app.lib.aneka
|
|
6
|
+
const { getDefaultValues } = this.app.dobo
|
|
6
7
|
const { runHook } = this.app.bajo
|
|
7
8
|
const { cloneDeep, get } = this.app.lib._
|
|
8
9
|
const { dataOnly = true } = opts
|
|
@@ -19,6 +20,8 @@ async function native (body = {}, opts = {}) {
|
|
|
19
20
|
await runHook('cache:clear', this, 'upsert', body)
|
|
20
21
|
return
|
|
21
22
|
}
|
|
23
|
+
const { warnings } = getDefaultValues(options)
|
|
24
|
+
if (!warnings) delete result.warnings
|
|
22
25
|
if (!noResultSanitizer) result.data = await this.sanitizeRecord(result.data, options)
|
|
23
26
|
if (isSet(options.refs)) await getSingleRef.call(this, result.data, options)
|
|
24
27
|
await handleReq.call(this, result.data.id, 'upserted', options)
|
package/lib/factory/model.js
CHANGED
|
@@ -160,8 +160,8 @@ async function modelFactory () {
|
|
|
160
160
|
getField = (name) => this.getProperty(name)
|
|
161
161
|
hasField = (name) => this.hasProperty(name)
|
|
162
162
|
|
|
163
|
-
dispose () {
|
|
164
|
-
super.dispose()
|
|
163
|
+
dispose = async () => {
|
|
164
|
+
await super.dispose()
|
|
165
165
|
this.connection = null
|
|
166
166
|
this.driver = null
|
|
167
167
|
}
|
package/package.json
CHANGED
package/wiki/CHANGES.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changes
|
|
2
2
|
|
|
3
|
+
## 2026-04-07
|
|
4
|
+
|
|
5
|
+
- [2.15.0] Add ```parseNql()```
|
|
6
|
+
- [2.15.0] Add ```parseQuery()```
|
|
7
|
+
- [2.15.0] Add ```replaceRegexInJson()```
|
|
8
|
+
- [2.15.0] Add ```reviveRegexInJson()```
|
|
9
|
+
- [2.15.0] Change all ```opts.fieldName``` to ```opts.field``` in features
|
|
10
|
+
- [2.15.0] Add ```ref.searchField``` in model reference
|
|
11
|
+
- [2.15.0] Add ```ref.labelField``` in model reference
|
|
12
|
+
- [2.15.0] Add ```ref.valueField``` in model reference
|
|
13
|
+
- [2.15.0] change ```ref.propName``` to ```ref.field``` in model reference
|
|
14
|
+
|
|
15
|
+
## 2026-04-02
|
|
16
|
+
|
|
17
|
+
- [2.14.1] Bug fix in ```hardCap```
|
|
18
|
+
- [2.14.1] ```warnings``` now can be turned off through ```config``` or site settings
|
|
19
|
+
|
|
3
20
|
## 2026-04-01
|
|
4
21
|
|
|
5
22
|
- [2.14.0] Add ```between``` as custom query, since it doesn't exists in NQL
|