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.
@@ -1,9 +1,9 @@
1
1
  async function createdAt (opts = {}) {
2
- opts.fieldName = opts.fieldName ?? 'createdAt'
2
+ opts.field = opts.field ?? 'createdAt'
3
3
  opts.noOverwrite = opts.noOverwrite ?? false
4
4
  return {
5
5
  properties: [{
6
- name: opts.fieldName,
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.fieldName] = new Date()
15
- else if (!isSet(body[opts.fieldName])) body[opts.fieldName] = new Date()
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.fieldName = opts.fieldName ?? 'dt'
2
+ opts.field = opts.field ?? 'dt'
3
3
  return {
4
4
  properties: [{
5
- name: opts.fieldName ?? 'dt',
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.fieldName)
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.fieldName = opts.fieldName ?? '_immutable'
9
+ opts.field = opts.field ?? '_immutable'
10
10
  return {
11
11
  properties: {
12
- name: opts.fieldName,
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.fieldName, null))
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.fieldName])) throw this.error('recordNotFound%s%s', id, this.name, { statusCode: 404 })
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.fieldName]
19
+ delete body[opts.field]
20
20
  }
21
21
 
22
22
  async function removedAt (opts = {}) {
23
- opts.fieldName = opts.fieldName ?? '_removedAt'
23
+ opts.field = opts.field ?? '_removedAt'
24
24
  return {
25
25
  properties: {
26
- name: opts.fieldName,
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.fieldName, new Date())
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.fieldName = opts.fieldName ?? 'id'
5
+ opts.field = opts.field ?? 'id'
6
6
  opts.fields = opts.fields ?? []
7
7
  return {
8
8
  properties: [{
9
- name: opts.fieldName,
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.fieldName])
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.fieldName] = crypto.createHash('md5').update(JSON.stringify(item)).digest('hex')
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.fieldName = opts.fieldName ?? 'updatedAt'
3
+ opts.field = opts.field ?? 'updatedAt'
4
4
  opts.noOverwrite = opts.noOverwrite ?? false
5
5
  return {
6
6
  properties: {
7
- name: opts.fieldName,
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.fieldName] = new Date()
15
- else if (!isSet(body[opts.fieldName])) body[opts.fieldName] = new Date()
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.fieldName] = new Date()
21
- else if (!isSet(body[opts.fieldName])) body[opts.fieldName] = new Date()
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, fieldName: req.params.field, file: '*', type })) ?? []
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: 50, // max allowed page number
154
- 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']
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
@@ -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, camelCase } = this.app.lib._
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: camelCase(item) }
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.fieldName === 'id') {
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 = { propName: 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.propName })
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.propName}@${ref.model}`)
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 ?? '*'
@@ -13,9 +13,9 @@ const methods = {
13
13
  createHistogram: ['params'],
14
14
  createAttachment: ['id'],
15
15
  findAttachment: ['id'],
16
- getAttachment: ['id', 'fieldName', 'file'],
16
+ getAttachment: ['id', 'field', 'file'],
17
17
  listAttachment: ['params'],
18
- removeAttachment: ['id', 'fieldName', 'file'],
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
- fieldName = value => {
127
- this._fieldName = value
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
  }
@@ -50,8 +50,8 @@ async function connectionFactory () {
50
50
  if (client) this.client = client
51
51
  }
52
52
 
53
- dispose () {
54
- super.dispose()
53
+ dispose = async () => {
54
+ await super.dispose()
55
55
  this.driver = null
56
56
  }
57
57
  }
@@ -19,8 +19,8 @@ async function featureFactory () {
19
19
  this.handler = options.handler
20
20
  }
21
21
 
22
- dispose () {
23
- super.dispose()
22
+ dispose = async () => {
23
+ await super.dispose()
24
24
  this.name = null
25
25
  this.handler = null
26
26
  }
@@ -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, fieldName, file, options = {}) {
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 = fieldName ? `${dir}/${fieldName}/${file}` : `${dir}/${file}`
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 [fieldName, ...parts] = path.basename(f).split('@')
136
+ let [field, ...parts] = path.basename(f).split('@')
138
137
  if (parts.length === 0) continue
139
- fieldName = setField ?? fieldName
138
+ field = setField ?? field
140
139
  const file = setFile ?? parts.join('@')
141
- const opts = { source: f, fieldName, file, mimeType, stats, req }
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
- const query = {}
181
- query[ref.propName] = record[prop.name]
182
- if (ref.propName === 'id') query[ref.propName] = this.sanitizeId(query[ref.propName])
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
- const query = {}
220
- query[ref.propName] = { $in: matches }
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.propName] + '') === rec[prop.name] + '')
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.call(this, query) // NQL
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 = nql(parts[0]).parse()
277
- else if (parts.length > 1) q = nql(parts.join(',')).parse()
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
- // query
370
- const query = JSON.stringify(filter.query ?? {}, (key, value) => {
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 = JSON.parse(query, (key, value) => {
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, fieldName = 'file', file, fullPath, stats, mimeType, req } = opts
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, fieldName, file, { dirOnly: true })
14
- let dir = `${baseDir}/${fieldName}`
15
- if ((fieldName || '').endsWith('[]')) dir = `${baseDir}/${fieldName.replace('[]', '')}`
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: fieldName === '' ? undefined : fieldName,
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 [, fieldName, file] = item.split('/')
17
+ let [, field, file] = item.split('/')
18
18
  if (!file) {
19
- file = fieldName
20
- fieldName = null
19
+ file = field
20
+ field = null
21
21
  }
22
- const rec = { fieldName, file }
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, fieldName, file, opts = {}] = args
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 (fieldName === 'null') fieldName = null
10
- const data = find(all, { fieldName, file })
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 = '*', fieldName = '*', file = '*', type } = params
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}/${fieldName}/${file}`
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, fieldName, file }
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, _fieldName, _file] = fullPath.split('/')
34
- row.url = routePath(`dobo:/attachment/${kebabCase(_model)}/${_id}/${_fieldName}/${_file}`)
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, fieldName, file, opts = {}] = args
7
+ const [id, field, file, opts = {}] = args
8
8
  const { fs } = this.app.lib
9
- const path = await getAttachmentPath.call(this, id, fieldName, file)
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'))
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dobo",
3
- "version": "2.14.1",
3
+ "version": "2.15.0",
4
4
  "description": "DBMS for Bajo Framework",
5
5
  "main": "index.js",
6
6
  "scripts": {
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