dobo 2.22.1 → 2.24.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 (38) 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/immutable.js +1 -2
  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 +131 -26
  17. package/lib/factory/feature.js +0 -1
  18. package/lib/factory/model/_util.js +37 -67
  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 +24 -10
  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 +11 -6
  32. package/lib/factory/model/sanitize-record.js +2 -1
  33. package/lib/factory/model/transaction.js +10 -1
  34. package/lib/factory/model/update-record.js +3 -3
  35. package/lib/factory/model/upsert-record.js +2 -2
  36. package/lib/factory/model.js +18 -5
  37. package/package.json +1 -1
  38. package/wiki/CHANGES.md +33 -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
  }
@@ -11,8 +11,7 @@ async function immutable (opts = {}) {
11
11
  return {
12
12
  properties: {
13
13
  name: opts.field,
14
- type: 'boolean',
15
- hidden: true
14
+ type: 'boolean'
16
15
  },
17
16
  hooks: [{
18
17
  name: 'beforeUpdateRecord',
@@ -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