dobo 2.22.0 → 2.23.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.
Files changed (37) hide show
  1. package/extend/bajo/intl/en-US.json +1 -1
  2. package/extend/bajo/intl/id.json +1 -0
  3. package/extend/dobo/driver/memory.js +26 -25
  4. package/extend/dobo/feature/created-at.js +14 -3
  5. package/extend/dobo/feature/image.js +14 -7
  6. package/extend/dobo/feature/removed-at.js +7 -0
  7. package/extend/dobo/feature/unique.js +18 -6
  8. package/extend/dobo/feature/updated-at.js +15 -5
  9. package/index.js +1 -1
  10. package/lib/collect-connections.js +3 -11
  11. package/lib/collect-drivers.js +2 -2
  12. package/lib/collect-features.js +2 -2
  13. package/lib/collect-models.js +25 -16
  14. package/lib/factory/action.js +0 -1
  15. package/lib/factory/connection.js +26 -4
  16. package/lib/factory/driver.js +34 -19
  17. package/lib/factory/feature.js +0 -1
  18. package/lib/factory/model/_util.js +37 -61
  19. package/lib/factory/model/{bulk-create-records.js → bulk-create-record.js} +9 -9
  20. package/lib/factory/model/create-attachment.js +1 -1
  21. package/lib/factory/model/create-record.js +2 -2
  22. package/lib/factory/model/find-all-record.js +9 -8
  23. package/lib/factory/model/find-attachment.js +1 -1
  24. package/lib/factory/model/find-record.js +3 -2
  25. package/lib/factory/model/get-attachment.js +1 -1
  26. package/lib/factory/model/get-record.js +2 -2
  27. package/lib/factory/model/list-attachment.js +1 -1
  28. package/lib/factory/model/load-fixtures.js +2 -2
  29. package/lib/factory/model/remove-attachment.js +1 -1
  30. package/lib/factory/model/remove-record.js +2 -2
  31. package/lib/factory/model/sanitize-body.js +10 -5
  32. package/lib/factory/model/sanitize-record.js +2 -1
  33. package/lib/factory/model/update-record.js +2 -2
  34. package/lib/factory/model/upsert-record.js +2 -2
  35. package/lib/factory/model.js +13 -3
  36. package/package.json +1 -1
  37. package/wiki/CHANGES.md +25 -3
@@ -141,7 +141,7 @@
141
141
  "invalidIdType%s%s": "Invalid ID type for '%s'. Please only use one of these: %s",
142
142
  "noDefaultConnection": "No default connection found. All models will use the built-in memory database instead",
143
143
  "recordImmutable%s%s": "Record with ID: '%s' & model: '%s' is immutable and can't be modified or deleted",
144
- "notSupported%s%s%s": "%s|upperFirst '%s' is not supported by driver '%s'",
144
+ "notSupportedDriver%s%s%s": "%s|upperFirst '%s' is not supported by driver '%s'",
145
145
  "inMemoryDb%s%s": "'%s' is an in-memory database, %s is not allowed",
146
146
  "buildOp": "'build' operation",
147
147
  "dropOp": "'drop' operation",
@@ -140,6 +140,7 @@
140
140
  "invalidIdType%s%s": "Invalid ID type for '%s'. Please only use one of these: %s",
141
141
  "noDefaultConnection": "No default connection found. All models will use the built-in memory database instead",
142
142
  "recordImmutable%s%s": "Data dengan ID: '%s' & model: '%s' adalah immutable dan tidak bisa diubah atau dihapus",
143
+ "notSupportedDriver%s%s%s": "%s|upperFirst '%s' tidak didukung oleh driver '%s'",
143
144
  "inMemoryDb%s%s": "'%s' adalah database in-memory, %s tidak diizinkan",
144
145
  "buildOp": "operasi 'build'",
145
146
  "dropOp": "operasi 'drop'",
