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.
- package/extend/bajo/intl/en-US.json +1 -1
- package/extend/bajo/intl/id.json +1 -0
- package/extend/dobo/driver/memory.js +26 -25
- package/extend/dobo/feature/created-at.js +14 -3
- package/extend/dobo/feature/image.js +14 -7
- package/extend/dobo/feature/removed-at.js +7 -0
- package/extend/dobo/feature/unique.js +18 -6
- package/extend/dobo/feature/updated-at.js +15 -5
- package/index.js +1 -1
- package/lib/collect-connections.js +3 -11
- package/lib/collect-drivers.js +2 -2
- package/lib/collect-features.js +2 -2
- package/lib/collect-models.js +25 -16
- package/lib/factory/action.js +0 -1
- package/lib/factory/connection.js +26 -4
- package/lib/factory/driver.js +34 -19
- package/lib/factory/feature.js +0 -1
- package/lib/factory/model/_util.js +37 -61
- package/lib/factory/model/{bulk-create-records.js → bulk-create-record.js} +9 -9
- package/lib/factory/model/create-attachment.js +1 -1
- package/lib/factory/model/create-record.js +2 -2
- package/lib/factory/model/find-all-record.js +9 -8
- package/lib/factory/model/find-attachment.js +1 -1
- package/lib/factory/model/find-record.js +3 -2
- package/lib/factory/model/get-attachment.js +1 -1
- package/lib/factory/model/get-record.js +2 -2
- package/lib/factory/model/list-attachment.js +1 -1
- package/lib/factory/model/load-fixtures.js +2 -2
- package/lib/factory/model/remove-attachment.js +1 -1
- package/lib/factory/model/remove-record.js +2 -2
- package/lib/factory/model/sanitize-body.js +10 -5
- package/lib/factory/model/sanitize-record.js +2 -1
- package/lib/factory/model/update-record.js +2 -2
- package/lib/factory/model/upsert-record.js +2 -2
- package/lib/factory/model.js +13 -3
- package/package.json +1 -1
- 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
|
-
"
|
|
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",
|
package/extend/bajo/intl/id.json
CHANGED
|
@@ -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
|
|
48
|
-
fs.ensureDirSync(
|
|
49
|
-
conn.
|
|
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 (
|
|
53
|
-
|
|
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
|
|
60
|
-
|
|
61
|
-
|
|
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.
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
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
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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
|
@@ -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
|
-
|
|
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
|
-
|
|
40
|
-
|
|
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')
|
package/lib/collect-drivers.js
CHANGED
|
@@ -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
|
-
|
|
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 })
|
package/lib/collect-features.js
CHANGED
|
@@ -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
|
-
|
|
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)
|
package/lib/collect-models.js
CHANGED
|
@@ -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
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
if (
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
package/lib/factory/action.js
CHANGED
|
@@ -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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
package/lib/factory/driver.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
441
|
+
throw this.plugin.error('notSupportedDriver%s%s%s', this.app.t('method'), 'findRecord', this.name)
|
|
438
442
|
}
|
|
439
443
|
|
|
440
|
-
async
|
|
441
|
-
throw this.plugin.error('
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
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
|
-
|
|
477
|
+
this.app.baseClass.DoboNullDriver = DoboNullDriver
|
|
463
478
|
}
|
|
464
479
|
|
|
465
480
|
export default driverFactory
|
package/lib/factory/feature.js
CHANGED
|
@@ -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
|
|
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 (
|
|
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
|
|
235
|
-
|
|
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
|
-
|
|
243
|
-
if (
|
|
244
|
-
const {
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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 = '
|
|
4
|
+
const action = 'bulkCreateRecord'
|
|
5
5
|
|
|
6
|
-
async function
|
|
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, '
|
|
21
|
-
await execModelHook.call(this, '
|
|
22
|
-
await execDynHook.call(this, '
|
|
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, '
|
|
31
|
-
await execModelHook.call(this, '
|
|
32
|
-
await execHook.call(this, '
|
|
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
|
|
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,
|
|
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
|
|
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 {
|
|
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, '
|
|
17
|
-
await execModelHook.call(this, '
|
|
18
|
-
await execDynHook.call(this, '
|
|
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
|
|
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, '
|
|
47
|
-
await execModelHook.call(this, '
|
|
48
|
-
await execHook.call(this, '
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
|
|
36
|
+
const properties = [...this.properties, ...extFields]
|
|
37
|
+
for (const prop of properties) {
|
|
37
38
|
try {
|
|
38
|
-
if (partial && !has(body, prop.name))
|
|
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 (
|
|
41
|
-
if (isSet(
|
|
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 (
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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)
|
package/lib/factory/model.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
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 ```
|
|
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 ```
|
|
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 ```
|
|
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```
|