dobo 2.14.1 → 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 +60 -2
- 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/create-attachment.js +5 -5
- package/lib/factory/model/find-attachment.js +4 -4
- package/lib/factory/model/get-attachment.js +3 -3
- package/lib/factory/model/list-attachment.js +5 -5
- package/lib/factory/model/remove-attachment.js +2 -2
- 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,8 +151,8 @@ 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
|
-
maxPage:
|
|
154
|
-
sort: ['dt:-1', 'updatedAt:-1', '
|
|
154
|
+
maxPage: 400, // max allowed page number
|
|
155
|
+
sort: ['dt:-1', 'updatedAt:-1', 'createdAt:-1', 'ts:-1', 'username', 'name']
|
|
155
156
|
},
|
|
156
157
|
cache: {
|
|
157
158
|
ttlDur: '10s'
|
|
@@ -518,6 +519,63 @@ async function factory (pkgName) {
|
|
|
518
519
|
}
|
|
519
520
|
}
|
|
520
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
|
+
}
|
|
521
579
|
}
|
|
522
580
|
|
|
523
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}"`)
|
|
@@ -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
|
}
|
|
@@ -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
|
}
|
|
@@ -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
|
}
|
|
@@ -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'))
|
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
|