@@ -11,7 +11,6 @@ async function memoryDriverFactory () {
11
11
  this.idGenerator = 'ulid'
12
12
  this.saving = true
13
13
  this.memory = true
14
- this.autoSave = []
15
14
  this.storage = {}
16
15
  this.support = {
17
16
  propType: {
@@ -21,19 +20,6 @@ async function memoryDriverFactory () {
21
20
  }
22
21
  }
23
22
 
24
- async _loadFromFile (model, dir) {
25
- const { fs } = this.app.lib
26
- this.autoSave.push(model.name)
27
- const file = `${dir}/${model.name}.json`
28
- if (!fs.existsSync(file)) return
29
- try {
30
- const data = fs.readFileSync(file, 'utf8')
31
- this.storage[model.name] = JSON.parse(data)
32
- } catch (err) {
33
- this.fatal('cantLoad%s%s', model.name, err.message)
34
- }
35
- }
36
-
37
23
  async sanitizeConnection (conn) {
38
24
  await super.sanitizeConnection(conn)
39
25
  conn.memory = true
@@ -41,27 +27,42 @@ async function memoryDriverFactory () {
41
27
 
42
28
  async connect (connection, noRebuild) {
43
29
  const conn = this.plugin.getConnection('memory')
44
- const models = this.plugin.getModelsByConnection(conn.name)
45
30
  const { getPluginDataDir } = this.app.bajo
46
31
  const { fs } = this.app.lib
47
- const pdir = `${getPluginDataDir(this.plugin.ns)}/memDb/data` // persistence dir
48
- fs.ensureDirSync(pdir)
49
- conn.autoSave = conn.autoSave ?? []
32
+ const dir = `${getPluginDataDir(this.plugin.ns)}/memDb/data` // persistence dir
33
+ fs.ensureDirSync(dir)
34
+ conn.persistences = conn.persistences ?? []
35
+
36
+ const models = this.plugin.getModelsByConnection(conn.name).map(model => {
37
+ if (conn.persistences.includes(model.name)) model.options.persistence = true
38
+ return model
39
+ })
40
+
50
41
  for (const model of models) {
51
42
  this.storage[model.name] = this.storage[model.name] ?? [] // init empty model
52
- if (conn.autoSave.includes(model.name)) await this._loadFromFile(model, pdir)
53
- await model.loadFixtures()
43
+ if (model.options.persistence) {
44
+ const file = `${dir}/${model.name}.json`
45
+ let data = []
46
+ if (fs.existsSync(file)) {
47
+ try {
48
+ data = JSON.parse(fs.readFileSync(file, 'utf8'))
49
+ } catch (err) {}
50
+ }
51
+ if (data.length === 0) await model.loadFixtures({ ignoreError: false })
52
+ else this.storage[model.name] = data
53
+ } else await model.loadFixtures({ ignoreError: false })
54
54
  }
55
- if (conn.autoSave.length === 0) return
56
55
  setInterval(() => {
57
56
  if (!this.saving) return
58
57
  this.saving = true
59
- for (const item of conn.autoSave) {
60
- const data = this.storage[item]
61
- fs.writeFileSync(`${pdir}/${item}.json`, JSON.stringify(data), 'utf8')
58
+ for (const model of models) {
59
+ if (!model.options.persistence) continue
60
+ try {
61
+ fs.writeFileSync(`${dir}/${model.name}.json`, JSON.stringify(this.storage[model.name], null, 2), 'utf8')
62
+ } catch (err) {}
62
63
  }
63
64
  this.saving = false
64
- }, this.plugin.config.memDb.autoSaveDur)
65
+ }, this.plugin.config.memDb.persistenceDur)
65
66
  }
66
67
 
67
68
  async _getOldRecord (model, id, options = {}) {
@@ -1,3 +1,9 @@
1
+ async function handler (body, options, opts) {
2
+ const { isSet } = this.app.lib.aneka
3
+ if (opts.noOverwrite) body[opts.field] = new Date()
4
+ else if (!isSet(body[opts.field])) body[opts.field] = new Date()
5
+ }
6
+
1
7
  async function createdAt (opts = {}) {
2
8
  opts.field = opts.field ?? 'createdAt'
3
9
  opts.noOverwrite = opts.noOverwrite ?? false
@@ -10,9 +16,14 @@ async function createdAt (opts = {}) {
10
16
  hooks: [{
11
17
  name: 'beforeCreateRecord',
12
18
  handler: async function (body, options) {
13
- const { isSet } = this.app.lib.aneka
14
- if (opts.noOverwrite) body[opts.field] = new Date()
15
- else if (!isSet(body[opts.field])) body[opts.field] = new Date()
19
+ await handler.call(this, body, options, opts)
20
+ }
21
+ }, {
22
+ name: 'beforeBulkCreateRecord',
23
+ handler: async function (bodies, options) {
24
+ for (const body of bodies) {
25
+ await handler.call(this, body, options, opts)
26
+ }
16
27
  }
17
28
  }]
18
29
  }
@@ -1,5 +1,15 @@
1
1
  import path from 'path'
2
2
 
3
+ async function handler (val, rec, opts) {
4
+ const atts = await this.listAttachment({ id: rec.id })
5
+ if (atts.length === 0) return
6
+ let items = atts.filter(att => att.mimeType.startsWith('image/'))
7
+ if (opts.asLink && this.app.waibu) items = items.map(f => `<a href="${f.url}">${opts.baseName ? path.basename(f.file) : f.file}</a>`)
8
+ else if (opts.baseName) items = items.map(f => path.basename(f.file))
9
+ if (opts.single) return items[0]
10
+ return opts.returnAsArray ? items : items.join(', ')
11
+ }
12
+
3
13
  async function image (opts = {}) {
4
14
  opts.field = opts.field ?? 'image'
5
15
  opts.baseName = opts.baseName ?? true
@@ -10,13 +20,10 @@ async function image (opts = {}) {
10
20
  type: 'string',
11
21
  virtual: true,
12
22
  getValue: async function (val, rec) {
13
- const atts = await this.listAttachment({ id: rec.id })
14
- if (atts.length === 0) return
15
- let items = atts.filter(att => att.mimeType.startsWith('image/'))
16
- if (opts.withLink && this.app.waibu) items = items.map(f => `<a href="${f.url}">${opts.baseName ? path.basename(f.file) : f.file}</a>`)
17
- else if (opts.baseName) items = items.map(f => path.basename(f.file))
18
- if (opts.single) return items[0]
19
- return items
23
+ return await handler.call(this, val, rec, { ...opts, asLink: false })
24
+ },
25
+ format: async function (val, rec) {
26
+ return await handler.call(this, val, rec, { ...opts, asLink: true })
20
27
  }
21
28
  }
22
29
  }
@@ -43,6 +43,13 @@ async function removedAt (opts = {}) {
43
43
  handler: async function (body, options) {
44
44
  await beforeCreateRecord.call(this, { body }, opts)
45
45
  }
46
+ }, {
47
+ name: 'beforeBulkCreateRecord',
48
+ handler: async function (bodies, options) {
49
+ for (const body of bodies) {
50
+ await beforeCreateRecord.call(this, { body }, opts)
51
+ }
52
+ }
46
53
  }, {
47
54
  name: 'beforeUpdateRecord',
48
55
  handler: async function (id, body, options) {
@@ -1,7 +1,16 @@
1
1
  import crypto from 'crypto'
2
2
 
3
- async function unique (opts = {}) {
3
+ async function handler (body, options, opts) {
4
4
  const { omit } = this.app.lib._
5
+ if (opts.fields.length === 0) opts.fields = omit(this.properties.map(prop => prop.name), [opts.field])
6
+ const item = {}
7
+ for (const f of opts.fields) {
8
+ item[f] = body[f]
9
+ }
10
+ body[opts.field] = crypto.createHash('md5').update(JSON.stringify(item)).digest('hex')
11
+ }
12
+
13
+ async function unique (opts = {}) {
5
14
  opts.field = opts.field ?? 'id'
6
15
  opts.fields = opts.fields ?? []
7
16
  return {
@@ -16,12 +25,15 @@ async function unique (opts = {}) {
16
25
  name: 'beforeCreateRecord',
17
26
  level: 1000,
18
27
  handler: async function (body, options) {
19
- if (opts.fields.length === 0) opts.fields = omit(this.properties.map(prop => prop.name), [opts.field])
20
- const item = {}
21
- for (const f of opts.fields) {
22
- item[f] = body[f]
28
+ await handler.call(this, body, options, opts)
29
+ }
30
+ }, {
31
+ name: 'beforeBulkCreateRecord',
32
+ level: 1000,
33
+ handler: async function (bodies, options) {
34
+ for (const body of bodies) {
35
+ await handler.call(this, body, options, opts)
23
36
  }
24
- body[opts.field] = crypto.createHash('md5').update(JSON.stringify(item)).digest('hex')
25
37
  }
26
38
  }]
27
39
  }
@@ -1,5 +1,10 @@
1
- async function updatedAt (opts = {}) {
1
+ async function createHandler (body, options, opts) {
2
2
  const { isSet } = this.app.lib.aneka
3
+ if (opts.noOverwrite) body[opts.field] = new Date()
4
+ else if (!isSet(body[opts.field])) body[opts.field] = new Date()
5
+ }
6
+
7
+ async function updatedAt (opts = {}) {
3
8
  opts.field = opts.field ?? 'updatedAt'
4
9
  opts.noOverwrite = opts.noOverwrite ?? false
5
10
  return {
@@ -11,14 +16,19 @@ async function updatedAt (opts = {}) {
11
16
  hooks: [{
12
17
  name: 'beforeCreateRecord',
13
18
  handler: async function (body, options) {
14
- if (opts.noOverwrite) body[opts.field] = new Date()
15
- else if (!isSet(body[opts.field])) body[opts.field] = new Date()
19
+ await createHandler.call(this, body, options, opts)
20
+ }
21
+ }, {
22
+ name: 'beforeBulkCreateRecord',
23
+ handler: async function (bodies, options) {
24
+ for (const body of bodies) {
25
+ await createHandler.call(this, body, options, opts)
26
+ }
16
27
  }
17
28
  }, {
18
29
  name: 'beforeUpdateRecord',
19
30
  handler: async function (id, body, options) {
20
- if (opts.noOverwrite) body[opts.field] = new Date()
21
- else if (!isSet(body[opts.field])) body[opts.field] = new Date()
31
+ await createHandler.call(this, body, options, opts)
22
32
  }
23
33
  }]
24
34
  }
package/index.js CHANGED
@@ -164,7 +164,7 @@ async function factory (pkgName) {
164
164
  }
165
165
  },
166
166
  memDb: {
167
- autoSaveDur: '1s'
167
+ persistenceDur: '1s'
168
168
  },
169
169
  applet: {
170
170
  confirmation: false
@@ -31,21 +31,13 @@ async function collectConnections () {
31
31
  const { buildCollections } = this.app.bajo
32
32
  const { pullAt } = this.app.lib._
33
33
  const { filterIndex } = this.app.lib.aneka
34
- const DoboConnection = await connectionFactory.call(this)
34
+ await connectionFactory.call(this)
35
35
 
36
36
  async function handler ({ item }) {
37
37
  const { has } = this.app.lib._
38
38
  if (!has(item, 'driver')) this.fatal('unknownDbDriver%s')
39
- let driver
40
- try {
41
- driver = this.getDriver(item.driver, true)
42
- if (!driver) throw new Error()
43
- } catch (err) {
44
- this.fatal('unknownDbDriver%s', item.driver)
45
- }
46
- await driver.sanitizeConnection(item)
47
- const conn = new DoboConnection(this, item)
48
- conn.driver = driver
39
+ const conn = new this.app.baseClass.DoboConnection(this, item)
40
+ await conn.initDriver(item.driver)
49
41
  return conn
50
42
  }
51
43
  const memIndexes = filterIndex(this.config.connections, current => current.driver === 'dobo:memory' || current.name === 'memory')
@@ -13,7 +13,7 @@ async function collectDrivers () {
13
13
  const { eachPlugins, runHook } = this.app.bajo
14
14
  const { importModule } = this.app.bajo
15
15
  const { camelCase, isFunction } = this.app.lib._
16
- const DoboDriver = await driverFactory.call(this)
16
+ await driverFactory.call(this)
17
17
 
18
18
  this.log.trace('collecting%s', this.t('driver'))
19
19
  const me = this
@@ -24,7 +24,7 @@ async function collectDrivers () {
24
24
  if (!isFunction(factory)) this.fatal('invalidDriverClassFactory%s%s', this.ns, name)
25
25
  const Cls = await factory.call(this)
26
26
  const instance = new Cls(this, name)
27
- if (!(instance instanceof DoboDriver)) this.fatal('invalidDriverClass%s%s', this.ns, name)
27
+ if (!(instance instanceof this.app.baseClass.DoboDriver)) this.fatal('invalidDriverClass%s%s', this.ns, name)
28
28
  me.drivers.push(instance)
29
29
  me.log.trace('- %s:%s', this.ns, name)
30
30
  }, { glob: 'driver/*.js', prefix: this.ns })
@@ -20,7 +20,7 @@ import featureFactory from './factory/feature.js'
20
20
  */
21
21
  async function collectFeature () {
22
22
  const { eachPlugins } = this.app.bajo
23
- const DoboFeature = await featureFactory.call(this)
23
+ await featureFactory.call(this)
24
24
 
25
25
  this.log.trace('collecting%s', this.t('feature'))
26
26
  const me = this
@@ -31,7 +31,7 @@ async function collectFeature () {
31
31
  const name = camelCase(path.basename(file, '.js'))
32
32
  const handler = await importModule(file)
33
33
  if (!isFunction(handler)) this.fatal('invalidFeatureHandler%s%s', this.ns, name)
34
- me.features.push(new DoboFeature(this, { name, handler }))
34
+ me.features.push(new this.app.baseClass.DoboFeature(this, { name, handler }))
35
35
  me.log.trace('- %s:%s', this.ns, name)
36
36
  }, { glob: 'feature/*.js', prefix: this.ns })
37
37
  this.log.debug('collected%s%d', this.t('feature'), this.features.length)
@@ -247,8 +247,6 @@ export async function sanitizeAll (model) {
247
247
  async function createSchema (item) {
248
248
  const { find, orderBy, get } = this.app.lib._
249
249
  const { mergeObjectsByKey, defaultsDeep, parseObject } = this.app.lib.aneka
250
- if (item.file && !item.base) item.base = path.basename(item.file, path.extname(item.file))
251
- item.attachment = item.attachment ?? true
252
250
  const feats = item.features ?? []
253
251
  const props = item.properties ?? []
254
252
  const indexes = item.indexes ?? []
@@ -258,21 +256,23 @@ async function createSchema (item) {
258
256
  item.hidden = item.hidden ?? []
259
257
  item.rules = item.rules ?? []
260
258
  item.buildLevel = item.buildLevel ?? 999
261
- const conn = item.connection ?? 'default'
262
- item.connection = null
263
259
  item.hooks = item.hooks ?? []
264
260
  item.disabled = item.disabled ?? []
265
261
  item.scanables = item.scanables ?? []
266
262
  if (item.disabled === 'all') item.disabled = ['find', 'get', 'create', 'update', 'remove']
267
263
  else if (item.disabled === 'readonly') item.disabled = ['create', 'update', 'remove']
268
- // Is there any overwritten connection?
269
- const newConn = find(this.connections, c => c.options.models.includes(item.name))
270
- if (newConn) item.connection = newConn
271
- else {
272
- item.connection = this.getConnection(conn, true)
273
- if (!item.connection && conn === 'default') item.connection = this.getConnection('memory')
264
+ const conn = item.connection ?? 'default'
265
+ if (!(conn instanceof this.app.baseClass.DoboConnection)) {
266
+ item.connection = null
267
+ // Is there any overwritten connection?
268
+ const newConn = find(this.connections, c => c.options.models.includes(item.name))
269
+ if (newConn) item.connection = newConn
270
+ else {
271
+ item.connection = this.getConnection(conn, true)
272
+ if (!item.connection && conn === 'default') item.connection = this.getConnection('memory')
273
+ }
274
+ if (!item.connection) this.fatal('unknownConn%s%s', conn, item.name)
274
275
  }
275
- if (!item.connection) this.fatal('unknownConn%s%s', conn, item.name)
276
276
  // cache settings
277
277
  const defCache = defaultsDeep({}, item.connection.options.cache, get(this, 'app.bajoCache.config.default', this.config.default.cache))
278
278
  if (item.cache === false) item.cache = { ttlDur: 0 }
@@ -305,9 +305,9 @@ async function createSchema (item) {
305
305
  */
306
306
  async function collectModels () {
307
307
  const { eachPlugins } = this.app.bajo
308
- const { orderBy, has } = this.app.lib._
308
+ const { orderBy, has, isFunction, omit } = this.app.lib._
309
309
  await actionFactory.call(this)
310
- const DoboModel = await modelFactory.call(this)
310
+ await modelFactory.call(this)
311
311
 
312
312
  this.log.trace('collecting%s', this.t('model'))
313
313
  const me = this
@@ -325,8 +325,13 @@ async function collectModels () {
325
325
  item.name = item.name ?? defName
326
326
  me.log.trace('- %s', item.name)
327
327
  item.collName = item.collName ?? item.name
328
- item.file = file
328
+ item.options = item.options ?? {}
329
+ item.options.attachment = item.options.attachment ?? true
330
+ item.options.persistence = item.options.persistence ?? true
331
+ item.options.file = file
332
+ item.options.base = item.options.base ?? path.basename(file, path.extname(file))
329
333
  item.ns = this.ns
334
+ if (isFunction(item.buildStart)) await item.buildStart.call(me, item)
330
335
  const schema = await createSchema.call(me, item)
331
336
  schemas.push(schema)
332
337
  }, { glob: 'model/*.*', prefix: this.ns })
@@ -337,11 +342,13 @@ async function collectModels () {
337
342
  const idProp = schema.properties.find(p => p.name === 'id')
338
343
  if (!this.constructor.idTypes.includes(idProp.type)) this.fatal('invalidIdType%s%s', schema.name, this.constructor.idTypes.join(', '))
339
344
  if (idProp.type === 'string' && !has(idProp, 'maxLength')) idProp.maxLength = 50
340
- const model = new DoboModel(plugin, schema)
345
+ const model = new this.app.baseClass.DoboModel(plugin, omit(schema, ['beforeCreate', 'afterCreate']))
346
+ schema.model = model
341
347
  me.models.push(model)
342
348
  }
343
349
  // last sanitizing & checking
344
- for (const model of me.models) {
350
+ for (const schema of schemas) {
351
+ const model = schema.model
345
352
  await sanitizeRef.call(this, model, me.models)
346
353
  for (const item of model.indexes) {
347
354
  for (const field of item.fields) {
@@ -353,7 +360,9 @@ async function collectModels () {
353
360
  const prop = model.properties.find(p => p.name === field)
354
361
  if (!prop || (prop && prop.virtual)) throw this.error('virtualFieldIn%s%s%s', field, 'scanable', model.name)
355
362
  }
363
+ if (isFunction(schema.buildEnd)) await schema.buildEnd.call(me, model)
356
364
  }
365
+ schemas = []
357
366
  this.log.debug('collected%s%d', this.t('model'), this.models.length)
358
367
  }
359
368
 
@@ -155,7 +155,6 @@ async function actionFactory () {
155
155
  }
156
156
 
157
157
  this.app.baseClass.DoboAction = DoboAction
158
- return DoboAction
159
158
  }
160
159
 
161
160
  export default actionFactory
@@ -28,15 +28,37 @@ async function connectionFactory () {
28
28
  * @type {string}
29
29
  */
30
30
  this.name = options.name
31
+ this.options = {
32
+ models: []
33
+ }
31
34
 
32
35
  /**
33
36
  * Options object from connection defined on ```dobo.config.connections```
34
37
  *
35
38
  * @type {Object}
36
39
  */
37
- this.options = omit(options, ['name', 'driver'])
38
- this.options.connName = options.name
39
- this.options.models = this.options.models ?? []
40
+ if (options instanceof this.app.baseClass.DoboNullDriver) {
41
+ this.driver = options
42
+ this.options.connName = 'nulldriver'
43
+ } else {
44
+ this.options = omit(options, ['name', 'driver'])
45
+ this.options.connName = options.name
46
+ this.options.models = this.options.models ?? []
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Init driver. Called automatically during connections collection
52
+ *
53
+ * @param {strin} name - Driver name
54
+ * @async
55
+ */
56
+ async initDriver (name) {
57
+ if (name instanceof this.app.baseClass.DoboDriver) this.driver = name
58
+ else {
59
+ this.driver = this.plugin.getDriver(name)
60
+ await this.driver.sanitizeConnection(this.options)
61
+ }
40
62
  }
41
63
 
42
64
  /**
@@ -48,6 +70,7 @@ async function connectionFactory () {
48
70
  async connect (noRebuild) {
49
71
  const client = await this.driver.connect(this, noRebuild)
50
72
  if (client) this.client = client
73
+ this.connected = true
51
74
  }
52
75
 
53
76
  dispose = async () => {
@@ -57,7 +80,6 @@ async function connectionFactory () {
57
80
  }
58
81
 
59
82
  this.app.baseClass.DoboConnection = DoboConnection
60
- return DoboConnection
61
83
  }
62
84
 
63
85
  export default connectionFactory
@@ -59,11 +59,13 @@ async function driverFactory () {
59
59
  sanitizeBody (model, body = {}, partial) {
60
60
  const { keys, pick } = this.app.lib._
61
61
  const item = cloneDeep(body)
62
+ let newId = false
62
63
  if (has(item, 'id') && this.idField.name !== 'id') {
63
64
  item[this.idField.name] = item.id
64
- delete item.id
65
+ newId = true
65
66
  }
66
67
  for (const prop of model.properties) {
68
+ if (item[prop.name] === 'null') item[prop.name] = null
67
69
  if (!isSet(item[prop.name]) && !this.support.nullableField) {
68
70
  switch (prop.type) {
69
71
  case 'datetime': item[prop.name] = new Date(0); break
@@ -80,7 +82,9 @@ async function driverFactory () {
80
82
  else if (['object', 'array'].includes(prop.type)) item[prop.name] = JSON.stringify(item[prop.name])
81
83
  }
82
84
  }
83
- return partial ? pick(item, keys(body)) : item
85
+ const result = partial ? pick(item, keys(body)) : item
86
+ if (newId) delete result.id
87
+ return result
84
88
  }
85
89
 
86
90
  sanitizeRecord (model, record = {}) {
@@ -259,7 +263,7 @@ async function driverFactory () {
259
263
  }
260
264
  const items = chunk(bodies, chunkSize)
261
265
  for (const item of items) {
262
- await this.bulkCreateRecords(model, item, options)
266
+ await this.bulkCreateRecord(model, item, options)
263
267
  }
264
268
  }
265
269
 
@@ -402,64 +406,75 @@ async function driverFactory () {
402
406
  }
403
407
 
404
408
  async modelExists (model, options = {}) {
405
- throw this.plugin.error('notSupported%s%s', this.app.t('method'), 'modelExists')
409
+ throw this.plugin.error('notSupportedDriver%s%s%s', this.app.t('method'), 'modelExists', this.name)
406
410
  }
407
411
 
408
412
  async buildModel (model, options = {}) {
409
- throw this.plugin.error('notSupported%s%s', this.app.t('method'), 'buildModel')
413
+ throw this.plugin.error('notSupportedDriver%s%s%s', this.app.t('method'), 'buildModel', this.name)
410
414
  }
411
415
 
412
416
  async dropModel (model, options = {}) {
413
- throw this.plugin.error('notSupported%s%s', this.app.t('method'), 'dropModel')
417
+ throw this.plugin.error('notSupportedDriver%s%s%s', this.app.t('method'), 'dropModel', this.name)
414
418
  }
415
419
 
416
420
  async createRecord (model, body = {}, options = {}) {
417
- throw this.plugin.error('notSupported%s%s', this.app.t('method'), 'createRecord')
421
+ throw this.plugin.error('notSupportedDriver%s%s%s', this.app.t('method'), 'createRecord', this.name)
418
422
  }
419
423
 
420
424
  async getRecord (model, id, options = {}) {
421
- throw this.plugin.error('notSupported%s%s', this.app.t('method'), 'getRecord')
425
+ throw this.plugin.error('notSupportedDriver%s%s%s', this.app.t('method'), 'getRecord', this.name)
422
426
  }
423
427
 
424
428
  async updateRecord (model, id, body = {}, options = {}) {
425
- throw this.plugin.error('notSupported%s%s', this.app.t('method'), 'updateRecord')
429
+ throw this.plugin.error('notSupportedDriver%s%s%s', this.app.t('method'), 'updateRecord', this.name)
426
430
  }
427
431
 
428
432
  async removeRecord (model, id, options = {}) {
429
- throw this.plugin.error('notSupported%s%s', this.app.t('method'), 'removeRecord')
433
+ throw this.plugin.error('notSupportedDriver%s%s%s', this.app.t('method'), 'removeRecord', this.name)
430
434
  }
431
435
 
432
436
  async clearRecord (model, options = {}) {
433
- throw this.plugin.error('notSupported%s%s', this.app.t('method'), 'clearRecord')
437
+ throw this.plugin.error('notSupportedDriver%s%s%s', this.app.t('method'), 'clearRecord', this.name)
434
438
  }
435
439
 
436
440
  async findRecord (model, filter = {}, options = {}) {
437
- throw this.plugin.error('notSupported%s%s', this.app.t('method'), 'findRecord')
441
+ throw this.plugin.error('notSupportedDriver%s%s%s', this.app.t('method'), 'findRecord', this.name)
438
442
  }
439
443
 
440
- async bulkCreateRecords (model, bodies = [], options = {}) {
441
- throw this.plugin.error('notSupported%s%s', this.app.t('method'), 'bulkCreateRecords')
444
+ async bulkCreateRecord (model, bodies = [], options = {}) {
445
+ throw this.plugin.error('notSupportedDriver%s%s%s', this.app.t('method'), 'bulkCreateRecord', this.name)
442
446
  }
443
447
 
444
448
  async countRecord (model, filter = {}, options = {}) {
445
- throw this.plugin.error('notSupported%s%s', this.app.t('method'), 'countRecord')
449
+ throw this.plugin.error('notSupportedDriver%s%s%s', this.app.t('method'), 'countRecord', this.name)
446
450
  }
447
451
 
448
452
  async createAggregate (model, filter = {}, params = {}, options = {}) {
449
- throw this.plugin.error('notSupported%s%s', this.app.t('method'), 'createAggregate')
453
+ throw this.plugin.error('notSupportedDriver%s%s%s', this.app.t('method'), 'createAggregate', this.name)
450
454
  }
451
455
 
452
456
  async createHistogram (model, filter = {}, params = {}, options = {}) {
453
- throw this.plugin.error('notSupported%s%s', this.app.t('method'), 'createHistogram')
457
+ throw this.plugin.error('notSupportedDriver%s%s%s', this.app.t('method'), 'createHistogram', this.name)
454
458
  }
455
459
 
456
460
  async transaction (model, handler, ...args) {
457
- throw this.plugin.error('notSupported%s%s', this.app.t('method'), 'transaction')
461
+ throw this.plugin.error('notSupportedDriver%s%s%s', this.app.t('method'), 'transaction', this.name)
462
+ }
463
+
464
+ async dispose () {
465
+ await super.dispose()
466
+ }
467
+ }
468
+
469
+ class DoboNullDriver extends DoboDriver {
470
+ constructor (plugin, name = 'null', options = {}) {
471
+ super(plugin, name, options)
472
+ this.memory = true
458
473
  }
459
474
  }
460
475
 
461
476
  this.app.baseClass.DoboDriver = DoboDriver
462
- return DoboDriver
477
+ this.app.baseClass.DoboNullDriver = DoboNullDriver
463
478
  }
464
479
 
465
480
  export default driverFactory
@@ -27,7 +27,6 @@ async function featureFactory () {
27
27
  }
28
28
 
29
29
  this.app.baseClass.DoboFeature = DoboFeature
30
- return DoboFeature
31
30
  }
32
31
 
33
32
  export default featureFactory
@@ -13,12 +13,18 @@ export function cloneOptions (options = {}) {
13
13
 
14
14
  export async function execHook (name, ...args) {
15
15
  const { runHook } = this.app.bajo
16
- const { camelCase, last } = this.app.lib._
16
+ const { camelCase, last, kebabCase } = this.app.lib._
17
17
  const { noHook } = last(args)
18
18
  const { ns } = this.app.dobo
19
+ let [prefix, ...action] = kebabCase(name).split('-')
20
+ action = camelCase(action.join(' '))
19
21
  if (!noHook) {
22
+ if (prefix === 'before') await runHook(`${ns}:beforeAction`, action, ...args)
20
23
  await runHook(`${ns}:${name}`, this.name, ...args)
24
+ if (prefix === 'after') await runHook(`${ns}:afterAction`, action, ...args)
25
+ if (prefix === 'before') await runHook(`${ns}.${camelCase(this.name)}:beforeAction`, action, ...args)
21
26
  await runHook(`${ns}.${camelCase(this.name)}:${name}`, ...args)
27
+ if (prefix === 'after') await runHook(`${ns}.${camelCase(this.name)}:afterAction`, action, ...args)
22
28
  }
23
29
  }
24
30
 
@@ -135,7 +141,7 @@ export async function getAttachmentPath (id, field, file, options = {}) {
135
141
 
136
142
  export async function copyAttachment (id, options = {}) {
137
143
  if (!this.app.waibu) return
138
- if (!this.attachment) return
144
+ if (!this.options.attachment) return
139
145
  const { fs } = this.app.lib
140
146
  const { req, setField, setFile, mimeType, stats } = options
141
147
  const { dir, files } = await this.app.waibu.getUploadedFiles(req.id, false, true)
@@ -158,7 +164,7 @@ export async function copyAttachment (id, options = {}) {
158
164
  }
159
165
 
160
166
  export async function handleAttachmentUpload (id, trigger, options = {}) {
161
- if (!this.attachment) return
167
+ if (!this.options.attachment) return
162
168
  const { getPluginDataDir } = this.app.bajo
163
169
  const { fs } = this.app.lib
164
170
  const { req, mimeType, stats, setFile, setField } = options
@@ -170,53 +176,7 @@ export async function handleAttachmentUpload (id, trigger, options = {}) {
170
176
  return copyAttachment.call(this, id, { req, mimeType, stats, setFile, setField })
171
177
  }
172
178
 
173
- async function _getRef ({ ref, rModel, prop, key, options, filter } = {}) {
174
- if (!((typeof options.refs === 'string' && ['*', 'all'].includes(options.refs)) || options.refs.includes(key))) return
175
- if (ref.fields.length === 0) return
176
- const { fmt } = options
177
- const fields = [...ref.fields]
178
- if (!fields.includes(prop.name)) fields.push(prop.name)
179
- const rOptions = { dataOnly: true, refs: [], fmt, fields }
180
- const results = await rModel.findRecord(filter, rOptions)
181
- return { rOptions, results }
182
- }
183
-
184
- export async function getSingleRef (record = {}, options = {}) {
185
- const { isSet } = this.app.lib.aneka
186
- const { parseQuery } = this.app.dobo
187
- const { get } = this.app.lib._
188
- const props = this.properties.filter(p => isSet(p.ref) && !(options.hidden ?? []).includes(p.name))
189
- const refs = {}
190
- options.refs = options.refs ?? []
191
- if (props.length > 0) {
192
- for (const prop of props) {
193
- for (const key in prop.ref) {
194
- try {
195
- if (get(record, `_ref.${key}`)) continue
196
- const ref = prop.ref[key]
197
- const rModel = this.app.dobo.getModel(ref.model, true)
198
- if (!rModel) return
199
- let query = {}
200
- query[ref.field] = record[prop.name]
201
- if (ref.field === 'id') query[ref.field] = this.sanitizeId(query[ref.field])
202
- if (ref.query) query = { $and: [query, parseQuery(ref.query, rModel)] }
203
- const filter = { query }
204
- const resp = await _getRef.call(this, { ref, rModel, prop, key, options, filter })
205
- if (!resp) continue
206
- const { rOptions, results } = resp
207
- const data = []
208
- for (const res of results) {
209
- data.push(await rModel.sanitizeRecord(res, rOptions))
210
- }
211
- refs[key] = ['1:1'].includes(ref.type) ? data[0] : data
212
- } catch (err) {}
213
- }
214
- }
215
- }
216
- record._ref = refs
217
- }
218
-
219
- export async function getMultiRefs (records = [], options = {}) {
179
+ export async function getRefs (records = [], options = {}) {
220
180
  const { isSet } = this.app.lib.aneka
221
181
  const { uniq, without, get } = this.app.lib._
222
182
  const { parseQuery } = this.app.dobo
@@ -226,30 +186,46 @@ export async function getMultiRefs (records = [], options = {}) {
226
186
  for (const prop of props) {
227
187
  for (const key in prop.ref) {
228
188
  try {
229
- if (get(records, `0._ref.${key}`)) continue
189
+ if (records.length === 0) return
190
+ const isValues = Array.isArray(records[0][prop.name])
191
+ if (get(records, `0._ref.${key}`)) return
230
192
  const ref = prop.ref[key]
231
193
  const rModel = this.app.dobo.getModel(ref.model, true)
232
194
  if (!rModel) return
233
195
  let matches = []
234
- for (const r of records) {
235
- matches.push(prop.name === 'id' ? rModel.sanitizeId(r[prop.name]) : r[prop.name])
196
+ for (const rec of records) {
197
+ const items = isValues ? [...rec[prop.name]] : [rec[prop.name]]
198
+ matches.push(...items.map(item => prop.name === 'id' ? rModel.sanitizeId(item) : item))
236
199
  }
237
200
  matches = uniq(without(matches, undefined, null, NaN)).map(i => i + '')
238
201
  let query = {}
239
202
  query[ref.field] = { $in: matches }
203
+
204
+ const siteIdProp = this.properties.find(item => item.name === 'siteId')
205
+ const siteIdRProp = rModel.properties.find(item => item.name === 'siteId')
206
+ if (siteIdProp && siteIdRProp) query.siteId = records[0].siteId
207
+
240
208
  if (ref.query) query = { $and: [query, parseQuery(ref.query, rModel)] }
241
209
  const filter = { query, limit: matches.length }
242
- const resp = await _getRef.call(this, { ref, rModel, prop, key, options, filter })
243
- if (!resp) continue
244
- const { rOptions, results } = resp
210
+ if (!((typeof options.refs === 'string' && ['*', 'all'].includes(options.refs)) || options.refs.includes(key))) return
211
+ if (ref.fields.length === 0) return
212
+ const { fmt, req } = options
213
+ const fields = [...ref.fields]
214
+ if (!fields.includes(prop.name)) fields.push(prop.name)
215
+ const rOptions = { dataOnly: true, refs: [], fmt, req, fields }
216
+ const results = await rModel.findRecord(filter, rOptions)
245
217
  for (const i in records) {
246
218
  records[i]._ref = records[i]._ref ?? {}
247
219
  const rec = records[i]
248
- const res = results.find(res => (res[ref.field] + '') === rec[prop.name] + '')
249
- if (res) records[i]._ref[key] = await rModel.sanitizeRecord(res, rOptions)
250
- else records[i]._ref[key] = {}
220
+ let items = isValues ? [...rec[prop.name]] : [rec[prop.name]]
221
+ items = items.map(item => item + '')
222
+ const res = results.filter(r => items.includes(r[ref.field] + ''))
223
+ if (res.length === 0) records[i]._ref[key] = isValues ? [] : {}
224
+ else records[i]._ref[key] = isValues ? res : res[0]
251
225
  }
252
- } catch (err) {}
226
+ } catch (err) {
227
+ if (this.app.bajo.config.log.level === 'trace') console.error(err)
228
+ }
253
229
  }
254
230
  }
255
231
  }
@@ -261,7 +237,7 @@ export function buildFilterQuery (filter = {}) {
261
237
  return sanitizeQuery.call(this, query)
262
238
  }
263
239
 
264
- function sanitizeQuery (query = {}, parent) {
240
+ export function sanitizeQuery (query = {}, parent) {
265
241
  const { isPlainObject, isArray, find, cloneDeep } = this.app.lib._
266
242
  const { isSet } = this.app.lib.aneka
267
243
  const { dayjs } = this.app.lib
@@ -1,9 +1,9 @@
1
1
  import { getFilterAndOptions, execHook, execValidation, execModelHook, execDynHook } from './_util.js'
2
2
 
3
3
  export const onlyTypes = ['datetime', 'date', 'time', 'timestamp']
4
- const action = 'bulkCreateRecords'
4
+ const action = 'bulkCreateRecord'
5
5
 
6
- async function bulkCreateRecords (...args) {
6
+ async function bulkCreateRecord (...args) {
7
7
  if (args.length === 0) return this.action(action, ...args)
8
8
  const [bodies = [], opts = {}] = args
9
9
  const { cloneDeep, get } = this.app.lib._
@@ -17,9 +17,9 @@ async function bulkCreateRecords (...args) {
17
17
  inputs[idx] = await this.sanitizeBody({ body: inputs[idx], extFields, strict: true, truncateString, onlyTypes })
18
18
  }
19
19
  }
20
- await execHook.call(this, 'beforeBulkCreateRecords', inputs, options)
21
- await execModelHook.call(this, 'beforeBulkCreateRecords', inputs, options)
22
- await execDynHook.call(this, 'beforeBulkCreateRecords', inputs, options)
20
+ await execHook.call(this, 'beforeBulkCreateRecord', inputs, options)
21
+ await execModelHook.call(this, 'beforeBulkCreateRecord', inputs, options)
22
+ await execDynHook.call(this, 'beforeBulkCreateRecord', inputs, options)
23
23
  if (!noValidation) {
24
24
  for (const input of inputs) {
25
25
  await execValidation.call(this, input, options)
@@ -27,10 +27,10 @@ async function bulkCreateRecords (...args) {
27
27
  }
28
28
  // TODO: bulk don't return anything currently, it should return at least a stat
29
29
  await this.driver._bulkCreateRecords(this, inputs, options)
30
- await execDynHook.call(this, 'afterBulkCreateRecords', inputs, options)
31
- await execModelHook.call(this, 'afterBulkCreateRecords', inputs, options)
32
- await execHook.call(this, 'afterBulkCreateRecords', inputs, options)
30
+ await execDynHook.call(this, 'afterBulkCreateRecord', inputs, options)
31
+ await execModelHook.call(this, 'afterBulkCreateRecord', inputs, options)
32
+ await execHook.call(this, 'afterBulkCreateRecord', inputs, options)
33
33
  return []
34
34
  }
35
35
 
36
- export default bulkCreateRecords
36
+ export default bulkCreateRecord
@@ -4,7 +4,7 @@ const action = 'createAttachment'
4
4
  async function createAttachment (...args) {
5
5
  const { createThumbnail } = this.app.bajoExtra
6
6
  const { thumbSizes: size } = this.app.dobo.config.default.attachment
7
- if (!this.attachment) return
7
+ if (!this.options.attachment) return
8
8
  if (args.length === 0) return this.action(action, ...args)
9
9
  const [id, opts = {}] = args
10
10
  const { fs } = this.app.lib
@@ -1,4 +1,4 @@
1
- import { getFilterAndOptions, execHook, execValidation, execModelHook, execDynHook, getSingleRef, handleReq } from './_util.js'
1
+ import { getFilterAndOptions, execHook, execValidation, execModelHook, execDynHook, getRefs, handleReq } from './_util.js'
2
2
 
3
3
  export const onlyTypes = ['datetime', 'date', 'time', 'timestamp', 'array', 'object']
4
4
  const action = 'createRecord'
@@ -25,7 +25,7 @@ async function createRecord (...args) {
25
25
  result = result ?? {}
26
26
  const { warnings } = getDefaultValues(options)
27
27
  if (!warnings) delete result.warnings
28
- if (isSet(options.refs)) await getSingleRef.call(this, result.data, options)
28
+ if (isSet(options.refs)) await getRefs.call(this, [result.data], options)
29
29
  if (!noResultSanitizer) result.data = await this.sanitizeRecord(result.data, options)
30
30
  await execDynHook.call(this, 'afterCreateRecord', input, result, options)
31
31
  await execModelHook.call(this, 'afterCreateRecord', input, result, options)
@@ -1,4 +1,4 @@
1
- import { getMultiRefs, execHook, execModelHook, execDynHook, getFilterAndOptions, cloneOptions } from './_util.js'
1
+ import { getRefs, execHook, execModelHook, execDynHook, getFilterAndOptions, cloneOptions, sanitizeQuery } from './_util.js'
2
2
  const action = 'findAllRecord'
3
3
 
4
4
  async function native (...args) {
@@ -13,9 +13,10 @@ async function native (...args) {
13
13
  const { hardCap, warnings } = getDefaultValues(options)
14
14
  if (dataOnly) options.count = false
15
15
  const { noResultSanitizer } = options
16
- await execHook.call(this, 'beforeFindRecord', filter, options)
17
- await execModelHook.call(this, 'beforeFindRecord', filter, options)
18
- await execDynHook.call(this, 'beforeFindRecord', filter, options)
16
+ await execHook.call(this, 'beforeFindAllRecord', filter, options)
17
+ await execModelHook.call(this, 'beforeFindAllRecord', filter, options)
18
+ await execDynHook.call(this, 'beforeFindAllRecord', filter, options)
19
+ filter.query = sanitizeQuery.call(this, filter.query)
19
20
  const cFilter = cloneDeep(filter)
20
21
  if (get) {
21
22
  const resp = await get({ model: this, action, filter: cFilter, options })
@@ -37,15 +38,15 @@ async function native (...args) {
37
38
  result.pages = options.count ? Math.ceil(result.count / filter.limit) : undefined
38
39
  if (!warnings) delete result.warnings
39
40
 
40
- if (isSet(options.refs)) await getMultiRefs.call(this, result.data, options)
41
+ if (isSet(options.refs)) await getRefs.call(this, result.data, options)
41
42
  if (!noResultSanitizer) {
42
43
  for (const idx in result.data) {
43
44
  result.data[idx] = await this.sanitizeRecord(result.data[idx], options)
44
45
  }
45
46
  }
46
- await execDynHook.call(this, 'afterFindRecord', filter, result, options)
47
- await execModelHook.call(this, 'afterFindRecord', filter, result, options)
48
- await execHook.call(this, 'afterFindRecord', filter, result, options)
47
+ await execDynHook.call(this, 'afterFindAllRecord', filter, result, options)
48
+ await execModelHook.call(this, 'afterFindAllRecord', filter, result, options)
49
+ await execHook.call(this, 'afterFindAllRecord', filter, result, options)
49
50
  if (set) await set({ model: this, action, filter: cFilter, options, result })
50
51
  return dataOnly ? result.data : result
51
52
  }
@@ -2,7 +2,7 @@ import { mergeAttachmentInfo } from './_util.js'
2
2
  const action = 'findAttachment'
3
3
 
4
4
  async function findAttachment (...args) {
5
- if (!this.attachment) return
5
+ if (!this.options.attachment) return
6
6
  if (args.length === 0) return this.action(action, ...args)
7
7
  const [id, opts = {}] = args
8
8
  const { fastGlob, fs } = this.app.lib
@@ -1,4 +1,4 @@
1
- import { getFilterAndOptions, execHook, execModelHook, execDynHook, getMultiRefs } from './_util.js'
1
+ import { getFilterAndOptions, execHook, execModelHook, execDynHook, getRefs, sanitizeQuery } from './_util.js'
2
2
  const action = 'findRecord'
3
3
 
4
4
  /**
@@ -80,6 +80,7 @@ async function findRecord (...args) {
80
80
  await execHook.call(this, 'beforeFindRecord', filter, options)
81
81
  await execModelHook.call(this, 'beforeFindRecord', filter, options)
82
82
  await execDynHook.call(this, 'beforeFindRecord', filter, options)
83
+ filter.query = sanitizeQuery.call(this, filter.query)
83
84
  const cFilter = cloneDeep(filter)
84
85
  if (get) {
85
86
  const resp = await get({ model: this, action, filter: cFilter, options })
@@ -101,7 +102,7 @@ async function findRecord (...args) {
101
102
  }
102
103
  result.pages = options.count ? Math.ceil(result.count / filter.limit) : undefined
103
104
  if (!warnings) delete result.warnings
104
- if (isSet(options.refs)) await getMultiRefs.call(this, result.data, options)
105
+ if (isSet(options.refs)) await getRefs.call(this, result.data, options)
105
106
  if (!noResultSanitizer) {
106
107
  for (const idx in result.data) {
107
108
  result.data[idx] = await this.sanitizeRecord(result.data[idx], options)
@@ -1,7 +1,7 @@
1
1
  const action = 'getAttachment'
2
2
 
3
3
  async function getAttachment (...args) {
4
- if (!this.attachment) return
4
+ if (!this.options.attachment) return
5
5
  if (args.length === 0) return this.action(action, ...args)
6
6
  let [id, field, file, opts = {}] = args
7
7
  const { find } = this.app.lib._
@@ -1,4 +1,4 @@
1
- import { getFilterAndOptions, execHook, execModelHook, execDynHook, getSingleRef } from './_util.js'
1
+ import { getFilterAndOptions, execHook, execModelHook, execDynHook, getRefs } from './_util.js'
2
2
  const action = 'getRecord'
3
3
 
4
4
  /**
@@ -68,7 +68,7 @@ async function getRecord (...args) {
68
68
  const { warnings } = getDefaultValues(options)
69
69
  if (!warnings) delete result.warnings
70
70
  if (isEmpty(result.data) && !options.throwNotFound) return dataOnly ? undefined : { data: undefined }
71
- if (isSet(options.refs)) await getSingleRef.call(this, result.data, options)
71
+ if (isSet(options.refs)) await getRefs.call(this, [result.data], options)
72
72
  if (!noResultSanitizer) result.data = await this.sanitizeRecord(result.data, options)
73
73
  await execDynHook.call(this, 'afterGetRecord', id, result, options)
74
74
  await execModelHook.call(this, 'afterGetRecord', id, result, options)
@@ -2,7 +2,7 @@ import path from 'path'
2
2
  const action = 'listAttachment'
3
3
 
4
4
  async function listAttachment (...args) {
5
- if (!this.attachment) return
5
+ if (!this.options.attachment) return
6
6
  if (args.length === 0) return this.action(action, ...args)
7
7
  const [params = {}, opts = {}] = args
8
8
  const { map, kebabCase } = this.app.lib._
@@ -33,8 +33,8 @@ async function loadFixtures ({ spinner, ignoreError = true, collectItems = false
33
33
  return
34
34
  }
35
35
  const result = { success: 0, failed: 0 }
36
- const base = path.basename(this.file, path.extname(this.file))
37
- const pattern = resolvePath(`${path.dirname(this.file)}/../fixture/${base}.*`)
36
+ const base = path.basename(this.options.file, path.extname(this.options.file))
37
+ const pattern = resolvePath(`${path.dirname(this.options.file)}/../fixture/${base}.*`)
38
38
  const items = await readConfig(pattern, { ns: this.plugin.ns, baseNs: 'dobo', checkOverride: true, defValue: [] })
39
39
  const opts = { ...options, noMagic: true }
40
40
  for (const item of items) {
@@ -3,7 +3,7 @@ import path from 'path'
3
3
  const action = 'removeAttachment'
4
4
 
5
5
  async function removeAttachment (...args) {
6
- if (!this.attachment) return
6
+ if (!this.options.attachment) return
7
7
  if (args.length === 0) return this.action(action, ...args)
8
8
  const [id, field, file, opts = {}] = args
9
9
  const { fs, fastGlob } = this.app.lib
@@ -1,4 +1,4 @@
1
- import { getFilterAndOptions, execHook, execModelHook, execDynHook, getSingleRef, handleReq, clearCache } from './_util.js'
1
+ import { getFilterAndOptions, execHook, execModelHook, execDynHook, getRefs, handleReq, clearCache } from './_util.js'
2
2
  const action = 'removeRecord'
3
3
 
4
4
  /**
@@ -51,7 +51,7 @@ async function removeRecord (...args) {
51
51
  const { warnings } = getDefaultValues(options)
52
52
  if (!warnings) delete result.warnings
53
53
  if (!noResultSanitizer) result.oldData = await this.sanitizeRecord(result.oldData, options)
54
- if (isSet(options.refs)) await getSingleRef.call(this, result.data, options)
54
+ if (isSet(options.refs)) await getRefs.call(this, [result.data], options)
55
55
  await execDynHook.call(this, 'afterRemoveRecord', id, result, options)
56
56
  await execModelHook.call(this, 'afterRemoveRecord', id, result, options)
57
57
  await execHook.call(this, 'afterRemoveRecord', id, result, options)
@@ -33,12 +33,17 @@ async function sanitizeBody ({ body = {}, partial, strict, extFields = [], noDef
33
33
 
34
34
  const omitted = []
35
35
  const details = []
36
- for (const prop of [...this.properties, ...extFields]) {
36
+ const properties = [...this.properties, ...extFields]
37
+ for (const prop of properties) {
37
38
  try {
38
- if (partial && !has(body, prop.name)) continue
39
+ if (partial && !has(body, prop.name)) {
40
+ if (prop.type === 'array') result[prop.name] = null
41
+ continue
42
+ }
39
43
  result[prop.name] = body[prop.name]
40
- if (body[prop.name] === null) continue
41
- if (isSet(body[prop.name])) sanitize(prop.name, prop.type)
44
+ if (result[prop.name] === null) continue
45
+ if (prop.type === 'array' && isSet(result[prop.name]) && !Array.isArray(result[prop.name])) result[prop.name] = [result[prop.name]]
46
+ if (isSet(result[prop.name])) sanitize(prop.name, prop.type)
42
47
  else {
43
48
  if (isSet(prop.default) && !noDefault) {
44
49
  result[prop.name] = prop.default
@@ -50,7 +55,7 @@ async function sanitizeBody ({ body = {}, partial, strict, extFields = [], noDef
50
55
  }
51
56
  if (truncateString && isSet(result[prop.name]) && ['string', 'text'].includes(prop.type)) result[prop.name] = result[prop.name].slice(0, prop.maxLength)
52
57
  if (prop.name.endsWith('Id') && prop.type === 'string' && ['smallint', 'integer'].includes(this.driver.idField.type)) result[prop.name] = result[prop.name] + ''
53
- if (body[prop.name] === undefined) omitted.push(prop.name)
58
+ if (result[prop.name] === undefined) omitted.push(prop.name)
54
59
  } catch (err) {
55
60
  details.push({ field: prop.name, error: err.message, value: body[prop.name], ext: { type: prop.type } })
56
61
  }
@@ -24,7 +24,7 @@ async function sanitizeRecord (record = {}, opts = {}) {
24
24
  if (!newFields.includes('id')) newFields.unshift('id')
25
25
  newFields = without(newFields, ...allHidden)
26
26
  const body = fillObject(record, newFields, null)
27
- const newRecord = await this.sanitizeBody({ body, noDefault: true })
27
+ const newRecord = await this.sanitizeBody({ body, partial: true, noDefault: true })
28
28
  if (record._ref) newRecord._ref = cloneDeep(record._ref)
29
29
  for (const key in newRecord) {
30
30
  const prop = this.getProperty(key)
@@ -35,6 +35,7 @@ async function sanitizeRecord (record = {}, opts = {}) {
35
35
  }
36
36
  if (opts.fmt) {
37
37
  newRecord._fmt = cloneDeep(newRecord)
38
+ delete newRecord._fmt._ref
38
39
  for (const key in newRecord) {
39
40
  const prop = this.getProperty(key)
40
41
  if (!prop) continue
@@ -1,4 +1,4 @@
1
- import { getFilterAndOptions, execHook, execValidation, execModelHook, execDynHook, getSingleRef, handleReq, clearCache } from './_util.js'
1
+ import { getFilterAndOptions, execHook, execValidation, execModelHook, execDynHook, getRefs, handleReq, clearCache } from './_util.js'
2
2
  import { onlyTypes } from './create-record.js'
3
3
  const action = 'updateRecord'
4
4
 
@@ -72,7 +72,7 @@ async function updateRecord (...args) {
72
72
  if (noResult) return
73
73
  const { warnings } = getDefaultValues(options)
74
74
  if (!warnings) delete result.warnings
75
- if (isSet(options.refs)) await getSingleRef.call(this, result.data, options)
75
+ if (isSet(options.refs)) await getRefs.call(this, [result.data], options)
76
76
  if (!noResultSanitizer) {
77
77
  result.data = await this.sanitizeRecord(result.data, options)
78
78
  result.oldData = await this.sanitizeRecord(result.oldData, options)
@@ -1,4 +1,4 @@
1
- import { getFilterAndOptions, execHook, execModelHook, execDynHook, execValidation, getSingleRef, handleReq, clearCache } from './_util.js'
1
+ import { getFilterAndOptions, execHook, execModelHook, execDynHook, execValidation, getRefs, handleReq, clearCache } from './_util.js'
2
2
  const action = 'upsertRecord'
3
3
 
4
4
  async function native (body = {}, opts = {}) {
@@ -21,7 +21,7 @@ async function native (body = {}, opts = {}) {
21
21
  if (noResult) return
22
22
  const { warnings } = getDefaultValues(options)
23
23
  if (!warnings) delete result.warnings
24
- if (isSet(options.refs)) await getSingleRef.call(this, result.data, options)
24
+ if (isSet(options.refs)) await getRefs.call(this, [result.data], options)
25
25
  if (!noResultSanitizer) result.data = await this.sanitizeRecord(result.data, options)
26
26
  await execDynHook.call(this, 'afterUpsertRecord', input, result, options)
27
27
  await execModelHook.call(this, 'afterUpsertRecord', input, result, options)
@@ -23,7 +23,7 @@ import sanitizeBody from './model/sanitize-body.js'
23
23
  import sanitizeRecord from './model/sanitize-record.js'
24
24
  import sanitizeId from './model/sanitize-id.js'
25
25
  import upsertRecord from './model/upsert-record.js'
26
- import bulkCreateRecords from './model/bulk-create-records.js'
26
+ import bulkCreateRecord from './model/bulk-create-record.js'
27
27
  import listAttachment from './model/list-attachment.js'
28
28
  import transaction from './model/transaction.js'
29
29
  import validate from './model/validate.js'
@@ -109,6 +109,16 @@ async function modelFactory () {
109
109
  return !!this.getProperty(name)
110
110
  }
111
111
 
112
+ syncIdField = (idField) => {
113
+ const { cloneDeep, findIndex } = this.app.lib._
114
+ if (!this.driver) return
115
+ if (!idField) idField = cloneDeep(this.driver.idField)
116
+ const idx = findIndex(this.properties, { name: 'id' })
117
+ if (idx === -1) return
118
+ this.properties.splice(idx, 1)
119
+ this.properties.unshift(idField)
120
+ }
121
+
112
122
  _simpleLookup = async (value, options = {}) => {
113
123
  const { get, isEmpty, isString, isPlainObject, isArray } = this.app.lib._
114
124
  let model
@@ -144,7 +154,7 @@ async function modelFactory () {
144
154
  findAllRecord = findAllRecord
145
155
 
146
156
  transaction = transaction
147
- bulkCreateRecords = bulkCreateRecords
157
+ bulkCreateRecord = bulkCreateRecord
148
158
 
149
159
  createAggregate = createAggregate
150
160
  createHistogram = createHistogram
@@ -170,6 +180,7 @@ async function modelFactory () {
170
180
  findRecords = findRecord
171
181
  findAllRecords = findAllRecord
172
182
  listAttachments = listAttachment
183
+ bulkCreateRecords = bulkCreateRecord
173
184
 
174
185
  getField = (name) => this.getProperty(name)
175
186
  hasField = (name) => this.hasProperty(name)
@@ -182,7 +193,6 @@ async function modelFactory () {
182
193
  }
183
194
 
184
195
  this.app.baseClass.DoboModel = DoboModel
185
- return DoboModel
186
196
  }
187
197
 
188
198
  export default modelFactory
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dobo",
3
- "version": "2.22.0",
3
+ "version": "2.23.0",
4
4
  "description": "DBMS for Bajo Framework",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/wiki/CHANGES.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changes
2
2
 
3
+ ## 2026-05-11
4
+
5
+ - [2.23.0] Add ```beforeBulkCreate``` model hook on ```dobo:unique``` feature
6
+ - [2.23.0] Add ```beforeBulkCreate``` model hook on ```dobo:updatedAt``` feature
7
+ - [2.23.0] Add ```beforeBulkCreate``` model hook on ```dobo:unique``` feature
8
+ - [2.23.0] Add ```connection.initDriver()```
9
+ - [2.23.0] Move ```model.file``` in model definition to ```model.options.file```
10
+ - [2.23.0] Move ```model.attachment``` in model definition to ```model.options.attachment```
11
+ - [2.23.0] Add ```model.buildStart()``` and ```model.buildEnd()``` in model definition
12
+ - [2.23.0] Add ```null``` driver
13
+ - [2.23.0] Add ```model.syncIdField()```
14
+ - [2.23.0] Rename method to ```model.bulkCreateRecord``` instead ```bulkCreateRecords```. The later name now serve only as alias
15
+ - [2.23.0] Remove ```getSingleRef()``` and ```getMultiRefs()```, use ```getRefs()``` instead
16
+ - [2.23.0] Add reference support for ```array``` column type
17
+ - [2.23.0] Bug fix in ```model.findAllRecord()```, now use correctly hook names
18
+ - [2.23.0] Bug fix in ```model.sanitizeBody()```
19
+ - [2.23.0] Bug fix in ```model.sanitizeRecord()```
20
+
21
+ ## 2026-05-03
22
+
23
+ - [2.22.1] Bug fix in ```dobo:image``` feature
24
+
3
25
  ## 2026-05-02
4
26
 
5
27
  - [2.22.0] Add auto thumbnail creation when image attachment is uploaded
@@ -83,7 +105,7 @@
83
105
  - [2.16.0] Rewrite ```getDefaultValues()``` to base on ```req.getSetting()```
84
106
  - [2.16.0] All inter site admins are now exempts from ```immutable``` row
85
107
  - [2.16.0] Bug fix in ```collect-models.js```
86
- - [2.16.0] Bug fix in ```getSingleRef()``` and ```getMultiRefs()```
108
+ - [2.16.0] Bug fix in ```getRefs()``` and ```getRefs()```
87
109
  - [2.16.0] Add feature to return formatted row(s) with ```options.formatValue```
88
110
  - [2.16.0] If row is formatted, add feature to save original row in ```_orig``` with ```options.retainOriginalValue```
89
111
 
@@ -117,7 +139,7 @@
117
139
 
118
140
  ## 2026-03-26
119
141
 
120
- - [2.11.4] Exceptions thrown in ```getSingleRef()``` && ```getMultiRefs()``` will be catched and are ignored
142
+ - [2.11.4] Exceptions thrown in ```getRefs()``` && ```getRefs()``` will be catched and are ignored
121
143
 
122
144
  ## 2026-03-25
123
145
 
@@ -222,7 +244,7 @@
222
244
 
223
245
  ## 2026-01-29
224
246
 
225
- - [2.4.0] Add ```bulkCreateRecords()``` on model & driver
247
+ - [2.4.0] Add ```bulkCreateRecord()``` on model & driver
226
248
  - [2.4.0] Add ```execModelHook()```
227
249
  - [2.4.0] Bug fix in models collection
228
250
  - [2.4.0] Add ```DoboAction``` to the ```app.baseClass```